admin.py 25 KB

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