admin.py 45 KB

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