admin.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  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 string
  25. import itertools
  26. import pkg_resources
  27. import libvirt
  28. import qubes.api
  29. import qubes.devices
  30. import qubes.firewall
  31. import qubes.storage
  32. import qubes.utils
  33. import qubes.vm
  34. import qubes.vm.qubesvm
  35. class QubesMgmtEventsDispatcher(object):
  36. def __init__(self, filters, send_event):
  37. self.filters = filters
  38. self.send_event = send_event
  39. def vm_handler(self, subject, event, **kwargs):
  40. if event.startswith('mgmt-permission:'):
  41. return
  42. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  43. self.filters)):
  44. return
  45. self.send_event(subject, event, **kwargs)
  46. def app_handler(self, subject, event, **kwargs):
  47. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  48. self.filters)):
  49. return
  50. self.send_event(subject, event, **kwargs)
  51. def on_domain_add(self, subject, event, vm):
  52. # pylint: disable=unused-argument
  53. vm.add_handler('*', self.vm_handler)
  54. def on_domain_delete(self, subject, event, vm):
  55. # pylint: disable=unused-argument
  56. vm.remove_handler('*', self.vm_handler)
  57. class QubesAdminAPI(qubes.api.AbstractQubesAPI):
  58. '''Implementation of Qubes Management API calls
  59. This class contains all the methods available in the main API.
  60. .. seealso::
  61. https://www.qubes-os.org/doc/mgmt1/
  62. '''
  63. SOCKNAME = '/var/run/qubesd.sock'
  64. @qubes.api.method('admin.vmclass.List', no_payload=True)
  65. @asyncio.coroutine
  66. def vmclass_list(self):
  67. '''List all VM classes'''
  68. assert not self.arg
  69. assert self.dest.name == 'dom0'
  70. entrypoints = self.fire_event_for_filter(
  71. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  72. return ''.join('{}\n'.format(ep.name)
  73. for ep in entrypoints)
  74. @qubes.api.method('admin.vm.List', no_payload=True)
  75. @asyncio.coroutine
  76. def vm_list(self):
  77. '''List all the domains'''
  78. assert not self.arg
  79. if self.dest.name == 'dom0':
  80. domains = self.fire_event_for_filter(self.app.domains)
  81. else:
  82. domains = self.fire_event_for_filter([self.dest])
  83. return ''.join('{} class={} state={}\n'.format(
  84. vm.name,
  85. vm.__class__.__name__,
  86. vm.get_power_state())
  87. for vm in sorted(domains))
  88. @qubes.api.method('admin.vm.property.List', no_payload=True)
  89. @asyncio.coroutine
  90. def vm_property_list(self):
  91. '''List all properties on a qube'''
  92. return self._property_list(self.dest)
  93. @qubes.api.method('admin.property.List', no_payload=True)
  94. @asyncio.coroutine
  95. def property_list(self):
  96. '''List all global properties'''
  97. assert self.dest.name == 'dom0'
  98. return self._property_list(self.app)
  99. def _property_list(self, dest):
  100. assert not self.arg
  101. properties = self.fire_event_for_filter(dest.property_list())
  102. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  103. @qubes.api.method('admin.vm.property.Get', no_payload=True)
  104. @asyncio.coroutine
  105. def vm_property_get(self):
  106. '''Get a value of one property'''
  107. return self._property_get(self.dest)
  108. @qubes.api.method('admin.property.Get', no_payload=True)
  109. @asyncio.coroutine
  110. def property_get(self):
  111. '''Get a value of one global property'''
  112. assert self.dest.name == 'dom0'
  113. return self._property_get(self.app)
  114. def _property_get(self, dest):
  115. if self.arg not in dest.property_list():
  116. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  117. self.fire_event_for_permission()
  118. property_def = dest.property_get_def(self.arg)
  119. # explicit list to be sure that it matches protocol spec
  120. if isinstance(property_def, qubes.vm.VMProperty):
  121. property_type = 'vm'
  122. elif property_def.type is int:
  123. property_type = 'int'
  124. elif property_def.type is bool:
  125. property_type = 'bool'
  126. elif self.arg == 'label':
  127. property_type = 'label'
  128. else:
  129. property_type = 'str'
  130. try:
  131. value = getattr(dest, self.arg)
  132. except AttributeError:
  133. return 'default=True type={} '.format(property_type)
  134. else:
  135. return 'default={} type={} {}'.format(
  136. str(dest.property_is_default(self.arg)),
  137. property_type,
  138. str(value) if value is not None else '')
  139. @qubes.api.method('admin.vm.property.Set')
  140. @asyncio.coroutine
  141. def vm_property_set(self, untrusted_payload):
  142. '''Set property value'''
  143. return self._property_set(self.dest,
  144. untrusted_payload=untrusted_payload)
  145. @qubes.api.method('admin.property.Set')
  146. @asyncio.coroutine
  147. def property_set(self, untrusted_payload):
  148. '''Set property value'''
  149. assert self.dest.name == 'dom0'
  150. return self._property_set(self.app,
  151. untrusted_payload=untrusted_payload)
  152. def _property_set(self, dest, untrusted_payload):
  153. if self.arg not in dest.property_list():
  154. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  155. property_def = dest.property_get_def(self.arg)
  156. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  157. self.fire_event_for_permission(newvalue=newvalue)
  158. setattr(dest, self.arg, newvalue)
  159. self.app.save()
  160. @qubes.api.method('admin.vm.property.Help', no_payload=True)
  161. @asyncio.coroutine
  162. def vm_property_help(self):
  163. '''Get help for one property'''
  164. return self._property_help(self.dest)
  165. @qubes.api.method('admin.property.Help', no_payload=True)
  166. @asyncio.coroutine
  167. def property_help(self):
  168. '''Get help for one property'''
  169. assert self.dest.name == 'dom0'
  170. return self._property_help(self.app)
  171. def _property_help(self, dest):
  172. if self.arg not in dest.property_list():
  173. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  174. self.fire_event_for_permission()
  175. try:
  176. doc = dest.property_get_def(self.arg).__doc__
  177. except AttributeError:
  178. return ''
  179. return qubes.utils.format_doc(doc)
  180. @qubes.api.method('admin.vm.property.Reset', no_payload=True)
  181. @asyncio.coroutine
  182. def vm_property_reset(self):
  183. '''Reset a property to a default value'''
  184. return self._property_reset(self.dest)
  185. @qubes.api.method('admin.property.Reset', no_payload=True)
  186. @asyncio.coroutine
  187. def property_reset(self):
  188. '''Reset a property to a default value'''
  189. assert self.dest.name == 'dom0'
  190. return self._property_reset(self.app)
  191. def _property_reset(self, dest):
  192. if self.arg not in dest.property_list():
  193. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  194. self.fire_event_for_permission()
  195. delattr(dest, self.arg)
  196. self.app.save()
  197. @qubes.api.method('admin.vm.volume.List', no_payload=True)
  198. @asyncio.coroutine
  199. def vm_volume_list(self):
  200. assert not self.arg
  201. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  202. return ''.join('{}\n'.format(name) for name in volume_names)
  203. @qubes.api.method('admin.vm.volume.Info', no_payload=True)
  204. @asyncio.coroutine
  205. def vm_volume_info(self):
  206. assert self.arg in self.dest.volumes.keys()
  207. self.fire_event_for_permission()
  208. volume = self.dest.volumes[self.arg]
  209. # properties defined in API
  210. volume_properties = [
  211. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  212. 'save_on_stop', 'snap_on_start']
  213. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  214. volume_properties)
  215. @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True)
  216. @asyncio.coroutine
  217. def vm_volume_listsnapshots(self):
  218. assert self.arg in self.dest.volumes.keys()
  219. volume = self.dest.volumes[self.arg]
  220. revisions = [revision for revision in volume.revisions]
  221. revisions = self.fire_event_for_filter(revisions)
  222. return ''.join('{}\n'.format(revision) for revision in revisions)
  223. @qubes.api.method('admin.vm.volume.Revert')
  224. @asyncio.coroutine
  225. def vm_volume_revert(self, untrusted_payload):
  226. assert self.arg in self.dest.volumes.keys()
  227. untrusted_revision = untrusted_payload.decode('ascii').strip()
  228. del untrusted_payload
  229. volume = self.dest.volumes[self.arg]
  230. snapshots = volume.revisions
  231. assert untrusted_revision in snapshots
  232. revision = untrusted_revision
  233. self.fire_event_for_permission(revision=revision)
  234. self.dest.storage.get_pool(volume).revert(revision)
  235. self.app.save()
  236. @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True)
  237. @asyncio.coroutine
  238. def vm_volume_clone_from(self):
  239. assert self.arg in self.dest.volumes.keys()
  240. volume = self.dest.volumes[self.arg]
  241. self.fire_event_for_permission(volume=volume)
  242. token = qubes.utils.random_string(32)
  243. # save token on self.app, as self is not persistent
  244. if not hasattr(self.app, 'api_admin_pending_clone'):
  245. self.app.api_admin_pending_clone = {}
  246. # don't handle collisions any better - if someone is so much out of
  247. # luck, can try again anyway
  248. assert token not in self.app.api_admin_pending_clone
  249. self.app.api_admin_pending_clone[token] = volume
  250. return token
  251. @qubes.api.method('admin.vm.volume.CloneTo')
  252. @asyncio.coroutine
  253. def vm_volume_clone_to(self, untrusted_payload):
  254. assert self.arg in self.dest.volumes.keys()
  255. untrusted_token = untrusted_payload.decode('ascii').strip()
  256. del untrusted_payload
  257. assert untrusted_token in getattr(self.app,
  258. 'api_admin_pending_clone', {})
  259. token = untrusted_token
  260. del untrusted_token
  261. src_volume = self.app.api_admin_pending_clone[token]
  262. del self.app.api_admin_pending_clone[token]
  263. # make sure the volume still exists, but invalidate token anyway
  264. assert str(src_volume.pool) in self.app.pools
  265. assert src_volume in self.app.pools[str(src_volume.pool)].volumes
  266. dst_volume = self.dest.volumes[self.arg]
  267. self.fire_event_for_permission(src_volume=src_volume,
  268. dst_volume=dst_volume)
  269. op_retval = dst_volume.import_volume(src_volume)
  270. # clone/import functions may be either synchronous or asynchronous
  271. # in the later case, we need to wait for them to finish
  272. if asyncio.iscoroutine(op_retval):
  273. op_retval = yield from op_retval
  274. self.dest.volumes[self.arg] = op_retval
  275. self.app.save()
  276. @qubes.api.method('admin.vm.volume.Resize')
  277. @asyncio.coroutine
  278. def vm_volume_resize(self, untrusted_payload):
  279. assert self.arg in self.dest.volumes.keys()
  280. untrusted_size = untrusted_payload.decode('ascii').strip()
  281. del untrusted_payload
  282. assert untrusted_size.isdigit() # only digits, forbid '-' too
  283. assert len(untrusted_size) <= 20 # limit to about 2^64
  284. size = int(untrusted_size)
  285. self.fire_event_for_permission(size=size)
  286. self.dest.storage.resize(self.arg, size)
  287. self.app.save()
  288. @qubes.api.method('admin.vm.volume.Import', no_payload=True)
  289. @asyncio.coroutine
  290. def vm_volume_import(self):
  291. '''Import volume data.
  292. Note that this function only returns a path to where data should be
  293. written, actual importing is done by a script in /etc/qubes-rpc
  294. When the script finish importing, it will trigger
  295. internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
  296. payload) and response from that call will be actually send to the
  297. caller.
  298. '''
  299. assert self.arg in self.dest.volumes.keys()
  300. self.fire_event_for_permission()
  301. if not self.dest.is_halted():
  302. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  303. path = self.dest.storage.import_data(self.arg)
  304. assert ' ' not in path
  305. size = self.dest.volumes[self.arg].size
  306. # when we know the action is allowed, inform extensions that it will
  307. # be performed
  308. self.dest.fire_event('domain-volume-import-begin', volume=self.arg)
  309. return '{} {}'.format(size, path)
  310. @qubes.api.method('admin.vm.tag.List', no_payload=True)
  311. @asyncio.coroutine
  312. def vm_tag_list(self):
  313. assert not self.arg
  314. tags = self.dest.tags
  315. tags = self.fire_event_for_filter(tags)
  316. return ''.join('{}\n'.format(tag) for tag in sorted(tags))
  317. @qubes.api.method('admin.vm.tag.Get', no_payload=True)
  318. @asyncio.coroutine
  319. def vm_tag_get(self):
  320. qubes.vm.Tags.validate_tag(self.arg)
  321. self.fire_event_for_permission()
  322. return '1' if self.arg in self.dest.tags else '0'
  323. @qubes.api.method('admin.vm.tag.Set', no_payload=True)
  324. @asyncio.coroutine
  325. def vm_tag_set(self):
  326. qubes.vm.Tags.validate_tag(self.arg)
  327. self.fire_event_for_permission()
  328. self.dest.tags.add(self.arg)
  329. self.app.save()
  330. @qubes.api.method('admin.vm.tag.Remove', no_payload=True)
  331. @asyncio.coroutine
  332. def vm_tag_remove(self):
  333. qubes.vm.Tags.validate_tag(self.arg)
  334. self.fire_event_for_permission()
  335. try:
  336. self.dest.tags.remove(self.arg)
  337. except KeyError:
  338. raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
  339. self.app.save()
  340. @qubes.api.method('admin.pool.List', no_payload=True)
  341. @asyncio.coroutine
  342. def pool_list(self):
  343. assert not self.arg
  344. assert self.dest.name == 'dom0'
  345. pools = self.fire_event_for_filter(self.app.pools)
  346. return ''.join('{}\n'.format(pool) for pool in pools)
  347. @qubes.api.method('admin.pool.ListDrivers', no_payload=True)
  348. @asyncio.coroutine
  349. def pool_listdrivers(self):
  350. assert self.dest.name == 'dom0'
  351. assert not self.arg
  352. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  353. return ''.join('{} {}\n'.format(
  354. driver,
  355. ' '.join(qubes.storage.driver_parameters(driver)))
  356. for driver in drivers)
  357. @qubes.api.method('admin.pool.Info', no_payload=True)
  358. @asyncio.coroutine
  359. def pool_info(self):
  360. assert self.dest.name == 'dom0'
  361. assert self.arg in self.app.pools.keys()
  362. pool = self.app.pools[self.arg]
  363. self.fire_event_for_permission(pool=pool)
  364. return ''.join('{}={}\n'.format(prop, val)
  365. for prop, val in sorted(pool.config.items()))
  366. @qubes.api.method('admin.pool.Add')
  367. @asyncio.coroutine
  368. def pool_add(self, untrusted_payload):
  369. assert self.dest.name == 'dom0'
  370. drivers = qubes.storage.pool_drivers()
  371. assert self.arg in drivers
  372. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  373. del untrusted_payload
  374. assert all(('=' in line) for line in untrusted_pool_config)
  375. # pairs of (option, value)
  376. untrusted_pool_config = [line.split('=', 1)
  377. for line in untrusted_pool_config]
  378. # reject duplicated options
  379. assert len(set(x[0] for x in untrusted_pool_config)) == \
  380. len([x[0] for x in untrusted_pool_config])
  381. # and convert to dict
  382. untrusted_pool_config = dict(untrusted_pool_config)
  383. assert 'name' in untrusted_pool_config
  384. untrusted_pool_name = untrusted_pool_config.pop('name')
  385. allowed_chars = string.ascii_letters + string.digits + '-_.'
  386. assert all(c in allowed_chars for c in untrusted_pool_name)
  387. pool_name = untrusted_pool_name
  388. assert pool_name not in self.app.pools
  389. driver_parameters = qubes.storage.driver_parameters(self.arg)
  390. assert all(key in driver_parameters for key in untrusted_pool_config)
  391. pool_config = untrusted_pool_config
  392. self.fire_event_for_permission(name=pool_name,
  393. pool_config=pool_config)
  394. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  395. self.app.save()
  396. @qubes.api.method('admin.pool.Remove', no_payload=True)
  397. @asyncio.coroutine
  398. def pool_remove(self):
  399. assert self.dest.name == 'dom0'
  400. assert self.arg in self.app.pools.keys()
  401. self.fire_event_for_permission()
  402. self.app.remove_pool(self.arg)
  403. self.app.save()
  404. @qubes.api.method('admin.label.List', no_payload=True)
  405. @asyncio.coroutine
  406. def label_list(self):
  407. assert self.dest.name == 'dom0'
  408. assert not self.arg
  409. labels = self.fire_event_for_filter(self.app.labels.values())
  410. return ''.join('{}\n'.format(label.name) for label in labels)
  411. @qubes.api.method('admin.label.Get', no_payload=True)
  412. @asyncio.coroutine
  413. def label_get(self):
  414. assert self.dest.name == 'dom0'
  415. try:
  416. label = self.app.get_label(self.arg)
  417. except KeyError:
  418. raise qubes.exc.QubesValueError
  419. self.fire_event_for_permission(label=label)
  420. return label.color
  421. @qubes.api.method('admin.label.Index', no_payload=True)
  422. @asyncio.coroutine
  423. def label_index(self):
  424. assert self.dest.name == 'dom0'
  425. try:
  426. label = self.app.get_label(self.arg)
  427. except KeyError:
  428. raise qubes.exc.QubesValueError
  429. self.fire_event_for_permission(label=label)
  430. return str(label.index)
  431. @qubes.api.method('admin.label.Create')
  432. @asyncio.coroutine
  433. def label_create(self, untrusted_payload):
  434. assert self.dest.name == 'dom0'
  435. # don't confuse label name with label index
  436. assert not self.arg.isdigit()
  437. allowed_chars = string.ascii_letters + string.digits + '-_.'
  438. assert all(c in allowed_chars for c in self.arg)
  439. try:
  440. self.app.get_label(self.arg)
  441. except KeyError:
  442. # ok, no such label yet
  443. pass
  444. else:
  445. raise qubes.exc.QubesValueError('label already exists')
  446. untrusted_payload = untrusted_payload.decode('ascii').strip()
  447. assert len(untrusted_payload) == 8
  448. assert untrusted_payload.startswith('0x')
  449. # besides prefix, only hex digits are allowed
  450. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  451. # SEE: #2732
  452. color = untrusted_payload
  453. self.fire_event_for_permission(color=color)
  454. # allocate new index, but make sure it's outside of default labels set
  455. new_index = max(
  456. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  457. label = qubes.Label(new_index, color, self.arg)
  458. self.app.labels[new_index] = label
  459. self.app.save()
  460. @qubes.api.method('admin.label.Remove', no_payload=True)
  461. @asyncio.coroutine
  462. def label_remove(self):
  463. assert self.dest.name == 'dom0'
  464. try:
  465. label = self.app.get_label(self.arg)
  466. except KeyError:
  467. raise qubes.exc.QubesValueError
  468. # don't allow removing default labels
  469. assert label.index > qubes.config.max_default_label
  470. # FIXME: this should be in app.add_label()
  471. for vm in self.app.domains:
  472. if vm.label == label:
  473. raise qubes.exc.QubesException('label still in use')
  474. self.fire_event_for_permission(label=label)
  475. del self.app.labels[label.index]
  476. self.app.save()
  477. @qubes.api.method('admin.vm.Start', no_payload=True)
  478. @asyncio.coroutine
  479. def vm_start(self):
  480. assert not self.arg
  481. self.fire_event_for_permission()
  482. try:
  483. yield from self.dest.start()
  484. except libvirt.libvirtError as e:
  485. # change to QubesException, so will be reported to the user
  486. raise qubes.exc.QubesException('Start failed: ' + str(e))
  487. @qubes.api.method('admin.vm.Shutdown', no_payload=True)
  488. @asyncio.coroutine
  489. def vm_shutdown(self):
  490. assert not self.arg
  491. self.fire_event_for_permission()
  492. yield from self.dest.shutdown()
  493. @qubes.api.method('admin.vm.Pause', no_payload=True)
  494. @asyncio.coroutine
  495. def vm_pause(self):
  496. assert not self.arg
  497. self.fire_event_for_permission()
  498. yield from self.dest.pause()
  499. @qubes.api.method('admin.vm.Unpause', no_payload=True)
  500. @asyncio.coroutine
  501. def vm_unpause(self):
  502. assert not self.arg
  503. self.fire_event_for_permission()
  504. yield from self.dest.unpause()
  505. @qubes.api.method('admin.vm.Kill', no_payload=True)
  506. @asyncio.coroutine
  507. def vm_kill(self):
  508. assert not self.arg
  509. self.fire_event_for_permission()
  510. yield from self.dest.kill()
  511. @qubes.api.method('admin.Events', no_payload=True)
  512. @asyncio.coroutine
  513. def events(self):
  514. assert not self.arg
  515. # run until client connection is terminated
  516. self.cancellable = True
  517. wait_for_cancel = asyncio.get_event_loop().create_future()
  518. # cache event filters, to not call an event each time an event arrives
  519. event_filters = self.fire_event_for_permission()
  520. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  521. if self.dest.name == 'dom0':
  522. self.app.add_handler('*', dispatcher.app_handler)
  523. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  524. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  525. for vm in self.app.domains:
  526. vm.add_handler('*', dispatcher.vm_handler)
  527. else:
  528. self.dest.add_handler('*', dispatcher.vm_handler)
  529. # send artificial event as a confirmation that connection is established
  530. self.send_event(self.app, 'connection-established')
  531. try:
  532. yield from wait_for_cancel
  533. except asyncio.CancelledError:
  534. # the above waiting was already interrupted, this is all we need
  535. pass
  536. if self.dest.name == 'dom0':
  537. self.app.remove_handler('*', dispatcher.app_handler)
  538. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  539. self.app.remove_handler('domain-delete',
  540. dispatcher.on_domain_delete)
  541. for vm in self.app.domains:
  542. vm.remove_handler('*', dispatcher.vm_handler)
  543. else:
  544. self.dest.remove_handler('*', dispatcher.vm_handler)
  545. @qubes.api.method('admin.vm.feature.List', no_payload=True)
  546. @asyncio.coroutine
  547. def vm_feature_list(self):
  548. assert not self.arg
  549. features = self.fire_event_for_filter(self.dest.features.keys())
  550. return ''.join('{}\n'.format(feature) for feature in features)
  551. @qubes.api.method('admin.vm.feature.Get', no_payload=True)
  552. @asyncio.coroutine
  553. def vm_feature_get(self):
  554. # validation of self.arg done by qrexec-policy is enough
  555. self.fire_event_for_permission()
  556. try:
  557. value = self.dest.features[self.arg]
  558. except KeyError:
  559. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  560. return value
  561. @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True)
  562. @asyncio.coroutine
  563. def vm_feature_checkwithtemplate(self):
  564. # validation of self.arg done by qrexec-policy is enough
  565. self.fire_event_for_permission()
  566. try:
  567. value = self.dest.features.check_with_template(self.arg)
  568. except KeyError:
  569. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  570. return value
  571. @qubes.api.method('admin.vm.feature.Remove', no_payload=True)
  572. @asyncio.coroutine
  573. def vm_feature_remove(self):
  574. # validation of self.arg done by qrexec-policy is enough
  575. self.fire_event_for_permission()
  576. try:
  577. del self.dest.features[self.arg]
  578. except KeyError:
  579. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  580. self.app.save()
  581. @qubes.api.method('admin.vm.feature.Set')
  582. @asyncio.coroutine
  583. def vm_feature_set(self, untrusted_payload):
  584. # validation of self.arg done by qrexec-policy is enough
  585. value = untrusted_payload.decode('ascii', errors='strict')
  586. del untrusted_payload
  587. self.fire_event_for_permission(value=value)
  588. self.dest.features[self.arg] = value
  589. self.app.save()
  590. @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
  591. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  592. @asyncio.coroutine
  593. def vm_create(self, endpoint, untrusted_payload=None):
  594. return self._vm_create(endpoint, allow_pool=False,
  595. untrusted_payload=untrusted_payload)
  596. @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  597. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  598. @asyncio.coroutine
  599. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  600. return self._vm_create(endpoint, allow_pool=True,
  601. untrusted_payload=untrusted_payload)
  602. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  603. assert self.dest.name == 'dom0'
  604. kwargs = {}
  605. pool = None
  606. pools = {}
  607. # this will raise exception if none is found
  608. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  609. vm_type)
  610. # if argument is given, it needs to be a valid template, and only
  611. # when given VM class do need a template
  612. if hasattr(vm_class, 'template'):
  613. if self.arg:
  614. assert self.arg in self.app.domains
  615. kwargs['template'] = self.app.domains[self.arg]
  616. else:
  617. assert not self.arg
  618. for untrusted_param in untrusted_payload.decode('ascii',
  619. errors='strict').split(' '):
  620. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  621. if untrusted_key in kwargs:
  622. raise qubes.api.ProtocolError('duplicated parameters')
  623. if untrusted_key == 'name':
  624. qubes.vm.validate_name(None, None, untrusted_value)
  625. kwargs['name'] = untrusted_value
  626. elif untrusted_key == 'label':
  627. # don't confuse label name with label index
  628. assert not untrusted_value.isdigit()
  629. allowed_chars = string.ascii_letters + string.digits + '-_.'
  630. assert all(c in allowed_chars for c in untrusted_value)
  631. try:
  632. kwargs['label'] = self.app.get_label(untrusted_value)
  633. except KeyError:
  634. raise qubes.exc.QubesValueError
  635. elif untrusted_key == 'pool' and allow_pool:
  636. if pool is not None:
  637. raise qubes.api.ProtocolError('duplicated pool parameter')
  638. pool = self.app.get_pool(untrusted_value)
  639. elif untrusted_key.startswith('pool:') and allow_pool:
  640. untrusted_volume = untrusted_key.split(':', 1)[1]
  641. # kind of ugly, but actual list of volumes is available only
  642. # after creating a VM
  643. assert untrusted_volume in ['root', 'private', 'volatile',
  644. 'kernel']
  645. volume = untrusted_volume
  646. if volume in pools:
  647. raise qubes.api.ProtocolError(
  648. 'duplicated pool:{} parameter'.format(volume))
  649. pools[volume] = self.app.get_pool(untrusted_value)
  650. else:
  651. raise qubes.api.ProtocolError('Invalid param name')
  652. del untrusted_payload
  653. if 'name' not in kwargs or 'label' not in kwargs:
  654. raise qubes.api.ProtocolError('Missing name or label')
  655. if pool and pools:
  656. raise qubes.api.ProtocolError(
  657. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  658. if kwargs['name'] in self.app.domains:
  659. raise qubes.exc.QubesValueError(
  660. 'VM {} already exists'.format(kwargs['name']))
  661. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  662. vm = self.app.add_new_vm(vm_class, **kwargs)
  663. # TODO: move this to extension (in race-free fashion)
  664. vm.tags.add('created-by-' + str(self.src))
  665. try:
  666. yield from vm.create_on_disk(pool=pool, pools=pools)
  667. except:
  668. del self.app.domains[vm]
  669. raise
  670. self.app.save()
  671. @qubes.api.method('admin.vm.Remove', no_payload=True)
  672. @asyncio.coroutine
  673. def vm_remove(self):
  674. assert not self.arg
  675. self.fire_event_for_permission()
  676. if not self.dest.is_halted():
  677. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  678. del self.app.domains[self.dest]
  679. try:
  680. yield from self.dest.remove_from_disk()
  681. except: # pylint: disable=bare-except
  682. self.app.log.exception('Error wile removing VM \'%s\' files',
  683. self.dest.name)
  684. self.app.save()
  685. @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
  686. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  687. no_payload=True)
  688. @asyncio.coroutine
  689. def vm_device_available(self, endpoint):
  690. devclass = endpoint
  691. devices = self.dest.devices[devclass].available()
  692. if self.arg:
  693. devices = [dev for dev in devices if dev.ident == self.arg]
  694. # no duplicated devices, but device may not exists, in which case
  695. # the list is empty
  696. assert len(devices) <= 1
  697. devices = self.fire_event_for_filter(devices, devclass=devclass)
  698. dev_info = {}
  699. for dev in devices:
  700. non_default_attrs = set(attr for attr in dir(dev) if
  701. not attr.startswith('_')).difference((
  702. 'backend_domain', 'ident', 'frontend_domain',
  703. 'description', 'options'))
  704. properties_txt = ' '.join(
  705. '{}={!s}'.format(prop, value) for prop, value
  706. in itertools.chain(
  707. ((key, getattr(dev, key)) for key in non_default_attrs),
  708. # keep description as the last one, according to API
  709. # specification
  710. (('description', dev.description),)
  711. ))
  712. assert '\n' not in properties_txt
  713. dev_info[dev.ident] = properties_txt
  714. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  715. for ident in sorted(dev_info))
  716. @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
  717. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  718. no_payload=True)
  719. @asyncio.coroutine
  720. def vm_device_list(self, endpoint):
  721. devclass = endpoint
  722. device_assignments = self.dest.devices[devclass].assignments()
  723. if self.arg:
  724. select_backend, select_ident = self.arg.split('+', 1)
  725. device_assignments = [dev for dev in device_assignments
  726. if (str(dev.backend_domain), dev.ident)
  727. == (select_backend, select_ident)]
  728. # no duplicated devices, but device may not exists, in which case
  729. # the list is empty
  730. assert len(device_assignments) <= 1
  731. device_assignments = self.fire_event_for_filter(device_assignments,
  732. devclass=devclass)
  733. dev_info = {}
  734. for dev in device_assignments:
  735. properties_txt = ' '.join(
  736. '{}={!s}'.format(opt, value) for opt, value
  737. in itertools.chain(
  738. dev.options.items(),
  739. (('persistent', 'yes' if dev.persistent else 'no'),)
  740. ))
  741. assert '\n' not in properties_txt
  742. ident = '{!s}+{!s}'.format(dev.backend_domain, dev.ident)
  743. dev_info[ident] = properties_txt
  744. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  745. for ident in sorted(dev_info))
  746. @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
  747. for ep in pkg_resources.iter_entry_points('qubes.devices')))
  748. @asyncio.coroutine
  749. def vm_device_attach(self, endpoint, untrusted_payload):
  750. devclass = endpoint
  751. options = {}
  752. persistent = False
  753. for untrusted_option in untrusted_payload.decode('ascii').split():
  754. try:
  755. untrusted_key, untrusted_value = untrusted_option.split('=', 1)
  756. except ValueError:
  757. raise qubes.api.ProtocolError('Invalid options format')
  758. if untrusted_key == 'persistent':
  759. persistent = qubes.property.bool(None, None, untrusted_value)
  760. else:
  761. allowed_chars_key = string.digits + string.ascii_letters + '-_.'
  762. allowed_chars_value = allowed_chars_key + ',+:'
  763. if any(x not in allowed_chars_key for x in untrusted_key):
  764. raise qubes.api.ProtocolError(
  765. 'Invalid chars in option name')
  766. if any(x not in allowed_chars_value for x in untrusted_value):
  767. raise qubes.api.ProtocolError(
  768. 'Invalid chars in option value')
  769. options[untrusted_key] = untrusted_value
  770. # qrexec already verified that no strange characters are in self.arg
  771. backend_domain, ident = self.arg.split('+', 1)
  772. # may raise KeyError, either on domain or ident
  773. dev = self.app.domains[backend_domain].devices[devclass][ident]
  774. self.fire_event_for_permission(device=dev,
  775. devclass=devclass, persistent=persistent,
  776. options=options)
  777. assignment = qubes.devices.DeviceAssignment(
  778. dev.backend_domain, dev.ident,
  779. options=options, persistent=persistent)
  780. self.dest.devices[devclass].attach(assignment)
  781. self.app.save()
  782. @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
  783. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  784. no_payload=True)
  785. @asyncio.coroutine
  786. def vm_device_detach(self, endpoint):
  787. devclass = endpoint
  788. # qrexec already verified that no strange characters are in self.arg
  789. backend_domain, ident = self.arg.split('+', 1)
  790. # may raise KeyError; if device isn't found, it will be UnknownDevice
  791. # instance - but allow it, otherwise it will be impossible to detach
  792. # already removed device
  793. dev = self.app.domains[backend_domain].devices[devclass][ident]
  794. self.fire_event_for_permission(device=dev,
  795. devclass=devclass)
  796. assignment = qubes.devices.DeviceAssignment(
  797. dev.backend_domain, dev.ident)
  798. self.dest.devices[devclass].detach(assignment)
  799. self.app.save()
  800. @qubes.api.method('admin.vm.firewall.Get', no_payload=True)
  801. @asyncio.coroutine
  802. def vm_firewall_get(self):
  803. assert not self.arg
  804. self.fire_event_for_permission()
  805. return ''.join('{}\n'.format(rule.api_rule)
  806. for rule in self.dest.firewall.rules)
  807. @qubes.api.method('admin.vm.firewall.Set')
  808. @asyncio.coroutine
  809. def vm_firewall_set(self, untrusted_payload):
  810. assert not self.arg
  811. rules = []
  812. for untrusted_line in untrusted_payload.decode('ascii',
  813. errors='strict').splitlines():
  814. rule = qubes.firewall.Rule.from_api_string(
  815. untrusted_rule=untrusted_line)
  816. rules.append(rule)
  817. self.fire_event_for_permission(rules=rules)
  818. self.dest.firewall.rules = rules
  819. self.dest.firewall.save()
  820. @qubes.api.method('admin.vm.firewall.Reload', no_payload=True)
  821. @asyncio.coroutine
  822. def vm_firewall_reload(self):
  823. assert not self.arg
  824. self.fire_event_for_permission()
  825. self.dest.fire_event('firewall-changed')