mgmt.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  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 string
  26. import pkg_resources
  27. import qubes.vm
  28. import qubes.vm.qubesvm
  29. import qubes.storage
  30. import qubes.utils
  31. class ProtocolError(AssertionError):
  32. '''Raised when something is wrong with data received'''
  33. pass
  34. class PermissionDenied(Exception):
  35. '''Raised deliberately by handlers when we decide not to cooperate'''
  36. pass
  37. def api(name, *, no_payload=False, endpoints=None):
  38. '''Decorator factory for methods intended to appear in API.
  39. The decorated method can be called from public API using a child of
  40. :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
  41. called using remote management interface.
  42. :param str name: qrexec rpc method name
  43. :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
  44. also will not pass payload at all to the method
  45. The expected function method should have one argument (other than usual
  46. *self*), ``untrusted_payload``, which will contain the payload.
  47. .. warning::
  48. This argument has to be named such, to remind the programmer that the
  49. content of this variable is indeed untrusted.
  50. If *no_payload* is true, then the method is called with no arguments.
  51. '''
  52. def decorator(func):
  53. if no_payload:
  54. # the following assignment is needed for how closures work in Python
  55. _func = func
  56. @functools.wraps(_func)
  57. def wrapper(self, untrusted_payload, **kwargs):
  58. if untrusted_payload != b'':
  59. raise ProtocolError('unexpected payload')
  60. return _func(self, **kwargs)
  61. func = wrapper
  62. # pylint: disable=protected-access
  63. if endpoints is None:
  64. func._rpcname = ((name, None),)
  65. else:
  66. func._rpcname = tuple(
  67. (name.format(endpoint=endpoint), endpoint)
  68. for endpoint in endpoints)
  69. return func
  70. return decorator
  71. def apply_filters(iterable, filters):
  72. '''Apply filters returned by mgmt-permission:... event'''
  73. for selector in filters:
  74. iterable = filter(selector, iterable)
  75. return iterable
  76. class AbstractQubesMgmt(object):
  77. '''Common code for Qubes Management Protocol handling
  78. Different interfaces can expose different API call sets, however they share
  79. common protocol and common implementation framework. This class is the
  80. latter.
  81. To implement a new interface, inherit from this class and write at least one
  82. method and decorate it with :py:func:`api` decorator. It will have access to
  83. pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
  84. :py:attr:`arg` and :py:attr:`method`.
  85. There are also two helper functions for firing events associated with API
  86. calls.
  87. '''
  88. def __init__(self, app, src, method, dest, arg, send_event=None):
  89. #: :py:class:`qubes.Qubes` object
  90. self.app = app
  91. #: source qube
  92. self.src = self.app.domains[src.decode('ascii')]
  93. #: destination qube
  94. self.dest = self.app.domains[dest.decode('ascii')]
  95. #: argument
  96. self.arg = arg.decode('ascii')
  97. #: name of the method
  98. self.method = method.decode('ascii')
  99. #: callback for sending events if applicable
  100. self.send_event = send_event
  101. #: is this operation cancellable?
  102. self.cancellable = False
  103. untrusted_candidates = []
  104. for attr in dir(self):
  105. func = getattr(self, attr)
  106. if not callable(func):
  107. continue
  108. try:
  109. # pylint: disable=protected-access
  110. for method, endpoint in func._rpcname:
  111. if method != self.method:
  112. continue
  113. untrusted_candidates.append((func, endpoint))
  114. except AttributeError:
  115. continue
  116. if not untrusted_candidates:
  117. raise ProtocolError('no such method: {!r}'.format(self.method))
  118. assert len(untrusted_candidates) == 1, \
  119. 'multiple candidates for method {!r}'.format(self.method)
  120. #: the method to execute
  121. self._handler = untrusted_candidates[0]
  122. self._running_handler = None
  123. del untrusted_candidates
  124. def execute(self, *, untrusted_payload):
  125. '''Execute management operation.
  126. This method is a coroutine.
  127. '''
  128. handler, endpoint = self._handler
  129. kwargs = {}
  130. if endpoint is not None:
  131. kwargs['endpoint'] = endpoint
  132. self._running_handler = asyncio.ensure_future(handler(
  133. untrusted_payload=untrusted_payload, **kwargs))
  134. return self._running_handler
  135. def cancel(self):
  136. '''If operation is cancellable, interrupt it'''
  137. if self.cancellable and self._running_handler is not None:
  138. self._running_handler.cancel()
  139. def fire_event_for_permission(self, **kwargs):
  140. '''Fire an event on the source qube to check for permission'''
  141. return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
  142. dest=self.dest, arg=self.arg, **kwargs)
  143. def fire_event_for_filter(self, iterable, **kwargs):
  144. '''Fire an event on the source qube to filter for permission'''
  145. return apply_filters(iterable,
  146. self.fire_event_for_permission(**kwargs))
  147. class QubesMgmtEventsDispatcher(object):
  148. def __init__(self, filters, send_event):
  149. self.filters = filters
  150. self.send_event = send_event
  151. def vm_handler(self, subject, event, **kwargs):
  152. if event.startswith('mgmt-permission:'):
  153. return
  154. if not list(apply_filters([(subject, event, kwargs)],
  155. self.filters)):
  156. return
  157. self.send_event(subject, event, **kwargs)
  158. def app_handler(self, subject, event, **kwargs):
  159. if not list(apply_filters([(subject, event, kwargs)],
  160. self.filters)):
  161. return
  162. self.send_event(subject, event, **kwargs)
  163. def on_domain_add(self, subject, event, vm):
  164. # pylint: disable=unused-argument
  165. vm.add_handler('*', self.vm_handler)
  166. def on_domain_delete(self, subject, event, vm):
  167. # pylint: disable=unused-argument
  168. vm.remove_handler('*', self.vm_handler)
  169. class QubesMgmt(AbstractQubesMgmt):
  170. '''Implementation of Qubes Management API calls
  171. This class contains all the methods available in the main API.
  172. .. seealso::
  173. https://www.qubes-os.org/doc/mgmt1/
  174. '''
  175. @api('mgmt.vmclass.List', no_payload=True)
  176. @asyncio.coroutine
  177. def vmclass_list(self):
  178. '''List all VM classes'''
  179. assert not self.arg
  180. assert self.dest.name == 'dom0'
  181. entrypoints = self.fire_event_for_filter(
  182. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  183. return ''.join('{}\n'.format(ep.name)
  184. for ep in entrypoints)
  185. @api('mgmt.vm.List', no_payload=True)
  186. @asyncio.coroutine
  187. def vm_list(self):
  188. '''List all the domains'''
  189. assert not self.arg
  190. if self.dest.name == 'dom0':
  191. domains = self.fire_event_for_filter(self.app.domains)
  192. else:
  193. domains = self.fire_event_for_filter([self.dest])
  194. return ''.join('{} class={} state={}\n'.format(
  195. vm.name,
  196. vm.__class__.__name__,
  197. vm.get_power_state())
  198. for vm in sorted(domains))
  199. @api('mgmt.vm.property.List', no_payload=True)
  200. @asyncio.coroutine
  201. def vm_property_list(self):
  202. '''List all properties on a qube'''
  203. assert not self.arg
  204. properties = self.fire_event_for_filter(self.dest.property_list())
  205. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  206. @api('mgmt.vm.property.Get', no_payload=True)
  207. @asyncio.coroutine
  208. def vm_property_get(self):
  209. '''Get a value of one property'''
  210. assert self.arg in self.dest.property_list()
  211. self.fire_event_for_permission()
  212. property_def = self.dest.property_get_def(self.arg)
  213. # explicit list to be sure that it matches protocol spec
  214. if isinstance(property_def, qubes.vm.VMProperty):
  215. property_type = 'vm'
  216. elif property_def.type is int:
  217. property_type = 'int'
  218. elif property_def.type is bool:
  219. property_type = 'bool'
  220. elif self.arg == 'label':
  221. property_type = 'label'
  222. else:
  223. property_type = 'str'
  224. try:
  225. value = getattr(self.dest, self.arg)
  226. except AttributeError:
  227. return 'default=True type={} '.format(property_type)
  228. else:
  229. return 'default={} type={} {}'.format(
  230. str(self.dest.property_is_default(self.arg)),
  231. property_type,
  232. str(value) if value is not None else '')
  233. @api('mgmt.vm.property.Set')
  234. @asyncio.coroutine
  235. def vm_property_set(self, untrusted_payload):
  236. assert self.arg in self.dest.property_list()
  237. property_def = self.dest.property_get_def(self.arg)
  238. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  239. self.fire_event_for_permission(newvalue=newvalue)
  240. setattr(self.dest, self.arg, newvalue)
  241. self.app.save()
  242. @api('mgmt.vm.property.Help', no_payload=True)
  243. @asyncio.coroutine
  244. def vm_property_help(self):
  245. '''Get help for one property'''
  246. assert self.arg in self.dest.property_list()
  247. self.fire_event_for_permission()
  248. try:
  249. doc = self.dest.property_get_def(self.arg).__doc__
  250. except AttributeError:
  251. return ''
  252. return qubes.utils.format_doc(doc)
  253. @api('mgmt.vm.property.Reset', no_payload=True)
  254. @asyncio.coroutine
  255. def vm_property_reset(self):
  256. '''Reset a property to a default value'''
  257. assert self.arg in self.dest.property_list()
  258. self.fire_event_for_permission()
  259. delattr(self.dest, self.arg)
  260. self.app.save()
  261. @api('mgmt.vm.volume.List', no_payload=True)
  262. @asyncio.coroutine
  263. def vm_volume_list(self):
  264. assert not self.arg
  265. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  266. return ''.join('{}\n'.format(name) for name in volume_names)
  267. @api('mgmt.vm.volume.Info', no_payload=True)
  268. @asyncio.coroutine
  269. def vm_volume_info(self):
  270. assert self.arg in self.dest.volumes.keys()
  271. self.fire_event_for_permission()
  272. volume = self.dest.volumes[self.arg]
  273. # properties defined in API
  274. volume_properties = [
  275. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  276. 'save_on_stop', 'snap_on_start']
  277. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  278. volume_properties)
  279. @api('mgmt.vm.volume.ListSnapshots', no_payload=True)
  280. @asyncio.coroutine
  281. def vm_volume_listsnapshots(self):
  282. assert self.arg in self.dest.volumes.keys()
  283. volume = self.dest.volumes[self.arg]
  284. revisions = [revision for revision in volume.revisions]
  285. revisions = self.fire_event_for_filter(revisions)
  286. return ''.join('{}\n'.format(revision) for revision in revisions)
  287. @api('mgmt.vm.volume.Revert')
  288. @asyncio.coroutine
  289. def vm_volume_revert(self, untrusted_payload):
  290. assert self.arg in self.dest.volumes.keys()
  291. untrusted_revision = untrusted_payload.decode('ascii').strip()
  292. del untrusted_payload
  293. volume = self.dest.volumes[self.arg]
  294. snapshots = volume.revisions
  295. assert untrusted_revision in snapshots
  296. revision = untrusted_revision
  297. self.fire_event_for_permission(revision=revision)
  298. self.dest.storage.get_pool(volume).revert(revision)
  299. self.app.save()
  300. @api('mgmt.vm.volume.Resize')
  301. @asyncio.coroutine
  302. def vm_volume_resize(self, untrusted_payload):
  303. assert self.arg in self.dest.volumes.keys()
  304. untrusted_size = untrusted_payload.decode('ascii').strip()
  305. del untrusted_payload
  306. assert untrusted_size.isdigit() # only digits, forbid '-' too
  307. assert len(untrusted_size) <= 20 # limit to about 2^64
  308. size = int(untrusted_size)
  309. self.fire_event_for_permission(size=size)
  310. self.dest.storage.resize(self.arg, size)
  311. self.app.save()
  312. @api('mgmt.pool.List', no_payload=True)
  313. @asyncio.coroutine
  314. def pool_list(self):
  315. assert not self.arg
  316. assert self.dest.name == 'dom0'
  317. pools = self.fire_event_for_filter(self.app.pools)
  318. return ''.join('{}\n'.format(pool) for pool in pools)
  319. @api('mgmt.pool.ListDrivers', no_payload=True)
  320. @asyncio.coroutine
  321. def pool_listdrivers(self):
  322. assert self.dest.name == 'dom0'
  323. assert not self.arg
  324. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  325. return ''.join('{} {}\n'.format(
  326. driver,
  327. ' '.join(qubes.storage.driver_parameters(driver)))
  328. for driver in drivers)
  329. @api('mgmt.pool.Info', no_payload=True)
  330. @asyncio.coroutine
  331. def pool_info(self):
  332. assert self.dest.name == 'dom0'
  333. assert self.arg in self.app.pools.keys()
  334. pool = self.app.pools[self.arg]
  335. self.fire_event_for_permission(pool=pool)
  336. return ''.join('{}={}\n'.format(prop, val)
  337. for prop, val in sorted(pool.config.items()))
  338. @api('mgmt.pool.Add')
  339. @asyncio.coroutine
  340. def pool_add(self, untrusted_payload):
  341. assert self.dest.name == 'dom0'
  342. drivers = qubes.storage.pool_drivers()
  343. assert self.arg in drivers
  344. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  345. del untrusted_payload
  346. assert all(('=' in line) for line in untrusted_pool_config)
  347. # pairs of (option, value)
  348. untrusted_pool_config = [line.split('=', 1)
  349. for line in untrusted_pool_config]
  350. # reject duplicated options
  351. assert len(set(x[0] for x in untrusted_pool_config)) == \
  352. len([x[0] for x in untrusted_pool_config])
  353. # and convert to dict
  354. untrusted_pool_config = dict(untrusted_pool_config)
  355. assert 'name' in untrusted_pool_config
  356. untrusted_pool_name = untrusted_pool_config.pop('name')
  357. allowed_chars = string.ascii_letters + string.digits + '-_.'
  358. assert all(c in allowed_chars for c in untrusted_pool_name)
  359. pool_name = untrusted_pool_name
  360. assert pool_name not in self.app.pools
  361. driver_parameters = qubes.storage.driver_parameters(self.arg)
  362. assert all(key in driver_parameters for key in untrusted_pool_config)
  363. pool_config = untrusted_pool_config
  364. self.fire_event_for_permission(name=pool_name,
  365. pool_config=pool_config)
  366. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  367. self.app.save()
  368. @api('mgmt.pool.Remove', no_payload=True)
  369. @asyncio.coroutine
  370. def pool_remove(self):
  371. assert self.dest.name == 'dom0'
  372. assert self.arg in self.app.pools.keys()
  373. self.fire_event_for_permission()
  374. self.app.remove_pool(self.arg)
  375. self.app.save()
  376. @api('mgmt.label.List', no_payload=True)
  377. @asyncio.coroutine
  378. def label_list(self):
  379. assert self.dest.name == 'dom0'
  380. assert not self.arg
  381. labels = self.fire_event_for_filter(self.app.labels.values())
  382. return ''.join('{}\n'.format(label.name) for label in labels)
  383. @api('mgmt.label.Get', no_payload=True)
  384. @asyncio.coroutine
  385. def label_get(self):
  386. assert self.dest.name == 'dom0'
  387. try:
  388. label = self.app.get_label(self.arg)
  389. except KeyError:
  390. raise qubes.exc.QubesValueError
  391. self.fire_event_for_permission(label=label)
  392. return label.color
  393. @api('mgmt.label.Index', no_payload=True)
  394. @asyncio.coroutine
  395. def label_index(self):
  396. assert self.dest.name == 'dom0'
  397. try:
  398. label = self.app.get_label(self.arg)
  399. except KeyError:
  400. raise qubes.exc.QubesValueError
  401. self.fire_event_for_permission(label=label)
  402. return str(label.index)
  403. @api('mgmt.label.Create')
  404. @asyncio.coroutine
  405. def label_create(self, untrusted_payload):
  406. assert self.dest.name == 'dom0'
  407. # don't confuse label name with label index
  408. assert not self.arg.isdigit()
  409. allowed_chars = string.ascii_letters + string.digits + '-_.'
  410. assert all(c in allowed_chars for c in self.arg)
  411. try:
  412. self.app.get_label(self.arg)
  413. except KeyError:
  414. # ok, no such label yet
  415. pass
  416. else:
  417. raise qubes.exc.QubesValueError('label already exists')
  418. untrusted_payload = untrusted_payload.decode('ascii').strip()
  419. assert len(untrusted_payload) == 8
  420. assert untrusted_payload.startswith('0x')
  421. # besides prefix, only hex digits are allowed
  422. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  423. # SEE: #2732
  424. color = untrusted_payload
  425. self.fire_event_for_permission(color=color)
  426. # allocate new index, but make sure it's outside of default labels set
  427. new_index = max(
  428. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  429. label = qubes.Label(new_index, color, self.arg)
  430. self.app.labels[new_index] = label
  431. self.app.save()
  432. @api('mgmt.label.Remove', no_payload=True)
  433. @asyncio.coroutine
  434. def label_remove(self):
  435. assert self.dest.name == 'dom0'
  436. try:
  437. label = self.app.get_label(self.arg)
  438. except KeyError:
  439. raise qubes.exc.QubesValueError
  440. # don't allow removing default labels
  441. assert label.index > qubes.config.max_default_label
  442. # FIXME: this should be in app.add_label()
  443. for vm in self.app.domains:
  444. if vm.label == label:
  445. raise qubes.exc.QubesException('label still in use')
  446. self.fire_event_for_permission(label=label)
  447. del self.app.labels[label.index]
  448. self.app.save()
  449. @api('mgmt.vm.Start', no_payload=True)
  450. @asyncio.coroutine
  451. def vm_start(self):
  452. assert not self.arg
  453. self.fire_event_for_permission()
  454. yield from self.dest.start()
  455. @api('mgmt.vm.Shutdown', no_payload=True)
  456. @asyncio.coroutine
  457. def vm_shutdown(self):
  458. assert not self.arg
  459. self.fire_event_for_permission()
  460. yield from self.dest.shutdown()
  461. @api('mgmt.vm.Pause', no_payload=True)
  462. @asyncio.coroutine
  463. def vm_pause(self):
  464. assert not self.arg
  465. self.fire_event_for_permission()
  466. yield from self.dest.pause()
  467. @api('mgmt.vm.Unpause', no_payload=True)
  468. @asyncio.coroutine
  469. def vm_unpause(self):
  470. assert not self.arg
  471. self.fire_event_for_permission()
  472. yield from self.dest.unpause()
  473. @api('mgmt.vm.Kill', no_payload=True)
  474. @asyncio.coroutine
  475. def vm_kill(self):
  476. assert not self.arg
  477. self.fire_event_for_permission()
  478. yield from self.dest.kill()
  479. @api('mgmt.Events', no_payload=True)
  480. @asyncio.coroutine
  481. def events(self):
  482. assert not self.arg
  483. # run until client connection is terminated
  484. self.cancellable = True
  485. wait_for_cancel = asyncio.get_event_loop().create_future()
  486. # cache event filters, to not call an event each time an event arrives
  487. event_filters = self.fire_event_for_permission()
  488. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  489. if self.dest.name == 'dom0':
  490. self.app.add_handler('*', dispatcher.app_handler)
  491. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  492. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  493. for vm in self.app.domains:
  494. vm.add_handler('*', dispatcher.vm_handler)
  495. else:
  496. self.dest.add_handler('*', dispatcher.vm_handler)
  497. # send artificial event as a confirmation that connection is established
  498. self.send_event(self.app, 'connection-established')
  499. try:
  500. yield from wait_for_cancel
  501. except asyncio.CancelledError:
  502. # the above waiting was already interrupted, this is all we need
  503. pass
  504. if self.dest.name == 'dom0':
  505. self.app.remove_handler('*', dispatcher.app_handler)
  506. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  507. self.app.remove_handler('domain-delete',
  508. dispatcher.on_domain_delete)
  509. for vm in self.app.domains:
  510. vm.remove_handler('*', dispatcher.vm_handler)
  511. else:
  512. self.dest.remove_handler('*', dispatcher.vm_handler)
  513. @api('mgmt.vm.feature.List', no_payload=True)
  514. @asyncio.coroutine
  515. def vm_feature_list(self):
  516. assert not self.arg
  517. features = self.fire_event_for_filter(self.dest.features.keys())
  518. return ''.join('{}\n'.format(feature) for feature in features)
  519. @api('mgmt.vm.feature.Get', no_payload=True)
  520. @asyncio.coroutine
  521. def vm_feature_get(self):
  522. # validation of self.arg done by qrexec-policy is enough
  523. self.fire_event_for_permission()
  524. try:
  525. value = self.dest.features[self.arg]
  526. except KeyError:
  527. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  528. return value
  529. @api('mgmt.vm.feature.CheckWithTemplate', no_payload=True)
  530. @asyncio.coroutine
  531. def vm_feature_checkwithtemplate(self):
  532. # validation of self.arg done by qrexec-policy is enough
  533. self.fire_event_for_permission()
  534. try:
  535. value = self.dest.features.check_with_template(self.arg)
  536. except KeyError:
  537. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  538. return value
  539. @api('mgmt.vm.feature.Remove', no_payload=True)
  540. @asyncio.coroutine
  541. def vm_feature_remove(self):
  542. # validation of self.arg done by qrexec-policy is enough
  543. self.fire_event_for_permission()
  544. try:
  545. del self.dest.features[self.arg]
  546. except KeyError:
  547. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  548. self.app.save()
  549. @api('mgmt.vm.feature.Set')
  550. @asyncio.coroutine
  551. def vm_feature_set(self, untrusted_payload):
  552. # validation of self.arg done by qrexec-policy is enough
  553. value = untrusted_payload.decode('ascii', errors='strict')
  554. del untrusted_payload
  555. self.fire_event_for_permission(value=value)
  556. self.dest.features[self.arg] = value
  557. self.app.save()
  558. @api('mgmt.vm.Create.{endpoint}', endpoints=(ep.name
  559. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  560. @asyncio.coroutine
  561. def vm_create(self, endpoint, untrusted_payload=None):
  562. return self._vm_create(endpoint, allow_pool=False,
  563. untrusted_payload=untrusted_payload)
  564. @api('mgmt.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  565. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  566. @asyncio.coroutine
  567. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  568. return self._vm_create(endpoint, allow_pool=True,
  569. untrusted_payload=untrusted_payload)
  570. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  571. assert self.dest.name == 'dom0'
  572. kwargs = {}
  573. pool = None
  574. pools = {}
  575. # this will raise exception if none is found
  576. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  577. vm_type)
  578. # if argument is given, it needs to be a valid template, and only
  579. # when given VM class do need a template
  580. if hasattr(vm_class, 'template'):
  581. assert self.arg in self.app.domains
  582. kwargs['template'] = self.app.domains[self.arg]
  583. else:
  584. assert not self.arg
  585. for untrusted_param in untrusted_payload.decode('ascii',
  586. errors='strict').split(' '):
  587. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  588. if untrusted_key in kwargs:
  589. raise ProtocolError('duplicated parameters')
  590. if untrusted_key == 'name':
  591. qubes.vm.validate_name(None, None, untrusted_value)
  592. kwargs['name'] = untrusted_value
  593. elif untrusted_key == 'label':
  594. # don't confuse label name with label index
  595. assert not untrusted_value.isdigit()
  596. allowed_chars = string.ascii_letters + string.digits + '-_.'
  597. assert all(c in allowed_chars for c in untrusted_value)
  598. try:
  599. kwargs['label'] = self.app.get_label(untrusted_value)
  600. except KeyError:
  601. raise qubes.exc.QubesValueError
  602. elif untrusted_key == 'pool' and allow_pool:
  603. if pool is not None:
  604. raise ProtocolError('duplicated pool parameter')
  605. pool = self.app.get_pool(untrusted_value)
  606. elif untrusted_key.startswith('pool:') and allow_pool:
  607. untrusted_volume = untrusted_key.split(':', 1)[1]
  608. # kind of ugly, but actual list of volumes is available only
  609. # after creating a VM
  610. assert untrusted_volume in ['root', 'private', 'volatile',
  611. 'kernel']
  612. volume = untrusted_volume
  613. if volume in pools:
  614. raise ProtocolError(
  615. 'duplicated pool:{} parameter'.format(volume))
  616. pools[volume] = self.app.get_pool(untrusted_value)
  617. else:
  618. raise ProtocolError('Invalid param name')
  619. del untrusted_payload
  620. if 'name' not in kwargs or 'label' not in kwargs:
  621. raise ProtocolError('Missing name or label')
  622. if pool and pools:
  623. raise ProtocolError(
  624. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  625. if kwargs['name'] in self.app.domains:
  626. raise qubes.exc.QubesValueError(
  627. 'VM {} already exists'.format(kwargs['name']))
  628. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  629. vm = self.app.add_new_vm(vm_class, **kwargs)
  630. try:
  631. yield from vm.create_on_disk(pool=pool, pools=pools)
  632. except:
  633. del self.app.domains[vm]
  634. raise
  635. self.app.save()
  636. @api('mgmt.vm.Clone')
  637. @asyncio.coroutine
  638. def vm_clone(self, untrusted_payload):
  639. assert not self.arg
  640. assert untrusted_payload.startswith(b'name=')
  641. untrusted_name = untrusted_payload[5:].decode('ascii')
  642. qubes.vm.validate_name(None, None, untrusted_name)
  643. new_name = untrusted_name
  644. del untrusted_payload
  645. if new_name in self.app.domains:
  646. raise qubes.exc.QubesValueError('Already exists')
  647. self.fire_event_for_permission(new_name=new_name)
  648. src_vm = self.dest
  649. dst_vm = self.app.add_new_vm(src_vm.__class__, name=new_name)
  650. try:
  651. dst_vm.clone_properties(src_vm)
  652. # TODO: tags
  653. # TODO: features
  654. # TODO: firewall
  655. # TODO: persistent devices
  656. yield from dst_vm.clone_disk_files(src_vm)
  657. except:
  658. del self.app.domains[dst_vm]
  659. raise
  660. self.app.save()