admin.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  18. #
  19. '''
  20. Qubes OS Management API
  21. '''
  22. import asyncio
  23. import functools
  24. import itertools
  25. import os
  26. import string
  27. import subprocess
  28. import libvirt
  29. import pkg_resources
  30. import yaml
  31. import qubes.api
  32. import qubes.backup
  33. import qubes.config
  34. import qubes.devices
  35. import qubes.firewall
  36. import qubes.storage
  37. import qubes.utils
  38. import qubes.vm
  39. import qubes.vm.adminvm
  40. import qubes.vm.qubesvm
  41. class QubesMgmtEventsDispatcher(object):
  42. def __init__(self, filters, send_event):
  43. self.filters = filters
  44. self.send_event = send_event
  45. def vm_handler(self, subject, event, **kwargs):
  46. # do not send internal events
  47. if event.startswith('admin-permission:'):
  48. return
  49. if event.startswith('device-get:'):
  50. return
  51. if event.startswith('device-list:'):
  52. return
  53. if event.startswith('device-list-attached:'):
  54. return
  55. if event in ('domain-is-fully-usable',):
  56. return
  57. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  58. self.filters)):
  59. return
  60. self.send_event(subject, event, **kwargs)
  61. def app_handler(self, subject, event, **kwargs):
  62. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  63. self.filters)):
  64. return
  65. self.send_event(subject, event, **kwargs)
  66. def on_domain_add(self, subject, event, vm):
  67. # pylint: disable=unused-argument
  68. vm.add_handler('*', self.vm_handler)
  69. def on_domain_delete(self, subject, event, vm):
  70. # pylint: disable=unused-argument
  71. vm.remove_handler('*', self.vm_handler)
  72. class QubesAdminAPI(qubes.api.AbstractQubesAPI):
  73. '''Implementation of Qubes Management API calls
  74. This class contains all the methods available in the main API.
  75. .. seealso::
  76. https://www.qubes-os.org/doc/mgmt1/
  77. '''
  78. SOCKNAME = '/var/run/qubesd.sock'
  79. @qubes.api.method('admin.vmclass.List', no_payload=True,
  80. scope='global', read=True)
  81. @asyncio.coroutine
  82. def vmclass_list(self):
  83. '''List all VM classes'''
  84. assert not self.arg
  85. assert self.dest.name == 'dom0'
  86. entrypoints = self.fire_event_for_filter(
  87. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  88. return ''.join('{}\n'.format(ep.name)
  89. for ep in entrypoints)
  90. @qubes.api.method('admin.vm.List', no_payload=True,
  91. scope='global', read=True)
  92. @asyncio.coroutine
  93. def vm_list(self):
  94. '''List all the domains'''
  95. assert not self.arg
  96. if self.dest.name == 'dom0':
  97. domains = self.fire_event_for_filter(self.app.domains)
  98. else:
  99. domains = self.fire_event_for_filter([self.dest])
  100. return ''.join('{} class={} state={}\n'.format(
  101. vm.name,
  102. vm.__class__.__name__,
  103. vm.get_power_state())
  104. for vm in sorted(domains))
  105. @qubes.api.method('admin.vm.property.List', no_payload=True,
  106. scope='local', read=True)
  107. @asyncio.coroutine
  108. def vm_property_list(self):
  109. '''List all properties on a qube'''
  110. return self._property_list(self.dest)
  111. @qubes.api.method('admin.property.List', no_payload=True,
  112. scope='global', read=True)
  113. @asyncio.coroutine
  114. def property_list(self):
  115. '''List all global properties'''
  116. assert self.dest.name == 'dom0'
  117. return self._property_list(self.app)
  118. def _property_list(self, dest):
  119. assert not self.arg
  120. properties = self.fire_event_for_filter(dest.property_list())
  121. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  122. @qubes.api.method('admin.vm.property.Get', no_payload=True,
  123. scope='local', read=True)
  124. @asyncio.coroutine
  125. def vm_property_get(self):
  126. '''Get a value of one property'''
  127. return self._property_get(self.dest)
  128. @qubes.api.method('admin.property.Get', no_payload=True,
  129. scope='global', read=True)
  130. @asyncio.coroutine
  131. def property_get(self):
  132. '''Get a value of one global property'''
  133. assert self.dest.name == 'dom0'
  134. return self._property_get(self.app)
  135. def _property_get(self, dest):
  136. if self.arg not in dest.property_list():
  137. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  138. self.fire_event_for_permission()
  139. property_def = dest.property_get_def(self.arg)
  140. # explicit list to be sure that it matches protocol spec
  141. if isinstance(property_def, qubes.vm.VMProperty):
  142. property_type = 'vm'
  143. elif property_def.type is int:
  144. property_type = 'int'
  145. elif property_def.type is bool:
  146. property_type = 'bool'
  147. elif self.arg == 'label':
  148. property_type = 'label'
  149. else:
  150. property_type = 'str'
  151. try:
  152. value = getattr(dest, self.arg)
  153. except AttributeError:
  154. return 'default=True type={} '.format(property_type)
  155. else:
  156. return 'default={} type={} {}'.format(
  157. str(dest.property_is_default(self.arg)),
  158. property_type,
  159. str(value) if value is not None else '')
  160. @qubes.api.method('admin.vm.property.GetDefault', no_payload=True,
  161. scope='local', read=True)
  162. @asyncio.coroutine
  163. def vm_property_get_default(self):
  164. '''Get a value of one property'''
  165. return self._property_get_default(self.dest)
  166. @qubes.api.method('admin.property.GetDefault', no_payload=True,
  167. scope='global', read=True)
  168. @asyncio.coroutine
  169. def property_get_default(self):
  170. '''Get a value of one global property'''
  171. assert self.dest.name == 'dom0'
  172. return self._property_get_default(self.app)
  173. def _property_get_default(self, dest):
  174. if self.arg not in dest.property_list():
  175. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  176. self.fire_event_for_permission()
  177. property_def = dest.property_get_def(self.arg)
  178. # explicit list to be sure that it matches protocol spec
  179. if isinstance(property_def, qubes.vm.VMProperty):
  180. property_type = 'vm'
  181. elif property_def.type is int:
  182. property_type = 'int'
  183. elif property_def.type is bool:
  184. property_type = 'bool'
  185. elif self.arg == 'label':
  186. property_type = 'label'
  187. else:
  188. property_type = 'str'
  189. try:
  190. value = property_def.get_default(dest)
  191. except AttributeError:
  192. return None
  193. else:
  194. return 'type={} {}'.format(
  195. property_type,
  196. str(value) if value is not None else '')
  197. @qubes.api.method('admin.vm.property.Set',
  198. scope='local', write=True)
  199. @asyncio.coroutine
  200. def vm_property_set(self, untrusted_payload):
  201. '''Set property value'''
  202. return self._property_set(self.dest,
  203. untrusted_payload=untrusted_payload)
  204. @qubes.api.method('admin.property.Set',
  205. scope='global', write=True)
  206. @asyncio.coroutine
  207. def property_set(self, untrusted_payload):
  208. '''Set property value'''
  209. assert self.dest.name == 'dom0'
  210. return self._property_set(self.app,
  211. untrusted_payload=untrusted_payload)
  212. def _property_set(self, dest, untrusted_payload):
  213. if self.arg not in dest.property_list():
  214. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  215. property_def = dest.property_get_def(self.arg)
  216. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  217. self.fire_event_for_permission(newvalue=newvalue)
  218. setattr(dest, self.arg, newvalue)
  219. self.app.save()
  220. @qubes.api.method('admin.vm.property.Help', no_payload=True,
  221. scope='local', read=True)
  222. @asyncio.coroutine
  223. def vm_property_help(self):
  224. '''Get help for one property'''
  225. return self._property_help(self.dest)
  226. @qubes.api.method('admin.property.Help', no_payload=True,
  227. scope='global', read=True)
  228. @asyncio.coroutine
  229. def property_help(self):
  230. '''Get help for one property'''
  231. assert self.dest.name == 'dom0'
  232. return self._property_help(self.app)
  233. def _property_help(self, dest):
  234. if self.arg not in dest.property_list():
  235. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  236. self.fire_event_for_permission()
  237. try:
  238. doc = dest.property_get_def(self.arg).__doc__
  239. except AttributeError:
  240. return ''
  241. return qubes.utils.format_doc(doc)
  242. @qubes.api.method('admin.vm.property.Reset', no_payload=True,
  243. scope='local', write=True)
  244. @asyncio.coroutine
  245. def vm_property_reset(self):
  246. '''Reset a property to a default value'''
  247. return self._property_reset(self.dest)
  248. @qubes.api.method('admin.property.Reset', no_payload=True,
  249. scope='global', write=True)
  250. @asyncio.coroutine
  251. def property_reset(self):
  252. '''Reset a property to a default value'''
  253. assert self.dest.name == 'dom0'
  254. return self._property_reset(self.app)
  255. def _property_reset(self, dest):
  256. if self.arg not in dest.property_list():
  257. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  258. self.fire_event_for_permission()
  259. delattr(dest, self.arg)
  260. self.app.save()
  261. @qubes.api.method('admin.vm.volume.List', no_payload=True,
  262. scope='local', read=True)
  263. @asyncio.coroutine
  264. def vm_volume_list(self):
  265. assert not self.arg
  266. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  267. return ''.join('{}\n'.format(name) for name in volume_names)
  268. @qubes.api.method('admin.vm.volume.Info', no_payload=True,
  269. scope='local', read=True)
  270. @asyncio.coroutine
  271. def vm_volume_info(self):
  272. assert self.arg in self.dest.volumes.keys()
  273. self.fire_event_for_permission()
  274. volume = self.dest.volumes[self.arg]
  275. # properties defined in API
  276. volume_properties = [
  277. 'pool', 'vid', 'size', 'usage', 'rw', 'source',
  278. 'save_on_stop', 'snap_on_start', 'revisions_to_keep', 'is_outdated']
  279. def _serialize(value):
  280. if callable(value):
  281. value = value()
  282. if value is None:
  283. value = ''
  284. return str(value)
  285. return ''.join('{}={}\n'.format(key, _serialize(getattr(volume, key)))
  286. for key in volume_properties)
  287. @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True,
  288. scope='local', read=True)
  289. @asyncio.coroutine
  290. def vm_volume_listsnapshots(self):
  291. assert self.arg in self.dest.volumes.keys()
  292. volume = self.dest.volumes[self.arg]
  293. id_to_timestamp = volume.revisions
  294. revisions = sorted(id_to_timestamp, key=id_to_timestamp.__getitem__)
  295. revisions = self.fire_event_for_filter(revisions)
  296. return ''.join('{}\n'.format(revision) for revision in revisions)
  297. @qubes.api.method('admin.vm.volume.Revert',
  298. scope='local', write=True)
  299. @asyncio.coroutine
  300. def vm_volume_revert(self, untrusted_payload):
  301. assert self.arg in self.dest.volumes.keys()
  302. untrusted_revision = untrusted_payload.decode('ascii').strip()
  303. del untrusted_payload
  304. volume = self.dest.volumes[self.arg]
  305. snapshots = volume.revisions
  306. assert untrusted_revision in snapshots
  307. revision = untrusted_revision
  308. self.fire_event_for_permission(volume=volume, revision=revision)
  309. ret = volume.revert(revision)
  310. if asyncio.iscoroutine(ret):
  311. yield from ret
  312. self.app.save()
  313. # write=True because this allow to clone VM - and most likely modify that
  314. # one - still having the same data
  315. @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True,
  316. scope='local', write=True)
  317. @asyncio.coroutine
  318. def vm_volume_clone_from(self):
  319. assert self.arg in self.dest.volumes.keys()
  320. volume = self.dest.volumes[self.arg]
  321. self.fire_event_for_permission(volume=volume)
  322. token = qubes.utils.random_string(32)
  323. # save token on self.app, as self is not persistent
  324. if not hasattr(self.app, 'api_admin_pending_clone'):
  325. self.app.api_admin_pending_clone = {}
  326. # don't handle collisions any better - if someone is so much out of
  327. # luck, can try again anyway
  328. assert token not in self.app.api_admin_pending_clone
  329. self.app.api_admin_pending_clone[token] = volume
  330. return token
  331. @qubes.api.method('admin.vm.volume.CloneTo',
  332. scope='local', write=True)
  333. @asyncio.coroutine
  334. def vm_volume_clone_to(self, untrusted_payload):
  335. assert self.arg in self.dest.volumes.keys()
  336. untrusted_token = untrusted_payload.decode('ascii').strip()
  337. del untrusted_payload
  338. assert untrusted_token in getattr(self.app,
  339. 'api_admin_pending_clone', {})
  340. token = untrusted_token
  341. del untrusted_token
  342. src_volume = self.app.api_admin_pending_clone[token]
  343. del self.app.api_admin_pending_clone[token]
  344. # make sure the volume still exists, but invalidate token anyway
  345. assert str(src_volume.pool) in self.app.pools
  346. assert src_volume in self.app.pools[str(src_volume.pool)].volumes
  347. dst_volume = self.dest.volumes[self.arg]
  348. self.fire_event_for_permission(src_volume=src_volume,
  349. dst_volume=dst_volume)
  350. op_retval = dst_volume.import_volume(src_volume)
  351. # clone/import functions may be either synchronous or asynchronous
  352. # in the later case, we need to wait for them to finish
  353. if asyncio.iscoroutine(op_retval):
  354. op_retval = yield from op_retval
  355. self.dest.volumes[self.arg] = op_retval
  356. self.app.save()
  357. @qubes.api.method('admin.vm.volume.Resize',
  358. scope='local', write=True)
  359. @asyncio.coroutine
  360. def vm_volume_resize(self, untrusted_payload):
  361. assert self.arg in self.dest.volumes.keys()
  362. untrusted_size = untrusted_payload.decode('ascii').strip()
  363. del untrusted_payload
  364. assert untrusted_size.isdigit() # only digits, forbid '-' too
  365. assert len(untrusted_size) <= 20 # limit to about 2^64
  366. size = int(untrusted_size)
  367. self.fire_event_for_permission(size=size)
  368. yield from self.dest.storage.resize(self.arg, size)
  369. self.app.save()
  370. @qubes.api.method('admin.vm.volume.Import', no_payload=True,
  371. scope='local', write=True)
  372. @asyncio.coroutine
  373. def vm_volume_import(self):
  374. '''Import volume data.
  375. Note that this function only returns a path to where data should be
  376. written, actual importing is done by a script in /etc/qubes-rpc
  377. When the script finish importing, it will trigger
  378. internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
  379. payload) and response from that call will be actually send to the
  380. caller.
  381. '''
  382. assert self.arg in self.dest.volumes.keys()
  383. self.fire_event_for_permission()
  384. if not self.dest.is_halted():
  385. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  386. path = self.dest.storage.import_data(self.arg)
  387. assert ' ' not in path
  388. size = self.dest.volumes[self.arg].size
  389. # when we know the action is allowed, inform extensions that it will
  390. # be performed
  391. self.dest.fire_event('domain-volume-import-begin', volume=self.arg)
  392. return '{} {}'.format(size, path)
  393. @qubes.api.method('admin.vm.volume.Set.revisions_to_keep',
  394. scope='local', write=True)
  395. @asyncio.coroutine
  396. def vm_volume_set_revisions_to_keep(self, untrusted_payload):
  397. assert self.arg in self.dest.volumes.keys()
  398. try:
  399. untrusted_value = int(untrusted_payload.decode('ascii'))
  400. except (UnicodeDecodeError, ValueError):
  401. raise qubes.api.ProtocolError('Invalid value')
  402. del untrusted_payload
  403. assert untrusted_value >= 0
  404. newvalue = untrusted_value
  405. del untrusted_value
  406. self.fire_event_for_permission(newvalue=newvalue)
  407. self.dest.volumes[self.arg].revisions_to_keep = newvalue
  408. self.app.save()
  409. @qubes.api.method('admin.vm.volume.Set.rw',
  410. scope='local', write=True)
  411. @asyncio.coroutine
  412. def vm_volume_set_rw(self, untrusted_payload):
  413. assert self.arg in self.dest.volumes.keys()
  414. try:
  415. newvalue = qubes.property.bool(None, None,
  416. untrusted_payload.decode('ascii'))
  417. except (UnicodeDecodeError, ValueError):
  418. raise qubes.api.ProtocolError('Invalid value')
  419. del untrusted_payload
  420. self.fire_event_for_permission(newvalue=newvalue)
  421. if not self.dest.is_halted():
  422. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  423. self.dest.volumes[self.arg].rw = newvalue
  424. self.app.save()
  425. @qubes.api.method('admin.vm.tag.List', no_payload=True,
  426. scope='local', read=True)
  427. @asyncio.coroutine
  428. def vm_tag_list(self):
  429. assert not self.arg
  430. tags = self.dest.tags
  431. tags = self.fire_event_for_filter(tags)
  432. return ''.join('{}\n'.format(tag) for tag in sorted(tags))
  433. @qubes.api.method('admin.vm.tag.Get', no_payload=True,
  434. scope='local', read=True)
  435. @asyncio.coroutine
  436. def vm_tag_get(self):
  437. qubes.vm.Tags.validate_tag(self.arg)
  438. self.fire_event_for_permission()
  439. return '1' if self.arg in self.dest.tags else '0'
  440. @qubes.api.method('admin.vm.tag.Set', no_payload=True,
  441. scope='local', write=True)
  442. @asyncio.coroutine
  443. def vm_tag_set(self):
  444. qubes.vm.Tags.validate_tag(self.arg)
  445. self.fire_event_for_permission()
  446. self.dest.tags.add(self.arg)
  447. self.app.save()
  448. @qubes.api.method('admin.vm.tag.Remove', no_payload=True,
  449. scope='local', write=True)
  450. @asyncio.coroutine
  451. def vm_tag_remove(self):
  452. qubes.vm.Tags.validate_tag(self.arg)
  453. self.fire_event_for_permission()
  454. try:
  455. self.dest.tags.remove(self.arg)
  456. except KeyError:
  457. raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
  458. self.app.save()
  459. @qubes.api.method('admin.pool.List', no_payload=True,
  460. scope='global', read=True)
  461. @asyncio.coroutine
  462. def pool_list(self):
  463. assert not self.arg
  464. assert self.dest.name == 'dom0'
  465. pools = self.fire_event_for_filter(self.app.pools)
  466. return ''.join('{}\n'.format(pool) for pool in pools)
  467. @qubes.api.method('admin.pool.ListDrivers', no_payload=True,
  468. scope='global', read=True)
  469. @asyncio.coroutine
  470. def pool_listdrivers(self):
  471. assert self.dest.name == 'dom0'
  472. assert not self.arg
  473. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  474. return ''.join('{} {}\n'.format(
  475. driver,
  476. ' '.join(qubes.storage.driver_parameters(driver)))
  477. for driver in drivers)
  478. @qubes.api.method('admin.pool.Info', no_payload=True,
  479. scope='global', read=True)
  480. @asyncio.coroutine
  481. def pool_info(self):
  482. assert self.dest.name == 'dom0'
  483. assert self.arg in self.app.pools.keys()
  484. pool = self.app.pools[self.arg]
  485. self.fire_event_for_permission(pool=pool)
  486. other_info = ''
  487. pool_size = pool.size
  488. if pool_size is not None:
  489. other_info += 'size={}\n'.format(pool_size)
  490. pool_usage = pool.usage
  491. if pool_usage is not None:
  492. other_info += 'usage={}\n'.format(pool_usage)
  493. try:
  494. included_in = pool.included_in(self.app)
  495. if included_in:
  496. other_info += 'included_in={}\n'.format(str(included_in))
  497. except NotImplementedError:
  498. pass
  499. return ''.join('{}={}\n'.format(prop, val)
  500. for prop, val in sorted(pool.config.items())) + \
  501. other_info
  502. @qubes.api.method('admin.pool.Add',
  503. scope='global', write=True)
  504. @asyncio.coroutine
  505. def pool_add(self, untrusted_payload):
  506. assert self.dest.name == 'dom0'
  507. drivers = qubes.storage.pool_drivers()
  508. assert self.arg in drivers
  509. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  510. del untrusted_payload
  511. assert all(('=' in line) for line in untrusted_pool_config)
  512. # pairs of (option, value)
  513. untrusted_pool_config = [line.split('=', 1)
  514. for line in untrusted_pool_config]
  515. # reject duplicated options
  516. assert len(set(x[0] for x in untrusted_pool_config)) == \
  517. len([x[0] for x in untrusted_pool_config])
  518. # and convert to dict
  519. untrusted_pool_config = dict(untrusted_pool_config)
  520. assert 'name' in untrusted_pool_config
  521. untrusted_pool_name = untrusted_pool_config.pop('name')
  522. allowed_chars = string.ascii_letters + string.digits + '-_.'
  523. assert all(c in allowed_chars for c in untrusted_pool_name)
  524. pool_name = untrusted_pool_name
  525. assert pool_name not in self.app.pools
  526. driver_parameters = qubes.storage.driver_parameters(self.arg)
  527. assert all(key in driver_parameters for key in untrusted_pool_config)
  528. pool_config = untrusted_pool_config
  529. self.fire_event_for_permission(name=pool_name,
  530. pool_config=pool_config)
  531. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  532. self.app.save()
  533. @qubes.api.method('admin.pool.Remove', no_payload=True,
  534. scope='global', write=True)
  535. @asyncio.coroutine
  536. def pool_remove(self):
  537. assert self.dest.name == 'dom0'
  538. assert self.arg in self.app.pools.keys()
  539. self.fire_event_for_permission()
  540. self.app.remove_pool(self.arg)
  541. self.app.save()
  542. @qubes.api.method('admin.pool.Set.revisions_to_keep',
  543. scope='global', write=True)
  544. @asyncio.coroutine
  545. def pool_set_revisions_to_keep(self, untrusted_payload):
  546. assert self.dest.name == 'dom0'
  547. assert self.arg in self.app.pools.keys()
  548. pool = self.app.pools[self.arg]
  549. try:
  550. untrusted_value = int(untrusted_payload.decode('ascii'))
  551. except (UnicodeDecodeError, ValueError):
  552. raise qubes.api.ProtocolError('Invalid value')
  553. del untrusted_payload
  554. assert untrusted_value >= 0
  555. newvalue = untrusted_value
  556. del untrusted_value
  557. self.fire_event_for_permission(newvalue=newvalue)
  558. pool.revisions_to_keep = newvalue
  559. self.app.save()
  560. @qubes.api.method('admin.label.List', no_payload=True,
  561. scope='global', read=True)
  562. @asyncio.coroutine
  563. def label_list(self):
  564. assert self.dest.name == 'dom0'
  565. assert not self.arg
  566. labels = self.fire_event_for_filter(self.app.labels.values())
  567. return ''.join('{}\n'.format(label.name) for label in labels)
  568. @qubes.api.method('admin.label.Get', no_payload=True,
  569. scope='global', read=True)
  570. @asyncio.coroutine
  571. def label_get(self):
  572. assert self.dest.name == 'dom0'
  573. try:
  574. label = self.app.get_label(self.arg)
  575. except KeyError:
  576. raise qubes.exc.QubesValueError
  577. self.fire_event_for_permission(label=label)
  578. return label.color
  579. @qubes.api.method('admin.label.Index', no_payload=True,
  580. scope='global', read=True)
  581. @asyncio.coroutine
  582. def label_index(self):
  583. assert self.dest.name == 'dom0'
  584. try:
  585. label = self.app.get_label(self.arg)
  586. except KeyError:
  587. raise qubes.exc.QubesValueError
  588. self.fire_event_for_permission(label=label)
  589. return str(label.index)
  590. @qubes.api.method('admin.label.Create',
  591. scope='global', write=True)
  592. @asyncio.coroutine
  593. def label_create(self, untrusted_payload):
  594. assert self.dest.name == 'dom0'
  595. # don't confuse label name with label index
  596. assert not self.arg.isdigit()
  597. allowed_chars = string.ascii_letters + string.digits + '-_.'
  598. assert all(c in allowed_chars for c in self.arg)
  599. try:
  600. self.app.get_label(self.arg)
  601. except KeyError:
  602. # ok, no such label yet
  603. pass
  604. else:
  605. raise qubes.exc.QubesValueError('label already exists')
  606. untrusted_payload = untrusted_payload.decode('ascii').strip()
  607. assert len(untrusted_payload) == 8
  608. assert untrusted_payload.startswith('0x')
  609. # besides prefix, only hex digits are allowed
  610. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  611. # SEE: #2732
  612. color = untrusted_payload
  613. self.fire_event_for_permission(color=color)
  614. # allocate new index, but make sure it's outside of default labels set
  615. new_index = max(
  616. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  617. label = qubes.Label(new_index, color, self.arg)
  618. self.app.labels[new_index] = label
  619. self.app.save()
  620. @qubes.api.method('admin.label.Remove', no_payload=True,
  621. scope='global', write=True)
  622. @asyncio.coroutine
  623. def label_remove(self):
  624. assert self.dest.name == 'dom0'
  625. try:
  626. label = self.app.get_label(self.arg)
  627. except KeyError:
  628. raise qubes.exc.QubesValueError
  629. # don't allow removing default labels
  630. assert label.index > qubes.config.max_default_label
  631. # FIXME: this should be in app.add_label()
  632. for vm in self.app.domains:
  633. if vm.label == label:
  634. raise qubes.exc.QubesException('label still in use')
  635. self.fire_event_for_permission(label=label)
  636. del self.app.labels[label.index]
  637. self.app.save()
  638. @qubes.api.method('admin.vm.Start', no_payload=True,
  639. scope='local', execute=True)
  640. @asyncio.coroutine
  641. def vm_start(self):
  642. assert not self.arg
  643. self.fire_event_for_permission()
  644. try:
  645. yield from self.dest.start()
  646. except libvirt.libvirtError as e:
  647. # change to QubesException, so will be reported to the user
  648. raise qubes.exc.QubesException('Start failed: ' + str(e) +
  649. ', see /var/log/libvirt/libxl/libxl-driver.log for details')
  650. @qubes.api.method('admin.vm.Shutdown', no_payload=True,
  651. scope='local', execute=True)
  652. @asyncio.coroutine
  653. def vm_shutdown(self):
  654. assert not self.arg
  655. self.fire_event_for_permission()
  656. yield from self.dest.shutdown()
  657. @qubes.api.method('admin.vm.Pause', no_payload=True,
  658. scope='local', execute=True)
  659. @asyncio.coroutine
  660. def vm_pause(self):
  661. assert not self.arg
  662. self.fire_event_for_permission()
  663. yield from self.dest.pause()
  664. @qubes.api.method('admin.vm.Unpause', no_payload=True,
  665. scope='local', execute=True)
  666. @asyncio.coroutine
  667. def vm_unpause(self):
  668. assert not self.arg
  669. self.fire_event_for_permission()
  670. yield from self.dest.unpause()
  671. @qubes.api.method('admin.vm.Kill', no_payload=True,
  672. scope='local', execute=True)
  673. @asyncio.coroutine
  674. def vm_kill(self):
  675. assert not self.arg
  676. self.fire_event_for_permission()
  677. yield from self.dest.kill()
  678. @qubes.api.method('admin.Events', no_payload=True,
  679. scope='global', read=True)
  680. @asyncio.coroutine
  681. def events(self):
  682. assert not self.arg
  683. # run until client connection is terminated
  684. self.cancellable = True
  685. wait_for_cancel = asyncio.get_event_loop().create_future()
  686. # cache event filters, to not call an event each time an event arrives
  687. event_filters = self.fire_event_for_permission()
  688. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  689. if self.dest.name == 'dom0':
  690. self.app.add_handler('*', dispatcher.app_handler)
  691. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  692. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  693. for vm in self.app.domains:
  694. vm.add_handler('*', dispatcher.vm_handler)
  695. else:
  696. self.dest.add_handler('*', dispatcher.vm_handler)
  697. # send artificial event as a confirmation that connection is established
  698. self.send_event(self.app, 'connection-established')
  699. try:
  700. yield from wait_for_cancel
  701. except asyncio.CancelledError:
  702. # the above waiting was already interrupted, this is all we need
  703. pass
  704. if self.dest.name == 'dom0':
  705. self.app.remove_handler('*', dispatcher.app_handler)
  706. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  707. self.app.remove_handler('domain-delete',
  708. dispatcher.on_domain_delete)
  709. for vm in self.app.domains:
  710. vm.remove_handler('*', dispatcher.vm_handler)
  711. else:
  712. self.dest.remove_handler('*', dispatcher.vm_handler)
  713. @qubes.api.method('admin.vm.feature.List', no_payload=True,
  714. scope='local', read=True)
  715. @asyncio.coroutine
  716. def vm_feature_list(self):
  717. assert not self.arg
  718. features = self.fire_event_for_filter(self.dest.features.keys())
  719. return ''.join('{}\n'.format(feature) for feature in features)
  720. @qubes.api.method('admin.vm.feature.Get', no_payload=True,
  721. scope='local', read=True)
  722. @asyncio.coroutine
  723. def vm_feature_get(self):
  724. # validation of self.arg done by qrexec-policy is enough
  725. self.fire_event_for_permission()
  726. try:
  727. value = self.dest.features[self.arg]
  728. except KeyError:
  729. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  730. return value
  731. @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True,
  732. scope='local', read=True)
  733. @asyncio.coroutine
  734. def vm_feature_checkwithtemplate(self):
  735. # validation of self.arg done by qrexec-policy is enough
  736. self.fire_event_for_permission()
  737. try:
  738. value = self.dest.features.check_with_template(self.arg)
  739. except KeyError:
  740. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  741. return value
  742. @qubes.api.method('admin.vm.feature.CheckWithNetvm', no_payload=True,
  743. scope='local', read=True)
  744. @asyncio.coroutine
  745. def vm_feature_checkwithnetvm(self):
  746. # validation of self.arg done by qrexec-policy is enough
  747. self.fire_event_for_permission()
  748. try:
  749. value = self.dest.features.check_with_netvm(self.arg)
  750. except KeyError:
  751. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  752. return value
  753. @qubes.api.method('admin.vm.feature.Remove', no_payload=True,
  754. scope='local', write=True)
  755. @asyncio.coroutine
  756. def vm_feature_remove(self):
  757. # validation of self.arg done by qrexec-policy is enough
  758. self.fire_event_for_permission()
  759. try:
  760. del self.dest.features[self.arg]
  761. except KeyError:
  762. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  763. self.app.save()
  764. @qubes.api.method('admin.vm.feature.Set',
  765. scope='local', write=True)
  766. @asyncio.coroutine
  767. def vm_feature_set(self, untrusted_payload):
  768. # validation of self.arg done by qrexec-policy is enough
  769. value = untrusted_payload.decode('ascii', errors='strict')
  770. del untrusted_payload
  771. self.fire_event_for_permission(value=value)
  772. self.dest.features[self.arg] = value
  773. self.app.save()
  774. @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
  775. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  776. scope='global', write=True)
  777. @asyncio.coroutine
  778. def vm_create(self, endpoint, untrusted_payload=None):
  779. return self._vm_create(endpoint, allow_pool=False,
  780. untrusted_payload=untrusted_payload)
  781. @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  782. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  783. scope='global', write=True)
  784. @asyncio.coroutine
  785. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  786. return self._vm_create(endpoint, allow_pool=True,
  787. untrusted_payload=untrusted_payload)
  788. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  789. assert self.dest.name == 'dom0'
  790. kwargs = {}
  791. pool = None
  792. pools = {}
  793. # this will raise exception if none is found
  794. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  795. vm_type)
  796. # if argument is given, it needs to be a valid template, and only
  797. # when given VM class do need a template
  798. if hasattr(vm_class, 'template'):
  799. if self.arg:
  800. assert self.arg in self.app.domains
  801. kwargs['template'] = self.app.domains[self.arg]
  802. else:
  803. assert not self.arg
  804. for untrusted_param in untrusted_payload.decode('ascii',
  805. errors='strict').split(' '):
  806. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  807. if untrusted_key in kwargs:
  808. raise qubes.api.ProtocolError('duplicated parameters')
  809. if untrusted_key == 'name':
  810. qubes.vm.validate_name(None, None, untrusted_value)
  811. kwargs['name'] = untrusted_value
  812. elif untrusted_key == 'label':
  813. # don't confuse label name with label index
  814. assert not untrusted_value.isdigit()
  815. allowed_chars = string.ascii_letters + string.digits + '-_.'
  816. assert all(c in allowed_chars for c in untrusted_value)
  817. try:
  818. kwargs['label'] = self.app.get_label(untrusted_value)
  819. except KeyError:
  820. raise qubes.exc.QubesValueError
  821. elif untrusted_key == 'pool' and allow_pool:
  822. if pool is not None:
  823. raise qubes.api.ProtocolError('duplicated pool parameter')
  824. pool = self.app.get_pool(untrusted_value)
  825. elif untrusted_key.startswith('pool:') and allow_pool:
  826. untrusted_volume = untrusted_key.split(':', 1)[1]
  827. # kind of ugly, but actual list of volumes is available only
  828. # after creating a VM
  829. assert untrusted_volume in ['root', 'private', 'volatile',
  830. 'kernel']
  831. volume = untrusted_volume
  832. if volume in pools:
  833. raise qubes.api.ProtocolError(
  834. 'duplicated pool:{} parameter'.format(volume))
  835. pools[volume] = self.app.get_pool(untrusted_value)
  836. else:
  837. raise qubes.api.ProtocolError('Invalid param name')
  838. del untrusted_payload
  839. if 'name' not in kwargs or 'label' not in kwargs:
  840. raise qubes.api.ProtocolError('Missing name or label')
  841. if pool and pools:
  842. raise qubes.api.ProtocolError(
  843. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  844. if kwargs['name'] in self.app.domains:
  845. raise qubes.exc.QubesValueError(
  846. 'VM {} already exists'.format(kwargs['name']))
  847. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  848. vm = self.app.add_new_vm(vm_class, **kwargs)
  849. # TODO: move this to extension (in race-free fashion)
  850. vm.tags.add('created-by-' + str(self.src))
  851. try:
  852. yield from vm.create_on_disk(pool=pool, pools=pools)
  853. except:
  854. del self.app.domains[vm]
  855. raise
  856. self.app.save()
  857. @qubes.api.method('admin.vm.CreateDisposable', no_payload=True,
  858. scope='global', write=True)
  859. @asyncio.coroutine
  860. def create_disposable(self):
  861. assert not self.arg
  862. if self.dest.name == 'dom0':
  863. dispvm_template = self.src.default_dispvm
  864. else:
  865. dispvm_template = self.dest
  866. self.fire_event_for_permission(dispvm_template=dispvm_template)
  867. dispvm = yield from qubes.vm.dispvm.DispVM.from_appvm(dispvm_template)
  868. # TODO: move this to extension (in race-free fashion, better than here)
  869. dispvm.tags.add('disp-created-by-' + str(self.src))
  870. return dispvm.name
  871. @qubes.api.method('admin.vm.Remove', no_payload=True,
  872. scope='global', write=True)
  873. @asyncio.coroutine
  874. def vm_remove(self):
  875. assert not self.arg
  876. self.fire_event_for_permission()
  877. if not self.dest.is_halted():
  878. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  879. if self.dest.installed_by_rpm:
  880. raise qubes.exc.QubesVMInUseError(self.dest, \
  881. "VM installed by package manager: " + self.dest.name)
  882. del self.app.domains[self.dest]
  883. try:
  884. yield from self.dest.remove_from_disk()
  885. except: # pylint: disable=bare-except
  886. self.app.log.exception('Error wile removing VM \'%s\' files',
  887. self.dest.name)
  888. self.app.save()
  889. @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
  890. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  891. no_payload=True,
  892. scope='local', read=True)
  893. @asyncio.coroutine
  894. def vm_device_available(self, endpoint):
  895. devclass = endpoint
  896. devices = self.dest.devices[devclass].available()
  897. if self.arg:
  898. devices = [dev for dev in devices if dev.ident == self.arg]
  899. # no duplicated devices, but device may not exists, in which case
  900. # the list is empty
  901. assert len(devices) <= 1
  902. devices = self.fire_event_for_filter(devices, devclass=devclass)
  903. dev_info = {}
  904. for dev in devices:
  905. non_default_attrs = set(attr for attr in dir(dev) if
  906. not attr.startswith('_')).difference((
  907. 'backend_domain', 'ident', 'frontend_domain',
  908. 'description', 'options', 'regex'))
  909. properties_txt = ' '.join(
  910. '{}={!s}'.format(prop, value) for prop, value
  911. in itertools.chain(
  912. ((key, getattr(dev, key)) for key in non_default_attrs),
  913. # keep description as the last one, according to API
  914. # specification
  915. (('description', dev.description),)
  916. ))
  917. assert '\n' not in properties_txt
  918. dev_info[dev.ident] = properties_txt
  919. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  920. for ident in sorted(dev_info))
  921. @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
  922. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  923. no_payload=True,
  924. scope='local', read=True)
  925. @asyncio.coroutine
  926. def vm_device_list(self, endpoint):
  927. devclass = endpoint
  928. device_assignments = self.dest.devices[devclass].assignments()
  929. if self.arg:
  930. select_backend, select_ident = self.arg.split('+', 1)
  931. device_assignments = [dev for dev in device_assignments
  932. if (str(dev.backend_domain), dev.ident)
  933. == (select_backend, select_ident)]
  934. # no duplicated devices, but device may not exists, in which case
  935. # the list is empty
  936. assert len(device_assignments) <= 1
  937. device_assignments = self.fire_event_for_filter(device_assignments,
  938. devclass=devclass)
  939. dev_info = {}
  940. for dev in device_assignments:
  941. properties_txt = ' '.join(
  942. '{}={!s}'.format(opt, value) for opt, value
  943. in itertools.chain(
  944. dev.options.items(),
  945. (('persistent', 'yes' if dev.persistent else 'no'),)
  946. ))
  947. assert '\n' not in properties_txt
  948. ident = '{!s}+{!s}'.format(dev.backend_domain, dev.ident)
  949. dev_info[ident] = properties_txt
  950. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  951. for ident in sorted(dev_info))
  952. # Attach/Detach action can both modify persistent state (with
  953. # persistent=True) and volatile state of running VM (with persistent=False).
  954. # For this reason, write=True + execute=True
  955. @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
  956. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  957. scope='local', write=True, execute=True)
  958. @asyncio.coroutine
  959. def vm_device_attach(self, endpoint, untrusted_payload):
  960. devclass = endpoint
  961. options = {}
  962. persistent = False
  963. for untrusted_option in untrusted_payload.decode('ascii').split():
  964. try:
  965. untrusted_key, untrusted_value = untrusted_option.split('=', 1)
  966. except ValueError:
  967. raise qubes.api.ProtocolError('Invalid options format')
  968. if untrusted_key == 'persistent':
  969. persistent = qubes.property.bool(None, None, untrusted_value)
  970. else:
  971. allowed_chars_key = string.digits + string.ascii_letters + '-_.'
  972. allowed_chars_value = allowed_chars_key + ',+:'
  973. if any(x not in allowed_chars_key for x in untrusted_key):
  974. raise qubes.api.ProtocolError(
  975. 'Invalid chars in option name')
  976. if any(x not in allowed_chars_value for x in untrusted_value):
  977. raise qubes.api.ProtocolError(
  978. 'Invalid chars in option value')
  979. options[untrusted_key] = untrusted_value
  980. # qrexec already verified that no strange characters are in self.arg
  981. backend_domain, ident = self.arg.split('+', 1)
  982. # may raise KeyError, either on domain or ident
  983. dev = self.app.domains[backend_domain].devices[devclass][ident]
  984. self.fire_event_for_permission(device=dev,
  985. devclass=devclass, persistent=persistent,
  986. options=options)
  987. assignment = qubes.devices.DeviceAssignment(
  988. dev.backend_domain, dev.ident,
  989. options=options, persistent=persistent)
  990. yield from self.dest.devices[devclass].attach(assignment)
  991. self.app.save()
  992. # Attach/Detach action can both modify persistent state (with
  993. # persistent=True) and volatile state of running VM (with persistent=False).
  994. # For this reason, write=True + execute=True
  995. @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
  996. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  997. no_payload=True,
  998. scope='local', write=True, execute=True)
  999. @asyncio.coroutine
  1000. def vm_device_detach(self, endpoint):
  1001. devclass = endpoint
  1002. # qrexec already verified that no strange characters are in self.arg
  1003. backend_domain, ident = self.arg.split('+', 1)
  1004. # may raise KeyError; if device isn't found, it will be UnknownDevice
  1005. # instance - but allow it, otherwise it will be impossible to detach
  1006. # already removed device
  1007. dev = self.app.domains[backend_domain].devices[devclass][ident]
  1008. self.fire_event_for_permission(device=dev,
  1009. devclass=devclass)
  1010. assignment = qubes.devices.DeviceAssignment(
  1011. dev.backend_domain, dev.ident)
  1012. yield from self.dest.devices[devclass].detach(assignment)
  1013. self.app.save()
  1014. # Attach/Detach action can both modify persistent state (with
  1015. # persistent=True) and volatile state of running VM (with persistent=False).
  1016. # For this reason, write=True + execute=True
  1017. @qubes.api.method('admin.vm.device.{endpoint}.Set.persistent',
  1018. endpoints=(ep.name
  1019. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1020. scope='local', write=True, execute=True)
  1021. @asyncio.coroutine
  1022. def vm_device_set_persistent(self, endpoint, untrusted_payload):
  1023. devclass = endpoint
  1024. assert untrusted_payload in (b'True', b'False')
  1025. persistent = untrusted_payload == b'True'
  1026. del untrusted_payload
  1027. # qrexec already verified that no strange characters are in self.arg
  1028. backend_domain, ident = self.arg.split('+', 1)
  1029. # device must be already attached
  1030. matching_devices = [dev for dev
  1031. in self.dest.devices[devclass].attached()
  1032. if dev.backend_domain.name == backend_domain and dev.ident == ident]
  1033. assert len(matching_devices) == 1
  1034. dev = matching_devices[0]
  1035. self.fire_event_for_permission(device=dev,
  1036. persistent=persistent)
  1037. self.dest.devices[devclass].update_persistent(dev, persistent)
  1038. self.app.save()
  1039. @qubes.api.method('admin.vm.firewall.Get', no_payload=True,
  1040. scope='local', read=True)
  1041. @asyncio.coroutine
  1042. def vm_firewall_get(self):
  1043. assert not self.arg
  1044. self.fire_event_for_permission()
  1045. return ''.join('{}\n'.format(rule.api_rule)
  1046. for rule in self.dest.firewall.rules
  1047. if rule.api_rule is not None)
  1048. @qubes.api.method('admin.vm.firewall.Set',
  1049. scope='local', write=True)
  1050. @asyncio.coroutine
  1051. def vm_firewall_set(self, untrusted_payload):
  1052. assert not self.arg
  1053. rules = []
  1054. for untrusted_line in untrusted_payload.decode('ascii',
  1055. errors='strict').splitlines():
  1056. rule = qubes.firewall.Rule.from_api_string(
  1057. untrusted_rule=untrusted_line)
  1058. rules.append(rule)
  1059. self.fire_event_for_permission(rules=rules)
  1060. self.dest.firewall.rules = rules
  1061. self.dest.firewall.save()
  1062. @qubes.api.method('admin.vm.firewall.Reload', no_payload=True,
  1063. scope='local', execute=True)
  1064. @asyncio.coroutine
  1065. def vm_firewall_reload(self):
  1066. assert not self.arg
  1067. self.fire_event_for_permission()
  1068. self.dest.fire_event('firewall-changed')
  1069. @asyncio.coroutine
  1070. def _load_backup_profile(self, profile_name, skip_passphrase=False):
  1071. '''Load backup profile and return :py:class:`qubes.backup.Backup`
  1072. instance
  1073. :param profile_name: name of the profile
  1074. :param skip_passphrase: do not load passphrase - only backup summary
  1075. can be retrieved when this option is in use
  1076. '''
  1077. profile_path = os.path.join(
  1078. qubes.config.backup_profile_dir, profile_name + '.conf')
  1079. with open(profile_path) as profile_file:
  1080. profile_data = yaml.safe_load(profile_file)
  1081. try:
  1082. dest_vm = profile_data['destination_vm']
  1083. dest_path = profile_data['destination_path']
  1084. include_vms = profile_data['include']
  1085. if include_vms is not None:
  1086. # convert old keywords to new keywords
  1087. include_vms = [vm.replace('$', '@') for vm in include_vms]
  1088. exclude_vms = profile_data.get('exclude', [])
  1089. # convert old keywords to new keywords
  1090. exclude_vms = [vm.replace('$', '@') for vm in exclude_vms]
  1091. compression = profile_data.get('compression', True)
  1092. except KeyError as err:
  1093. raise qubes.exc.QubesException(
  1094. 'Invalid backup profile - missing {}'.format(err))
  1095. try:
  1096. dest_vm = self.app.domains[dest_vm]
  1097. except KeyError:
  1098. raise qubes.exc.QubesException(
  1099. 'Invalid destination_vm specified in backup profile')
  1100. if isinstance(dest_vm, qubes.vm.adminvm.AdminVM):
  1101. dest_vm = None
  1102. if skip_passphrase:
  1103. passphrase = None
  1104. elif 'passphrase_text' in profile_data:
  1105. passphrase = profile_data['passphrase_text']
  1106. elif 'passphrase_vm' in profile_data:
  1107. passphrase_vm_name = profile_data['passphrase_vm']
  1108. try:
  1109. passphrase_vm = self.app.domains[passphrase_vm_name]
  1110. except KeyError:
  1111. raise qubes.exc.QubesException(
  1112. 'Invalid backup profile - invalid passphrase_vm')
  1113. try:
  1114. passphrase, _ = yield from passphrase_vm.run_service_for_stdio(
  1115. 'qubes.BackupPassphrase+' + self.arg)
  1116. # make it foolproof against "echo passphrase" implementation
  1117. passphrase = passphrase.strip()
  1118. assert b'\n' not in passphrase
  1119. except subprocess.CalledProcessError:
  1120. raise qubes.exc.QubesException(
  1121. 'Failed to retrieve passphrase from \'{}\' VM'.format(
  1122. passphrase_vm_name))
  1123. else:
  1124. raise qubes.exc.QubesException(
  1125. 'Invalid backup profile - you need to '
  1126. 'specify passphrase_text or passphrase_vm')
  1127. # handle include
  1128. if include_vms is None:
  1129. vms_to_backup = None
  1130. else:
  1131. vms_to_backup = set(vm for vm in self.app.domains
  1132. if any(qubes.utils.match_vm_name_with_special(vm, name)
  1133. for name in include_vms))
  1134. # handle exclude
  1135. vms_to_exclude = set(vm.name for vm in self.app.domains
  1136. if any(qubes.utils.match_vm_name_with_special(vm, name)
  1137. for name in exclude_vms))
  1138. kwargs = {
  1139. 'target_vm': dest_vm,
  1140. 'target_dir': dest_path,
  1141. 'compressed': bool(compression),
  1142. 'passphrase': passphrase,
  1143. }
  1144. if isinstance(compression, str):
  1145. kwargs['compression_filter'] = compression
  1146. backup = qubes.backup.Backup(self.app, vms_to_backup, vms_to_exclude,
  1147. **kwargs)
  1148. return backup
  1149. def _backup_progress_callback(self, profile_name, progress):
  1150. self.app.fire_event('backup-progress', backup_profile=profile_name,
  1151. progress=progress)
  1152. @qubes.api.method('admin.backup.Execute', no_payload=True,
  1153. scope='global', read=True, execute=True)
  1154. @asyncio.coroutine
  1155. def backup_execute(self):
  1156. assert self.dest.name == 'dom0'
  1157. assert self.arg
  1158. assert '/' not in self.arg
  1159. self.fire_event_for_permission()
  1160. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1161. self.arg + '.conf')
  1162. if not os.path.exists(profile_path):
  1163. raise qubes.api.PermissionDenied(
  1164. 'Backup profile {} does not exist'.format(self.arg))
  1165. if not hasattr(self.app, 'api_admin_running_backups'):
  1166. self.app.api_admin_running_backups = {}
  1167. backup = yield from self._load_backup_profile(self.arg)
  1168. backup.progress_callback = functools.partial(
  1169. self._backup_progress_callback, self.arg)
  1170. # forbid running the same backup operation twice at the time
  1171. assert self.arg not in self.app.api_admin_running_backups
  1172. backup_task = asyncio.ensure_future(backup.backup_do())
  1173. self.app.api_admin_running_backups[self.arg] = backup_task
  1174. try:
  1175. yield from backup_task
  1176. except asyncio.CancelledError:
  1177. raise qubes.exc.QubesException('Backup cancelled')
  1178. finally:
  1179. del self.app.api_admin_running_backups[self.arg]
  1180. @qubes.api.method('admin.backup.Cancel', no_payload=True,
  1181. scope='global', execute=True)
  1182. @asyncio.coroutine
  1183. def backup_cancel(self):
  1184. assert self.dest.name == 'dom0'
  1185. assert self.arg
  1186. assert '/' not in self.arg
  1187. self.fire_event_for_permission()
  1188. if not hasattr(self.app, 'api_admin_running_backups'):
  1189. self.app.api_admin_running_backups = {}
  1190. if self.arg not in self.app.api_admin_running_backups:
  1191. raise qubes.exc.QubesException('Backup operation not running')
  1192. self.app.api_admin_running_backups[self.arg].cancel()
  1193. @qubes.api.method('admin.backup.Info', no_payload=True,
  1194. scope='local', read=True)
  1195. @asyncio.coroutine
  1196. def backup_info(self):
  1197. assert self.dest.name == 'dom0'
  1198. assert self.arg
  1199. assert '/' not in self.arg
  1200. self.fire_event_for_permission()
  1201. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1202. self.arg + '.conf')
  1203. if not os.path.exists(profile_path):
  1204. raise qubes.api.PermissionDenied(
  1205. 'Backup profile {} does not exist'.format(self.arg))
  1206. backup = yield from self._load_backup_profile(self.arg,
  1207. skip_passphrase=True)
  1208. return backup.get_backup_summary()
  1209. def _send_stats_single(self, info_time, info, only_vm, filters,
  1210. id_to_name_map):
  1211. '''A single iteration of sending VM stats
  1212. :param info_time: time of previous iteration
  1213. :param info: information retrieved in previous iteration
  1214. :param only_vm: send information only about this VM
  1215. :param filters: filters to apply on stats before sending
  1216. :param id_to_name_map: ID->VM name map, may be modified
  1217. :return: tuple(info_time, info) - new information (to be passed to
  1218. the next iteration)
  1219. '''
  1220. (info_time, info) = self.app.host.get_vm_stats(info_time, info,
  1221. only_vm=only_vm)
  1222. for vm_id, vm_info in info.items():
  1223. if vm_id not in id_to_name_map:
  1224. try:
  1225. name = \
  1226. self.app.vmm.libvirt_conn.lookupByID(vm_id).name()
  1227. except libvirt.libvirtError as err:
  1228. if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
  1229. # stubdomain or so
  1230. name = None
  1231. else:
  1232. raise
  1233. id_to_name_map[vm_id] = name
  1234. else:
  1235. name = id_to_name_map[vm_id]
  1236. # skip VMs with unknown name
  1237. if name is None:
  1238. continue
  1239. if not list(qubes.api.apply_filters([name], filters)):
  1240. continue
  1241. self.send_event(name, 'vm-stats',
  1242. memory_kb=int(vm_info['memory_kb']),
  1243. cpu_time=int(vm_info['cpu_time'] / 1000000),
  1244. cpu_usage=int(vm_info['cpu_usage']))
  1245. return info_time, info
  1246. @qubes.api.method('admin.vm.Stats', no_payload=True,
  1247. scope='global', read=True)
  1248. @asyncio.coroutine
  1249. def vm_stats(self):
  1250. assert not self.arg
  1251. # run until client connection is terminated
  1252. self.cancellable = True
  1253. # cache event filters, to not call an event each time an event arrives
  1254. stats_filters = self.fire_event_for_permission()
  1255. only_vm = None
  1256. if self.dest.name != 'dom0':
  1257. only_vm = self.dest
  1258. self.send_event(self.app, 'connection-established')
  1259. info_time = None
  1260. info = None
  1261. id_to_name_map = {0: 'dom0'}
  1262. try:
  1263. while True:
  1264. info_time, info = self._send_stats_single(info_time, info,
  1265. only_vm, stats_filters, id_to_name_map)
  1266. yield from asyncio.sleep(self.app.stats_interval)
  1267. except asyncio.CancelledError:
  1268. # valid method to terminate this loop
  1269. pass