admin.py 39 KB

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