admin.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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 pkg_resources
  26. import qubes.api
  27. import qubes.storage
  28. import qubes.utils
  29. import qubes.vm
  30. import qubes.vm.qubesvm
  31. class QubesMgmtEventsDispatcher(object):
  32. def __init__(self, filters, send_event):
  33. self.filters = filters
  34. self.send_event = send_event
  35. def vm_handler(self, subject, event, **kwargs):
  36. if event.startswith('mgmt-permission:'):
  37. return
  38. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  39. self.filters)):
  40. return
  41. self.send_event(subject, event, **kwargs)
  42. def app_handler(self, subject, event, **kwargs):
  43. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  44. self.filters)):
  45. return
  46. self.send_event(subject, event, **kwargs)
  47. def on_domain_add(self, subject, event, vm):
  48. # pylint: disable=unused-argument
  49. vm.add_handler('*', self.vm_handler)
  50. def on_domain_delete(self, subject, event, vm):
  51. # pylint: disable=unused-argument
  52. vm.remove_handler('*', self.vm_handler)
  53. class QubesAdminAPI(qubes.api.AbstractQubesAPI):
  54. '''Implementation of Qubes Management API calls
  55. This class contains all the methods available in the main API.
  56. .. seealso::
  57. https://www.qubes-os.org/doc/mgmt1/
  58. '''
  59. @qubes.api.method('admin.vmclass.List', no_payload=True)
  60. @asyncio.coroutine
  61. def vmclass_list(self):
  62. '''List all VM classes'''
  63. assert not self.arg
  64. assert self.dest.name == 'dom0'
  65. entrypoints = self.fire_event_for_filter(
  66. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  67. return ''.join('{}\n'.format(ep.name)
  68. for ep in entrypoints)
  69. @qubes.api.method('admin.vm.List', no_payload=True)
  70. @asyncio.coroutine
  71. def vm_list(self):
  72. '''List all the domains'''
  73. assert not self.arg
  74. if self.dest.name == 'dom0':
  75. domains = self.fire_event_for_filter(self.app.domains)
  76. else:
  77. domains = self.fire_event_for_filter([self.dest])
  78. return ''.join('{} class={} state={}\n'.format(
  79. vm.name,
  80. vm.__class__.__name__,
  81. vm.get_power_state())
  82. for vm in sorted(domains))
  83. @qubes.api.method('admin.vm.property.List', no_payload=True)
  84. @asyncio.coroutine
  85. def vm_property_list(self):
  86. '''List all properties on a qube'''
  87. assert not self.arg
  88. properties = self.fire_event_for_filter(self.dest.property_list())
  89. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  90. @qubes.api.method('admin.vm.property.Get', no_payload=True)
  91. @asyncio.coroutine
  92. def vm_property_get(self):
  93. '''Get a value of one property'''
  94. assert self.arg in self.dest.property_list()
  95. self.fire_event_for_permission()
  96. property_def = self.dest.property_get_def(self.arg)
  97. # explicit list to be sure that it matches protocol spec
  98. if isinstance(property_def, qubes.vm.VMProperty):
  99. property_type = 'vm'
  100. elif property_def.type is int:
  101. property_type = 'int'
  102. elif property_def.type is bool:
  103. property_type = 'bool'
  104. elif self.arg == 'label':
  105. property_type = 'label'
  106. else:
  107. property_type = 'str'
  108. try:
  109. value = getattr(self.dest, self.arg)
  110. except AttributeError:
  111. return 'default=True type={} '.format(property_type)
  112. else:
  113. return 'default={} type={} {}'.format(
  114. str(self.dest.property_is_default(self.arg)),
  115. property_type,
  116. str(value) if value is not None else '')
  117. @qubes.api.method('admin.vm.property.Set')
  118. @asyncio.coroutine
  119. def vm_property_set(self, untrusted_payload):
  120. assert self.arg in self.dest.property_list()
  121. property_def = self.dest.property_get_def(self.arg)
  122. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  123. self.fire_event_for_permission(newvalue=newvalue)
  124. setattr(self.dest, self.arg, newvalue)
  125. self.app.save()
  126. @qubes.api.method('admin.vm.property.Help', no_payload=True)
  127. @asyncio.coroutine
  128. def vm_property_help(self):
  129. '''Get help for one property'''
  130. assert self.arg in self.dest.property_list()
  131. self.fire_event_for_permission()
  132. try:
  133. doc = self.dest.property_get_def(self.arg).__doc__
  134. except AttributeError:
  135. return ''
  136. return qubes.utils.format_doc(doc)
  137. @qubes.api.method('admin.vm.property.Reset', no_payload=True)
  138. @asyncio.coroutine
  139. def vm_property_reset(self):
  140. '''Reset a property to a default value'''
  141. assert self.arg in self.dest.property_list()
  142. self.fire_event_for_permission()
  143. delattr(self.dest, self.arg)
  144. self.app.save()
  145. @qubes.api.method('admin.vm.volume.List', no_payload=True)
  146. @asyncio.coroutine
  147. def vm_volume_list(self):
  148. assert not self.arg
  149. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  150. return ''.join('{}\n'.format(name) for name in volume_names)
  151. @qubes.api.method('admin.vm.volume.Info', no_payload=True)
  152. @asyncio.coroutine
  153. def vm_volume_info(self):
  154. assert self.arg in self.dest.volumes.keys()
  155. self.fire_event_for_permission()
  156. volume = self.dest.volumes[self.arg]
  157. # properties defined in API
  158. volume_properties = [
  159. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  160. 'save_on_stop', 'snap_on_start']
  161. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  162. volume_properties)
  163. @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True)
  164. @asyncio.coroutine
  165. def vm_volume_listsnapshots(self):
  166. assert self.arg in self.dest.volumes.keys()
  167. volume = self.dest.volumes[self.arg]
  168. revisions = [revision for revision in volume.revisions]
  169. revisions = self.fire_event_for_filter(revisions)
  170. return ''.join('{}\n'.format(revision) for revision in revisions)
  171. @qubes.api.method('admin.vm.volume.Revert')
  172. @asyncio.coroutine
  173. def vm_volume_revert(self, untrusted_payload):
  174. assert self.arg in self.dest.volumes.keys()
  175. untrusted_revision = untrusted_payload.decode('ascii').strip()
  176. del untrusted_payload
  177. volume = self.dest.volumes[self.arg]
  178. snapshots = volume.revisions
  179. assert untrusted_revision in snapshots
  180. revision = untrusted_revision
  181. self.fire_event_for_permission(revision=revision)
  182. self.dest.storage.get_pool(volume).revert(revision)
  183. self.app.save()
  184. @qubes.api.method('admin.vm.volume.Resize')
  185. @asyncio.coroutine
  186. def vm_volume_resize(self, untrusted_payload):
  187. assert self.arg in self.dest.volumes.keys()
  188. untrusted_size = untrusted_payload.decode('ascii').strip()
  189. del untrusted_payload
  190. assert untrusted_size.isdigit() # only digits, forbid '-' too
  191. assert len(untrusted_size) <= 20 # limit to about 2^64
  192. size = int(untrusted_size)
  193. self.fire_event_for_permission(size=size)
  194. self.dest.storage.resize(self.arg, size)
  195. self.app.save()
  196. @qubes.api.method('admin.pool.List', no_payload=True)
  197. @asyncio.coroutine
  198. def pool_list(self):
  199. assert not self.arg
  200. assert self.dest.name == 'dom0'
  201. pools = self.fire_event_for_filter(self.app.pools)
  202. return ''.join('{}\n'.format(pool) for pool in pools)
  203. @qubes.api.method('admin.pool.ListDrivers', no_payload=True)
  204. @asyncio.coroutine
  205. def pool_listdrivers(self):
  206. assert self.dest.name == 'dom0'
  207. assert not self.arg
  208. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  209. return ''.join('{} {}\n'.format(
  210. driver,
  211. ' '.join(qubes.storage.driver_parameters(driver)))
  212. for driver in drivers)
  213. @qubes.api.method('admin.pool.Info', no_payload=True)
  214. @asyncio.coroutine
  215. def pool_info(self):
  216. assert self.dest.name == 'dom0'
  217. assert self.arg in self.app.pools.keys()
  218. pool = self.app.pools[self.arg]
  219. self.fire_event_for_permission(pool=pool)
  220. return ''.join('{}={}\n'.format(prop, val)
  221. for prop, val in sorted(pool.config.items()))
  222. @qubes.api.method('admin.pool.Add')
  223. @asyncio.coroutine
  224. def pool_add(self, untrusted_payload):
  225. assert self.dest.name == 'dom0'
  226. drivers = qubes.storage.pool_drivers()
  227. assert self.arg in drivers
  228. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  229. del untrusted_payload
  230. assert all(('=' in line) for line in untrusted_pool_config)
  231. # pairs of (option, value)
  232. untrusted_pool_config = [line.split('=', 1)
  233. for line in untrusted_pool_config]
  234. # reject duplicated options
  235. assert len(set(x[0] for x in untrusted_pool_config)) == \
  236. len([x[0] for x in untrusted_pool_config])
  237. # and convert to dict
  238. untrusted_pool_config = dict(untrusted_pool_config)
  239. assert 'name' in untrusted_pool_config
  240. untrusted_pool_name = untrusted_pool_config.pop('name')
  241. allowed_chars = string.ascii_letters + string.digits + '-_.'
  242. assert all(c in allowed_chars for c in untrusted_pool_name)
  243. pool_name = untrusted_pool_name
  244. assert pool_name not in self.app.pools
  245. driver_parameters = qubes.storage.driver_parameters(self.arg)
  246. assert all(key in driver_parameters for key in untrusted_pool_config)
  247. pool_config = untrusted_pool_config
  248. self.fire_event_for_permission(name=pool_name,
  249. pool_config=pool_config)
  250. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  251. self.app.save()
  252. @qubes.api.method('admin.pool.Remove', no_payload=True)
  253. @asyncio.coroutine
  254. def pool_remove(self):
  255. assert self.dest.name == 'dom0'
  256. assert self.arg in self.app.pools.keys()
  257. self.fire_event_for_permission()
  258. self.app.remove_pool(self.arg)
  259. self.app.save()
  260. @qubes.api.method('admin.label.List', no_payload=True)
  261. @asyncio.coroutine
  262. def label_list(self):
  263. assert self.dest.name == 'dom0'
  264. assert not self.arg
  265. labels = self.fire_event_for_filter(self.app.labels.values())
  266. return ''.join('{}\n'.format(label.name) for label in labels)
  267. @qubes.api.method('admin.label.Get', no_payload=True)
  268. @asyncio.coroutine
  269. def label_get(self):
  270. assert self.dest.name == 'dom0'
  271. try:
  272. label = self.app.get_label(self.arg)
  273. except KeyError:
  274. raise qubes.exc.QubesValueError
  275. self.fire_event_for_permission(label=label)
  276. return label.color
  277. @qubes.api.method('admin.label.Index', no_payload=True)
  278. @asyncio.coroutine
  279. def label_index(self):
  280. assert self.dest.name == 'dom0'
  281. try:
  282. label = self.app.get_label(self.arg)
  283. except KeyError:
  284. raise qubes.exc.QubesValueError
  285. self.fire_event_for_permission(label=label)
  286. return str(label.index)
  287. @qubes.api.method('admin.label.Create')
  288. @asyncio.coroutine
  289. def label_create(self, untrusted_payload):
  290. assert self.dest.name == 'dom0'
  291. # don't confuse label name with label index
  292. assert not self.arg.isdigit()
  293. allowed_chars = string.ascii_letters + string.digits + '-_.'
  294. assert all(c in allowed_chars for c in self.arg)
  295. try:
  296. self.app.get_label(self.arg)
  297. except KeyError:
  298. # ok, no such label yet
  299. pass
  300. else:
  301. raise qubes.exc.QubesValueError('label already exists')
  302. untrusted_payload = untrusted_payload.decode('ascii').strip()
  303. assert len(untrusted_payload) == 8
  304. assert untrusted_payload.startswith('0x')
  305. # besides prefix, only hex digits are allowed
  306. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  307. # SEE: #2732
  308. color = untrusted_payload
  309. self.fire_event_for_permission(color=color)
  310. # allocate new index, but make sure it's outside of default labels set
  311. new_index = max(
  312. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  313. label = qubes.Label(new_index, color, self.arg)
  314. self.app.labels[new_index] = label
  315. self.app.save()
  316. @qubes.api.method('admin.label.Remove', no_payload=True)
  317. @asyncio.coroutine
  318. def label_remove(self):
  319. assert self.dest.name == 'dom0'
  320. try:
  321. label = self.app.get_label(self.arg)
  322. except KeyError:
  323. raise qubes.exc.QubesValueError
  324. # don't allow removing default labels
  325. assert label.index > qubes.config.max_default_label
  326. # FIXME: this should be in app.add_label()
  327. for vm in self.app.domains:
  328. if vm.label == label:
  329. raise qubes.exc.QubesException('label still in use')
  330. self.fire_event_for_permission(label=label)
  331. del self.app.labels[label.index]
  332. self.app.save()
  333. @qubes.api.method('admin.vm.Start', no_payload=True)
  334. @asyncio.coroutine
  335. def vm_start(self):
  336. assert not self.arg
  337. self.fire_event_for_permission()
  338. yield from self.dest.start()
  339. @qubes.api.method('admin.vm.Shutdown', no_payload=True)
  340. @asyncio.coroutine
  341. def vm_shutdown(self):
  342. assert not self.arg
  343. self.fire_event_for_permission()
  344. yield from self.dest.shutdown()
  345. @qubes.api.method('admin.vm.Pause', no_payload=True)
  346. @asyncio.coroutine
  347. def vm_pause(self):
  348. assert not self.arg
  349. self.fire_event_for_permission()
  350. yield from self.dest.pause()
  351. @qubes.api.method('admin.vm.Unpause', no_payload=True)
  352. @asyncio.coroutine
  353. def vm_unpause(self):
  354. assert not self.arg
  355. self.fire_event_for_permission()
  356. yield from self.dest.unpause()
  357. @qubes.api.method('admin.vm.Kill', no_payload=True)
  358. @asyncio.coroutine
  359. def vm_kill(self):
  360. assert not self.arg
  361. self.fire_event_for_permission()
  362. yield from self.dest.kill()
  363. @qubes.api.method('admin.Events', no_payload=True)
  364. @asyncio.coroutine
  365. def events(self):
  366. assert not self.arg
  367. # run until client connection is terminated
  368. self.cancellable = True
  369. wait_for_cancel = asyncio.get_event_loop().create_future()
  370. # cache event filters, to not call an event each time an event arrives
  371. event_filters = self.fire_event_for_permission()
  372. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  373. if self.dest.name == 'dom0':
  374. self.app.add_handler('*', dispatcher.app_handler)
  375. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  376. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  377. for vm in self.app.domains:
  378. vm.add_handler('*', dispatcher.vm_handler)
  379. else:
  380. self.dest.add_handler('*', dispatcher.vm_handler)
  381. # send artificial event as a confirmation that connection is established
  382. self.send_event(self.app, 'connection-established')
  383. try:
  384. yield from wait_for_cancel
  385. except asyncio.CancelledError:
  386. # the above waiting was already interrupted, this is all we need
  387. pass
  388. if self.dest.name == 'dom0':
  389. self.app.remove_handler('*', dispatcher.app_handler)
  390. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  391. self.app.remove_handler('domain-delete',
  392. dispatcher.on_domain_delete)
  393. for vm in self.app.domains:
  394. vm.remove_handler('*', dispatcher.vm_handler)
  395. else:
  396. self.dest.remove_handler('*', dispatcher.vm_handler)
  397. @qubes.api.method('admin.vm.feature.List', no_payload=True)
  398. @asyncio.coroutine
  399. def vm_feature_list(self):
  400. assert not self.arg
  401. features = self.fire_event_for_filter(self.dest.features.keys())
  402. return ''.join('{}\n'.format(feature) for feature in features)
  403. @qubes.api.method('admin.vm.feature.Get', no_payload=True)
  404. @asyncio.coroutine
  405. def vm_feature_get(self):
  406. # validation of self.arg done by qrexec-policy is enough
  407. self.fire_event_for_permission()
  408. try:
  409. value = self.dest.features[self.arg]
  410. except KeyError:
  411. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  412. return value
  413. @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True)
  414. @asyncio.coroutine
  415. def vm_feature_checkwithtemplate(self):
  416. # validation of self.arg done by qrexec-policy is enough
  417. self.fire_event_for_permission()
  418. try:
  419. value = self.dest.features.check_with_template(self.arg)
  420. except KeyError:
  421. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  422. return value
  423. @qubes.api.method('admin.vm.feature.Remove', no_payload=True)
  424. @asyncio.coroutine
  425. def vm_feature_remove(self):
  426. # validation of self.arg done by qrexec-policy is enough
  427. self.fire_event_for_permission()
  428. try:
  429. del self.dest.features[self.arg]
  430. except KeyError:
  431. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  432. self.app.save()
  433. @qubes.api.method('admin.vm.feature.Set')
  434. @asyncio.coroutine
  435. def vm_feature_set(self, untrusted_payload):
  436. # validation of self.arg done by qrexec-policy is enough
  437. value = untrusted_payload.decode('ascii', errors='strict')
  438. del untrusted_payload
  439. self.fire_event_for_permission(value=value)
  440. self.dest.features[self.arg] = value
  441. self.app.save()
  442. @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
  443. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  444. @asyncio.coroutine
  445. def vm_create(self, endpoint, untrusted_payload=None):
  446. return self._vm_create(endpoint, allow_pool=False,
  447. untrusted_payload=untrusted_payload)
  448. @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  449. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
  450. @asyncio.coroutine
  451. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  452. return self._vm_create(endpoint, allow_pool=True,
  453. untrusted_payload=untrusted_payload)
  454. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  455. assert self.dest.name == 'dom0'
  456. kwargs = {}
  457. pool = None
  458. pools = {}
  459. # this will raise exception if none is found
  460. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  461. vm_type)
  462. # if argument is given, it needs to be a valid template, and only
  463. # when given VM class do need a template
  464. if hasattr(vm_class, 'template'):
  465. assert self.arg in self.app.domains
  466. kwargs['template'] = self.app.domains[self.arg]
  467. else:
  468. assert not self.arg
  469. for untrusted_param in untrusted_payload.decode('ascii',
  470. errors='strict').split(' '):
  471. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  472. if untrusted_key in kwargs:
  473. raise qubes.api.ProtocolError('duplicated parameters')
  474. if untrusted_key == 'name':
  475. qubes.vm.validate_name(None, None, untrusted_value)
  476. kwargs['name'] = untrusted_value
  477. elif untrusted_key == 'label':
  478. # don't confuse label name with label index
  479. assert not untrusted_value.isdigit()
  480. allowed_chars = string.ascii_letters + string.digits + '-_.'
  481. assert all(c in allowed_chars for c in untrusted_value)
  482. try:
  483. kwargs['label'] = self.app.get_label(untrusted_value)
  484. except KeyError:
  485. raise qubes.exc.QubesValueError
  486. elif untrusted_key == 'pool' and allow_pool:
  487. if pool is not None:
  488. raise qubes.api.ProtocolError('duplicated pool parameter')
  489. pool = self.app.get_pool(untrusted_value)
  490. elif untrusted_key.startswith('pool:') and allow_pool:
  491. untrusted_volume = untrusted_key.split(':', 1)[1]
  492. # kind of ugly, but actual list of volumes is available only
  493. # after creating a VM
  494. assert untrusted_volume in ['root', 'private', 'volatile',
  495. 'kernel']
  496. volume = untrusted_volume
  497. if volume in pools:
  498. raise qubes.api.ProtocolError(
  499. 'duplicated pool:{} parameter'.format(volume))
  500. pools[volume] = self.app.get_pool(untrusted_value)
  501. else:
  502. raise qubes.api.ProtocolError('Invalid param name')
  503. del untrusted_payload
  504. if 'name' not in kwargs or 'label' not in kwargs:
  505. raise qubes.api.ProtocolError('Missing name or label')
  506. if pool and pools:
  507. raise qubes.api.ProtocolError(
  508. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  509. if kwargs['name'] in self.app.domains:
  510. raise qubes.exc.QubesValueError(
  511. 'VM {} already exists'.format(kwargs['name']))
  512. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  513. vm = self.app.add_new_vm(vm_class, **kwargs)
  514. try:
  515. yield from vm.create_on_disk(pool=pool, pools=pools)
  516. except:
  517. del self.app.domains[vm]
  518. raise
  519. self.app.save()
  520. @qubes.api.method('admin.vm.Clone')
  521. @asyncio.coroutine
  522. def vm_clone(self, untrusted_payload):
  523. assert not self.arg
  524. assert untrusted_payload.startswith(b'name=')
  525. untrusted_name = untrusted_payload[5:].decode('ascii')
  526. qubes.vm.validate_name(None, None, untrusted_name)
  527. new_name = untrusted_name
  528. del untrusted_payload
  529. if new_name in self.app.domains:
  530. raise qubes.exc.QubesValueError('Already exists')
  531. self.fire_event_for_permission(new_name=new_name)
  532. src_vm = self.dest
  533. dst_vm = self.app.add_new_vm(src_vm.__class__, name=new_name)
  534. try:
  535. dst_vm.clone_properties(src_vm)
  536. # TODO: tags
  537. # TODO: features
  538. # TODO: firewall
  539. # TODO: persistent devices
  540. yield from dst_vm.clone_disk_files(src_vm)
  541. except:
  542. del self.app.domains[dst_vm]
  543. raise
  544. self.app.save()