admin.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364
  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 itertools
  26. import os
  27. import string
  28. import subprocess
  29. import libvirt
  30. import pkg_resources
  31. import yaml
  32. import qubes.api
  33. import qubes.backup
  34. import qubes.config
  35. import qubes.devices
  36. import qubes.firewall
  37. import qubes.storage
  38. import qubes.utils
  39. import qubes.vm
  40. import qubes.vm.adminvm
  41. import qubes.vm.qubesvm
  42. class QubesMgmtEventsDispatcher(object):
  43. def __init__(self, filters, send_event):
  44. self.filters = filters
  45. self.send_event = send_event
  46. def vm_handler(self, subject, event, **kwargs):
  47. # do not send internal events
  48. if event.startswith('admin-permission:'):
  49. return
  50. if event.startswith('device-get:'):
  51. return
  52. if event.startswith('device-list:'):
  53. return
  54. if event.startswith('device-list-attached:'):
  55. return
  56. if event in ('domain-is-fully-usable',):
  57. return
  58. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  59. self.filters)):
  60. return
  61. self.send_event(subject, event, **kwargs)
  62. def app_handler(self, subject, event, **kwargs):
  63. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  64. self.filters)):
  65. return
  66. self.send_event(subject, event, **kwargs)
  67. def on_domain_add(self, subject, event, vm):
  68. # pylint: disable=unused-argument
  69. vm.add_handler('*', self.vm_handler)
  70. def on_domain_delete(self, subject, event, vm):
  71. # pylint: disable=unused-argument
  72. vm.remove_handler('*', self.vm_handler)
  73. class QubesAdminAPI(qubes.api.AbstractQubesAPI):
  74. '''Implementation of Qubes Management API calls
  75. This class contains all the methods available in the main API.
  76. .. seealso::
  77. https://www.qubes-os.org/doc/mgmt1/
  78. '''
  79. SOCKNAME = '/var/run/qubesd.sock'
  80. @qubes.api.method('admin.vmclass.List', no_payload=True,
  81. scope='global', read=True)
  82. @asyncio.coroutine
  83. def vmclass_list(self):
  84. '''List all VM classes'''
  85. assert not self.arg
  86. assert self.dest.name == 'dom0'
  87. entrypoints = self.fire_event_for_filter(
  88. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  89. return ''.join('{}\n'.format(ep.name)
  90. for ep in entrypoints)
  91. @qubes.api.method('admin.vm.List', no_payload=True,
  92. scope='global', read=True)
  93. @asyncio.coroutine
  94. def vm_list(self):
  95. '''List all the domains'''
  96. assert not self.arg
  97. if self.dest.name == 'dom0':
  98. domains = self.fire_event_for_filter(self.app.domains)
  99. else:
  100. domains = self.fire_event_for_filter([self.dest])
  101. return ''.join('{} class={} state={}\n'.format(
  102. vm.name,
  103. vm.__class__.__name__,
  104. vm.get_power_state())
  105. for vm in sorted(domains))
  106. @qubes.api.method('admin.vm.property.List', no_payload=True,
  107. scope='local', read=True)
  108. @asyncio.coroutine
  109. def vm_property_list(self):
  110. '''List all properties on a qube'''
  111. return self._property_list(self.dest)
  112. @qubes.api.method('admin.property.List', no_payload=True,
  113. scope='global', read=True)
  114. @asyncio.coroutine
  115. def property_list(self):
  116. '''List all global properties'''
  117. assert self.dest.name == 'dom0'
  118. return self._property_list(self.app)
  119. def _property_list(self, dest):
  120. assert not self.arg
  121. properties = self.fire_event_for_filter(dest.property_list())
  122. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  123. @qubes.api.method('admin.vm.property.Get', no_payload=True,
  124. scope='local', read=True)
  125. @asyncio.coroutine
  126. def vm_property_get(self):
  127. '''Get a value of one property'''
  128. return self._property_get(self.dest)
  129. @qubes.api.method('admin.property.Get', no_payload=True,
  130. scope='global', read=True)
  131. @asyncio.coroutine
  132. def property_get(self):
  133. '''Get a value of one global property'''
  134. assert self.dest.name == 'dom0'
  135. return self._property_get(self.app)
  136. def _property_get(self, dest):
  137. if self.arg not in dest.property_list():
  138. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  139. self.fire_event_for_permission()
  140. property_def = dest.property_get_def(self.arg)
  141. # explicit list to be sure that it matches protocol spec
  142. if isinstance(property_def, qubes.vm.VMProperty):
  143. property_type = 'vm'
  144. elif property_def.type is int:
  145. property_type = 'int'
  146. elif property_def.type is bool:
  147. property_type = 'bool'
  148. elif self.arg == 'label':
  149. property_type = 'label'
  150. else:
  151. property_type = 'str'
  152. try:
  153. value = getattr(dest, self.arg)
  154. except AttributeError:
  155. return 'default=True type={} '.format(property_type)
  156. else:
  157. return 'default={} type={} {}'.format(
  158. str(dest.property_is_default(self.arg)),
  159. property_type,
  160. str(value) if value is not None else '')
  161. @qubes.api.method('admin.vm.property.Set',
  162. scope='local', write=True)
  163. @asyncio.coroutine
  164. def vm_property_set(self, untrusted_payload):
  165. '''Set property value'''
  166. return self._property_set(self.dest,
  167. untrusted_payload=untrusted_payload)
  168. @qubes.api.method('admin.property.Set',
  169. scope='global', write=True)
  170. @asyncio.coroutine
  171. def property_set(self, untrusted_payload):
  172. '''Set property value'''
  173. assert self.dest.name == 'dom0'
  174. return self._property_set(self.app,
  175. untrusted_payload=untrusted_payload)
  176. def _property_set(self, dest, untrusted_payload):
  177. if self.arg not in dest.property_list():
  178. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  179. property_def = dest.property_get_def(self.arg)
  180. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  181. self.fire_event_for_permission(newvalue=newvalue)
  182. setattr(dest, self.arg, newvalue)
  183. self.app.save()
  184. @qubes.api.method('admin.vm.property.Help', no_payload=True,
  185. scope='local', read=True)
  186. @asyncio.coroutine
  187. def vm_property_help(self):
  188. '''Get help for one property'''
  189. return self._property_help(self.dest)
  190. @qubes.api.method('admin.property.Help', no_payload=True,
  191. scope='global', read=True)
  192. @asyncio.coroutine
  193. def property_help(self):
  194. '''Get help for one property'''
  195. assert self.dest.name == 'dom0'
  196. return self._property_help(self.app)
  197. def _property_help(self, dest):
  198. if self.arg not in dest.property_list():
  199. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  200. self.fire_event_for_permission()
  201. try:
  202. doc = dest.property_get_def(self.arg).__doc__
  203. except AttributeError:
  204. return ''
  205. return qubes.utils.format_doc(doc)
  206. @qubes.api.method('admin.vm.property.Reset', no_payload=True,
  207. scope='local', write=True)
  208. @asyncio.coroutine
  209. def vm_property_reset(self):
  210. '''Reset a property to a default value'''
  211. return self._property_reset(self.dest)
  212. @qubes.api.method('admin.property.Reset', no_payload=True,
  213. scope='global', write=True)
  214. @asyncio.coroutine
  215. def property_reset(self):
  216. '''Reset a property to a default value'''
  217. assert self.dest.name == 'dom0'
  218. return self._property_reset(self.app)
  219. def _property_reset(self, dest):
  220. if self.arg not in dest.property_list():
  221. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  222. self.fire_event_for_permission()
  223. delattr(dest, self.arg)
  224. self.app.save()
  225. @qubes.api.method('admin.vm.volume.List', no_payload=True,
  226. scope='local', read=True)
  227. @asyncio.coroutine
  228. def vm_volume_list(self):
  229. assert not self.arg
  230. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  231. return ''.join('{}\n'.format(name) for name in volume_names)
  232. @qubes.api.method('admin.vm.volume.Info', no_payload=True,
  233. scope='local', read=True)
  234. @asyncio.coroutine
  235. def vm_volume_info(self):
  236. assert self.arg in self.dest.volumes.keys()
  237. self.fire_event_for_permission()
  238. volume = self.dest.volumes[self.arg]
  239. # properties defined in API
  240. volume_properties = [
  241. 'pool', 'vid', 'size', 'usage', 'rw', 'source',
  242. 'save_on_stop', 'snap_on_start']
  243. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  244. volume_properties)
  245. @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True,
  246. scope='local', read=True)
  247. @asyncio.coroutine
  248. def vm_volume_listsnapshots(self):
  249. assert self.arg in self.dest.volumes.keys()
  250. volume = self.dest.volumes[self.arg]
  251. revisions = [revision for revision in volume.revisions]
  252. revisions = self.fire_event_for_filter(revisions)
  253. return ''.join('{}\n'.format(revision) for revision in revisions)
  254. @qubes.api.method('admin.vm.volume.Revert',
  255. scope='local', write=True)
  256. @asyncio.coroutine
  257. def vm_volume_revert(self, untrusted_payload):
  258. assert self.arg in self.dest.volumes.keys()
  259. untrusted_revision = untrusted_payload.decode('ascii').strip()
  260. del untrusted_payload
  261. volume = self.dest.volumes[self.arg]
  262. snapshots = volume.revisions
  263. assert untrusted_revision in snapshots
  264. revision = untrusted_revision
  265. self.fire_event_for_permission(volume=volume, revision=revision)
  266. ret = volume.revert(revision)
  267. if asyncio.iscoroutine(ret):
  268. yield from ret
  269. self.app.save()
  270. # write=True because this allow to clone VM - and most likely modify that
  271. # one - still having the same data
  272. @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True,
  273. scope='local', write=True)
  274. @asyncio.coroutine
  275. def vm_volume_clone_from(self):
  276. assert self.arg in self.dest.volumes.keys()
  277. volume = self.dest.volumes[self.arg]
  278. self.fire_event_for_permission(volume=volume)
  279. token = qubes.utils.random_string(32)
  280. # save token on self.app, as self is not persistent
  281. if not hasattr(self.app, 'api_admin_pending_clone'):
  282. self.app.api_admin_pending_clone = {}
  283. # don't handle collisions any better - if someone is so much out of
  284. # luck, can try again anyway
  285. assert token not in self.app.api_admin_pending_clone
  286. self.app.api_admin_pending_clone[token] = volume
  287. return token
  288. @qubes.api.method('admin.vm.volume.CloneTo',
  289. scope='local', write=True)
  290. @asyncio.coroutine
  291. def vm_volume_clone_to(self, untrusted_payload):
  292. assert self.arg in self.dest.volumes.keys()
  293. untrusted_token = untrusted_payload.decode('ascii').strip()
  294. del untrusted_payload
  295. assert untrusted_token in getattr(self.app,
  296. 'api_admin_pending_clone', {})
  297. token = untrusted_token
  298. del untrusted_token
  299. src_volume = self.app.api_admin_pending_clone[token]
  300. del self.app.api_admin_pending_clone[token]
  301. # make sure the volume still exists, but invalidate token anyway
  302. assert str(src_volume.pool) in self.app.pools
  303. assert src_volume in self.app.pools[str(src_volume.pool)].volumes
  304. dst_volume = self.dest.volumes[self.arg]
  305. self.fire_event_for_permission(src_volume=src_volume,
  306. dst_volume=dst_volume)
  307. op_retval = dst_volume.import_volume(src_volume)
  308. # clone/import functions may be either synchronous or asynchronous
  309. # in the later case, we need to wait for them to finish
  310. if asyncio.iscoroutine(op_retval):
  311. op_retval = yield from op_retval
  312. self.dest.volumes[self.arg] = op_retval
  313. self.app.save()
  314. @qubes.api.method('admin.vm.volume.Resize',
  315. scope='local', write=True)
  316. @asyncio.coroutine
  317. def vm_volume_resize(self, untrusted_payload):
  318. assert self.arg in self.dest.volumes.keys()
  319. untrusted_size = untrusted_payload.decode('ascii').strip()
  320. del untrusted_payload
  321. assert untrusted_size.isdigit() # only digits, forbid '-' too
  322. assert len(untrusted_size) <= 20 # limit to about 2^64
  323. size = int(untrusted_size)
  324. self.fire_event_for_permission(size=size)
  325. yield from self.dest.storage.resize(self.arg, size)
  326. self.app.save()
  327. @qubes.api.method('admin.vm.volume.Import', no_payload=True,
  328. scope='local', write=True)
  329. @asyncio.coroutine
  330. def vm_volume_import(self):
  331. '''Import volume data.
  332. Note that this function only returns a path to where data should be
  333. written, actual importing is done by a script in /etc/qubes-rpc
  334. When the script finish importing, it will trigger
  335. internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
  336. payload) and response from that call will be actually send to the
  337. caller.
  338. '''
  339. assert self.arg in self.dest.volumes.keys()
  340. self.fire_event_for_permission()
  341. if not self.dest.is_halted():
  342. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  343. path = self.dest.storage.import_data(self.arg)
  344. assert ' ' not in path
  345. size = self.dest.volumes[self.arg].size
  346. # when we know the action is allowed, inform extensions that it will
  347. # be performed
  348. self.dest.fire_event('domain-volume-import-begin', volume=self.arg)
  349. return '{} {}'.format(size, path)
  350. @qubes.api.method('admin.vm.tag.List', no_payload=True,
  351. scope='local', read=True)
  352. @asyncio.coroutine
  353. def vm_tag_list(self):
  354. assert not self.arg
  355. tags = self.dest.tags
  356. tags = self.fire_event_for_filter(tags)
  357. return ''.join('{}\n'.format(tag) for tag in sorted(tags))
  358. @qubes.api.method('admin.vm.tag.Get', no_payload=True,
  359. scope='local', read=True)
  360. @asyncio.coroutine
  361. def vm_tag_get(self):
  362. qubes.vm.Tags.validate_tag(self.arg)
  363. self.fire_event_for_permission()
  364. return '1' if self.arg in self.dest.tags else '0'
  365. @qubes.api.method('admin.vm.tag.Set', no_payload=True,
  366. scope='local', write=True)
  367. @asyncio.coroutine
  368. def vm_tag_set(self):
  369. qubes.vm.Tags.validate_tag(self.arg)
  370. self.fire_event_for_permission()
  371. self.dest.tags.add(self.arg)
  372. self.app.save()
  373. @qubes.api.method('admin.vm.tag.Remove', no_payload=True,
  374. scope='local', write=True)
  375. @asyncio.coroutine
  376. def vm_tag_remove(self):
  377. qubes.vm.Tags.validate_tag(self.arg)
  378. self.fire_event_for_permission()
  379. try:
  380. self.dest.tags.remove(self.arg)
  381. except KeyError:
  382. raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
  383. self.app.save()
  384. @qubes.api.method('admin.pool.List', no_payload=True,
  385. scope='global', read=True)
  386. @asyncio.coroutine
  387. def pool_list(self):
  388. assert not self.arg
  389. assert self.dest.name == 'dom0'
  390. pools = self.fire_event_for_filter(self.app.pools)
  391. return ''.join('{}\n'.format(pool) for pool in pools)
  392. @qubes.api.method('admin.pool.ListDrivers', no_payload=True,
  393. scope='global', read=True)
  394. @asyncio.coroutine
  395. def pool_listdrivers(self):
  396. assert self.dest.name == 'dom0'
  397. assert not self.arg
  398. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  399. return ''.join('{} {}\n'.format(
  400. driver,
  401. ' '.join(qubes.storage.driver_parameters(driver)))
  402. for driver in drivers)
  403. @qubes.api.method('admin.pool.Info', no_payload=True,
  404. scope='global', read=True)
  405. @asyncio.coroutine
  406. def pool_info(self):
  407. assert self.dest.name == 'dom0'
  408. assert self.arg in self.app.pools.keys()
  409. pool = self.app.pools[self.arg]
  410. self.fire_event_for_permission(pool=pool)
  411. return ''.join('{}={}\n'.format(prop, val)
  412. for prop, val in sorted(pool.config.items()))
  413. @qubes.api.method('admin.pool.Add',
  414. scope='global', write=True)
  415. @asyncio.coroutine
  416. def pool_add(self, untrusted_payload):
  417. assert self.dest.name == 'dom0'
  418. drivers = qubes.storage.pool_drivers()
  419. assert self.arg in drivers
  420. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  421. del untrusted_payload
  422. assert all(('=' in line) for line in untrusted_pool_config)
  423. # pairs of (option, value)
  424. untrusted_pool_config = [line.split('=', 1)
  425. for line in untrusted_pool_config]
  426. # reject duplicated options
  427. assert len(set(x[0] for x in untrusted_pool_config)) == \
  428. len([x[0] for x in untrusted_pool_config])
  429. # and convert to dict
  430. untrusted_pool_config = dict(untrusted_pool_config)
  431. assert 'name' in untrusted_pool_config
  432. untrusted_pool_name = untrusted_pool_config.pop('name')
  433. allowed_chars = string.ascii_letters + string.digits + '-_.'
  434. assert all(c in allowed_chars for c in untrusted_pool_name)
  435. pool_name = untrusted_pool_name
  436. assert pool_name not in self.app.pools
  437. driver_parameters = qubes.storage.driver_parameters(self.arg)
  438. assert all(key in driver_parameters for key in untrusted_pool_config)
  439. pool_config = untrusted_pool_config
  440. self.fire_event_for_permission(name=pool_name,
  441. pool_config=pool_config)
  442. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  443. self.app.save()
  444. @qubes.api.method('admin.pool.Remove', no_payload=True,
  445. scope='global', write=True)
  446. @asyncio.coroutine
  447. def pool_remove(self):
  448. assert self.dest.name == 'dom0'
  449. assert self.arg in self.app.pools.keys()
  450. self.fire_event_for_permission()
  451. self.app.remove_pool(self.arg)
  452. self.app.save()
  453. @qubes.api.method('admin.label.List', no_payload=True,
  454. scope='global', read=True)
  455. @asyncio.coroutine
  456. def label_list(self):
  457. assert self.dest.name == 'dom0'
  458. assert not self.arg
  459. labels = self.fire_event_for_filter(self.app.labels.values())
  460. return ''.join('{}\n'.format(label.name) for label in labels)
  461. @qubes.api.method('admin.label.Get', no_payload=True,
  462. scope='global', read=True)
  463. @asyncio.coroutine
  464. def label_get(self):
  465. assert self.dest.name == 'dom0'
  466. try:
  467. label = self.app.get_label(self.arg)
  468. except KeyError:
  469. raise qubes.exc.QubesValueError
  470. self.fire_event_for_permission(label=label)
  471. return label.color
  472. @qubes.api.method('admin.label.Index', no_payload=True,
  473. scope='global', read=True)
  474. @asyncio.coroutine
  475. def label_index(self):
  476. assert self.dest.name == 'dom0'
  477. try:
  478. label = self.app.get_label(self.arg)
  479. except KeyError:
  480. raise qubes.exc.QubesValueError
  481. self.fire_event_for_permission(label=label)
  482. return str(label.index)
  483. @qubes.api.method('admin.label.Create',
  484. scope='global', write=True)
  485. @asyncio.coroutine
  486. def label_create(self, untrusted_payload):
  487. assert self.dest.name == 'dom0'
  488. # don't confuse label name with label index
  489. assert not self.arg.isdigit()
  490. allowed_chars = string.ascii_letters + string.digits + '-_.'
  491. assert all(c in allowed_chars for c in self.arg)
  492. try:
  493. self.app.get_label(self.arg)
  494. except KeyError:
  495. # ok, no such label yet
  496. pass
  497. else:
  498. raise qubes.exc.QubesValueError('label already exists')
  499. untrusted_payload = untrusted_payload.decode('ascii').strip()
  500. assert len(untrusted_payload) == 8
  501. assert untrusted_payload.startswith('0x')
  502. # besides prefix, only hex digits are allowed
  503. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  504. # SEE: #2732
  505. color = untrusted_payload
  506. self.fire_event_for_permission(color=color)
  507. # allocate new index, but make sure it's outside of default labels set
  508. new_index = max(
  509. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  510. label = qubes.Label(new_index, color, self.arg)
  511. self.app.labels[new_index] = label
  512. self.app.save()
  513. @qubes.api.method('admin.label.Remove', no_payload=True,
  514. scope='global', write=True)
  515. @asyncio.coroutine
  516. def label_remove(self):
  517. assert self.dest.name == 'dom0'
  518. try:
  519. label = self.app.get_label(self.arg)
  520. except KeyError:
  521. raise qubes.exc.QubesValueError
  522. # don't allow removing default labels
  523. assert label.index > qubes.config.max_default_label
  524. # FIXME: this should be in app.add_label()
  525. for vm in self.app.domains:
  526. if vm.label == label:
  527. raise qubes.exc.QubesException('label still in use')
  528. self.fire_event_for_permission(label=label)
  529. del self.app.labels[label.index]
  530. self.app.save()
  531. @qubes.api.method('admin.vm.Start', no_payload=True,
  532. scope='local', execute=True)
  533. @asyncio.coroutine
  534. def vm_start(self):
  535. assert not self.arg
  536. self.fire_event_for_permission()
  537. try:
  538. yield from self.dest.start()
  539. except libvirt.libvirtError as e:
  540. # change to QubesException, so will be reported to the user
  541. raise qubes.exc.QubesException('Start failed: ' + str(e))
  542. @qubes.api.method('admin.vm.Shutdown', no_payload=True,
  543. scope='local', execute=True)
  544. @asyncio.coroutine
  545. def vm_shutdown(self):
  546. assert not self.arg
  547. self.fire_event_for_permission()
  548. yield from self.dest.shutdown()
  549. @qubes.api.method('admin.vm.Pause', no_payload=True,
  550. scope='local', execute=True)
  551. @asyncio.coroutine
  552. def vm_pause(self):
  553. assert not self.arg
  554. self.fire_event_for_permission()
  555. yield from self.dest.pause()
  556. @qubes.api.method('admin.vm.Unpause', no_payload=True,
  557. scope='local', execute=True)
  558. @asyncio.coroutine
  559. def vm_unpause(self):
  560. assert not self.arg
  561. self.fire_event_for_permission()
  562. yield from self.dest.unpause()
  563. @qubes.api.method('admin.vm.Kill', no_payload=True,
  564. scope='local', execute=True)
  565. @asyncio.coroutine
  566. def vm_kill(self):
  567. assert not self.arg
  568. self.fire_event_for_permission()
  569. yield from self.dest.kill()
  570. @qubes.api.method('admin.Events', no_payload=True,
  571. scope='global', read=True)
  572. @asyncio.coroutine
  573. def events(self):
  574. assert not self.arg
  575. # run until client connection is terminated
  576. self.cancellable = True
  577. wait_for_cancel = asyncio.get_event_loop().create_future()
  578. # cache event filters, to not call an event each time an event arrives
  579. event_filters = self.fire_event_for_permission()
  580. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  581. if self.dest.name == 'dom0':
  582. self.app.add_handler('*', dispatcher.app_handler)
  583. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  584. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  585. for vm in self.app.domains:
  586. vm.add_handler('*', dispatcher.vm_handler)
  587. else:
  588. self.dest.add_handler('*', dispatcher.vm_handler)
  589. # send artificial event as a confirmation that connection is established
  590. self.send_event(self.app, 'connection-established')
  591. try:
  592. yield from wait_for_cancel
  593. except asyncio.CancelledError:
  594. # the above waiting was already interrupted, this is all we need
  595. pass
  596. if self.dest.name == 'dom0':
  597. self.app.remove_handler('*', dispatcher.app_handler)
  598. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  599. self.app.remove_handler('domain-delete',
  600. dispatcher.on_domain_delete)
  601. for vm in self.app.domains:
  602. vm.remove_handler('*', dispatcher.vm_handler)
  603. else:
  604. self.dest.remove_handler('*', dispatcher.vm_handler)
  605. @qubes.api.method('admin.vm.feature.List', no_payload=True,
  606. scope='local', read=True)
  607. @asyncio.coroutine
  608. def vm_feature_list(self):
  609. assert not self.arg
  610. features = self.fire_event_for_filter(self.dest.features.keys())
  611. return ''.join('{}\n'.format(feature) for feature in features)
  612. @qubes.api.method('admin.vm.feature.Get', no_payload=True,
  613. scope='local', read=True)
  614. @asyncio.coroutine
  615. def vm_feature_get(self):
  616. # validation of self.arg done by qrexec-policy is enough
  617. self.fire_event_for_permission()
  618. try:
  619. value = self.dest.features[self.arg]
  620. except KeyError:
  621. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  622. return value
  623. @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True,
  624. scope='local', read=True)
  625. @asyncio.coroutine
  626. def vm_feature_checkwithtemplate(self):
  627. # validation of self.arg done by qrexec-policy is enough
  628. self.fire_event_for_permission()
  629. try:
  630. value = self.dest.features.check_with_template(self.arg)
  631. except KeyError:
  632. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  633. return value
  634. @qubes.api.method('admin.vm.feature.Remove', no_payload=True,
  635. scope='local', write=True)
  636. @asyncio.coroutine
  637. def vm_feature_remove(self):
  638. # validation of self.arg done by qrexec-policy is enough
  639. self.fire_event_for_permission()
  640. try:
  641. del self.dest.features[self.arg]
  642. except KeyError:
  643. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  644. self.app.save()
  645. @qubes.api.method('admin.vm.feature.Set',
  646. scope='local', write=True)
  647. @asyncio.coroutine
  648. def vm_feature_set(self, untrusted_payload):
  649. # validation of self.arg done by qrexec-policy is enough
  650. value = untrusted_payload.decode('ascii', errors='strict')
  651. del untrusted_payload
  652. self.fire_event_for_permission(value=value)
  653. self.dest.features[self.arg] = value
  654. self.app.save()
  655. @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
  656. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  657. scope='global', write=True)
  658. @asyncio.coroutine
  659. def vm_create(self, endpoint, untrusted_payload=None):
  660. return self._vm_create(endpoint, allow_pool=False,
  661. untrusted_payload=untrusted_payload)
  662. @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  663. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  664. scope='global', write=True)
  665. @asyncio.coroutine
  666. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  667. return self._vm_create(endpoint, allow_pool=True,
  668. untrusted_payload=untrusted_payload)
  669. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  670. assert self.dest.name == 'dom0'
  671. kwargs = {}
  672. pool = None
  673. pools = {}
  674. # this will raise exception if none is found
  675. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  676. vm_type)
  677. # if argument is given, it needs to be a valid template, and only
  678. # when given VM class do need a template
  679. if hasattr(vm_class, 'template'):
  680. if self.arg:
  681. assert self.arg in self.app.domains
  682. kwargs['template'] = self.app.domains[self.arg]
  683. else:
  684. assert not self.arg
  685. for untrusted_param in untrusted_payload.decode('ascii',
  686. errors='strict').split(' '):
  687. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  688. if untrusted_key in kwargs:
  689. raise qubes.api.ProtocolError('duplicated parameters')
  690. if untrusted_key == 'name':
  691. qubes.vm.validate_name(None, None, untrusted_value)
  692. kwargs['name'] = untrusted_value
  693. elif untrusted_key == 'label':
  694. # don't confuse label name with label index
  695. assert not untrusted_value.isdigit()
  696. allowed_chars = string.ascii_letters + string.digits + '-_.'
  697. assert all(c in allowed_chars for c in untrusted_value)
  698. try:
  699. kwargs['label'] = self.app.get_label(untrusted_value)
  700. except KeyError:
  701. raise qubes.exc.QubesValueError
  702. elif untrusted_key == 'pool' and allow_pool:
  703. if pool is not None:
  704. raise qubes.api.ProtocolError('duplicated pool parameter')
  705. pool = self.app.get_pool(untrusted_value)
  706. elif untrusted_key.startswith('pool:') and allow_pool:
  707. untrusted_volume = untrusted_key.split(':', 1)[1]
  708. # kind of ugly, but actual list of volumes is available only
  709. # after creating a VM
  710. assert untrusted_volume in ['root', 'private', 'volatile',
  711. 'kernel']
  712. volume = untrusted_volume
  713. if volume in pools:
  714. raise qubes.api.ProtocolError(
  715. 'duplicated pool:{} parameter'.format(volume))
  716. pools[volume] = self.app.get_pool(untrusted_value)
  717. else:
  718. raise qubes.api.ProtocolError('Invalid param name')
  719. del untrusted_payload
  720. if 'name' not in kwargs or 'label' not in kwargs:
  721. raise qubes.api.ProtocolError('Missing name or label')
  722. if pool and pools:
  723. raise qubes.api.ProtocolError(
  724. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  725. if kwargs['name'] in self.app.domains:
  726. raise qubes.exc.QubesValueError(
  727. 'VM {} already exists'.format(kwargs['name']))
  728. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  729. vm = self.app.add_new_vm(vm_class, **kwargs)
  730. # TODO: move this to extension (in race-free fashion)
  731. vm.tags.add('created-by-' + str(self.src))
  732. try:
  733. yield from vm.create_on_disk(pool=pool, pools=pools)
  734. except:
  735. del self.app.domains[vm]
  736. raise
  737. self.app.save()
  738. @qubes.api.method('admin.vm.CreateDisposable', no_payload=True,
  739. scope='global', write=True)
  740. @asyncio.coroutine
  741. def create_disposable(self):
  742. assert not self.arg
  743. if self.dest.name == 'dom0':
  744. dispvm_template = self.src.default_dispvm
  745. else:
  746. dispvm_template = self.dest
  747. self.fire_event_for_permission(dispvm_template=dispvm_template)
  748. dispvm = yield from qubes.vm.dispvm.DispVM.from_appvm(dispvm_template)
  749. # TODO: move this to extension (in race-free fashion, better than here)
  750. dispvm.tags.add('disp-created-by-' + str(self.src))
  751. return dispvm.name
  752. @qubes.api.method('admin.vm.Remove', no_payload=True,
  753. scope='global', write=True)
  754. @asyncio.coroutine
  755. def vm_remove(self):
  756. assert not self.arg
  757. self.fire_event_for_permission()
  758. if not self.dest.is_halted():
  759. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  760. del self.app.domains[self.dest]
  761. try:
  762. yield from self.dest.remove_from_disk()
  763. except: # pylint: disable=bare-except
  764. self.app.log.exception('Error wile removing VM \'%s\' files',
  765. self.dest.name)
  766. self.app.save()
  767. @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
  768. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  769. no_payload=True,
  770. scope='local', read=True)
  771. @asyncio.coroutine
  772. def vm_device_available(self, endpoint):
  773. devclass = endpoint
  774. devices = self.dest.devices[devclass].available()
  775. if self.arg:
  776. devices = [dev for dev in devices if dev.ident == self.arg]
  777. # no duplicated devices, but device may not exists, in which case
  778. # the list is empty
  779. assert len(devices) <= 1
  780. devices = self.fire_event_for_filter(devices, devclass=devclass)
  781. dev_info = {}
  782. for dev in devices:
  783. non_default_attrs = set(attr for attr in dir(dev) if
  784. not attr.startswith('_')).difference((
  785. 'backend_domain', 'ident', 'frontend_domain',
  786. 'description', 'options'))
  787. properties_txt = ' '.join(
  788. '{}={!s}'.format(prop, value) for prop, value
  789. in itertools.chain(
  790. ((key, getattr(dev, key)) for key in non_default_attrs),
  791. # keep description as the last one, according to API
  792. # specification
  793. (('description', dev.description),)
  794. ))
  795. assert '\n' not in properties_txt
  796. dev_info[dev.ident] = properties_txt
  797. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  798. for ident in sorted(dev_info))
  799. @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
  800. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  801. no_payload=True,
  802. scope='local', read=True)
  803. @asyncio.coroutine
  804. def vm_device_list(self, endpoint):
  805. devclass = endpoint
  806. device_assignments = self.dest.devices[devclass].assignments()
  807. if self.arg:
  808. select_backend, select_ident = self.arg.split('+', 1)
  809. device_assignments = [dev for dev in device_assignments
  810. if (str(dev.backend_domain), dev.ident)
  811. == (select_backend, select_ident)]
  812. # no duplicated devices, but device may not exists, in which case
  813. # the list is empty
  814. assert len(device_assignments) <= 1
  815. device_assignments = self.fire_event_for_filter(device_assignments,
  816. devclass=devclass)
  817. dev_info = {}
  818. for dev in device_assignments:
  819. properties_txt = ' '.join(
  820. '{}={!s}'.format(opt, value) for opt, value
  821. in itertools.chain(
  822. dev.options.items(),
  823. (('persistent', 'yes' if dev.persistent else 'no'),)
  824. ))
  825. assert '\n' not in properties_txt
  826. ident = '{!s}+{!s}'.format(dev.backend_domain, dev.ident)
  827. dev_info[ident] = properties_txt
  828. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  829. for ident in sorted(dev_info))
  830. # Attach/Detach action can both modify persistent state (with
  831. # persistent=True) and volatile state of running VM (with persistent=False).
  832. # For this reason, write=True + execute=True
  833. @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
  834. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  835. scope='local', write=True, execute=True)
  836. @asyncio.coroutine
  837. def vm_device_attach(self, endpoint, untrusted_payload):
  838. devclass = endpoint
  839. options = {}
  840. persistent = False
  841. for untrusted_option in untrusted_payload.decode('ascii').split():
  842. try:
  843. untrusted_key, untrusted_value = untrusted_option.split('=', 1)
  844. except ValueError:
  845. raise qubes.api.ProtocolError('Invalid options format')
  846. if untrusted_key == 'persistent':
  847. persistent = qubes.property.bool(None, None, untrusted_value)
  848. else:
  849. allowed_chars_key = string.digits + string.ascii_letters + '-_.'
  850. allowed_chars_value = allowed_chars_key + ',+:'
  851. if any(x not in allowed_chars_key for x in untrusted_key):
  852. raise qubes.api.ProtocolError(
  853. 'Invalid chars in option name')
  854. if any(x not in allowed_chars_value for x in untrusted_value):
  855. raise qubes.api.ProtocolError(
  856. 'Invalid chars in option value')
  857. options[untrusted_key] = untrusted_value
  858. # qrexec already verified that no strange characters are in self.arg
  859. backend_domain, ident = self.arg.split('+', 1)
  860. # may raise KeyError, either on domain or ident
  861. dev = self.app.domains[backend_domain].devices[devclass][ident]
  862. self.fire_event_for_permission(device=dev,
  863. devclass=devclass, persistent=persistent,
  864. options=options)
  865. assignment = qubes.devices.DeviceAssignment(
  866. dev.backend_domain, dev.ident,
  867. options=options, persistent=persistent)
  868. yield from self.dest.devices[devclass].attach(assignment)
  869. self.app.save()
  870. # Attach/Detach action can both modify persistent state (with
  871. # persistent=True) and volatile state of running VM (with persistent=False).
  872. # For this reason, write=True + execute=True
  873. @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
  874. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  875. no_payload=True,
  876. scope='local', write=True, execute=True)
  877. @asyncio.coroutine
  878. def vm_device_detach(self, endpoint):
  879. devclass = endpoint
  880. # qrexec already verified that no strange characters are in self.arg
  881. backend_domain, ident = self.arg.split('+', 1)
  882. # may raise KeyError; if device isn't found, it will be UnknownDevice
  883. # instance - but allow it, otherwise it will be impossible to detach
  884. # already removed device
  885. dev = self.app.domains[backend_domain].devices[devclass][ident]
  886. self.fire_event_for_permission(device=dev,
  887. devclass=devclass)
  888. assignment = qubes.devices.DeviceAssignment(
  889. dev.backend_domain, dev.ident)
  890. yield from self.dest.devices[devclass].detach(assignment)
  891. self.app.save()
  892. @qubes.api.method('admin.vm.firewall.Get', no_payload=True,
  893. scope='local', read=True)
  894. @asyncio.coroutine
  895. def vm_firewall_get(self):
  896. assert not self.arg
  897. self.fire_event_for_permission()
  898. return ''.join('{}\n'.format(rule.api_rule)
  899. for rule in self.dest.firewall.rules)
  900. @qubes.api.method('admin.vm.firewall.Set',
  901. scope='local', write=True)
  902. @asyncio.coroutine
  903. def vm_firewall_set(self, untrusted_payload):
  904. assert not self.arg
  905. rules = []
  906. for untrusted_line in untrusted_payload.decode('ascii',
  907. errors='strict').splitlines():
  908. rule = qubes.firewall.Rule.from_api_string(
  909. untrusted_rule=untrusted_line)
  910. rules.append(rule)
  911. self.fire_event_for_permission(rules=rules)
  912. self.dest.firewall.rules = rules
  913. self.dest.firewall.save()
  914. @qubes.api.method('admin.vm.firewall.Reload', no_payload=True,
  915. scope='local', execute=True)
  916. @asyncio.coroutine
  917. def vm_firewall_reload(self):
  918. assert not self.arg
  919. self.fire_event_for_permission()
  920. self.dest.fire_event('firewall-changed')
  921. @asyncio.coroutine
  922. def _load_backup_profile(self, profile_name, skip_passphrase=False):
  923. '''Load backup profile and return :py:class:`qubes.backup.Backup`
  924. instance
  925. :param profile_name: name of the profile
  926. :param skip_passphrase: do not load passphrase - only backup summary
  927. can be retrieved when this option is in use
  928. '''
  929. profile_path = os.path.join(
  930. qubes.config.backup_profile_dir, profile_name + '.conf')
  931. with open(profile_path) as profile_file:
  932. profile_data = yaml.safe_load(profile_file)
  933. try:
  934. dest_vm = profile_data['destination_vm']
  935. dest_path = profile_data['destination_path']
  936. include_vms = profile_data['include']
  937. exclude_vms = profile_data.get('exclude', [])
  938. compression = profile_data.get('compression', True)
  939. except KeyError as err:
  940. raise qubes.exc.QubesException(
  941. 'Invalid backup profile - missing {}'.format(err))
  942. try:
  943. dest_vm = self.app.domains[dest_vm]
  944. except KeyError:
  945. raise qubes.exc.QubesException(
  946. 'Invalid destination_vm specified in backup profile')
  947. if isinstance(dest_vm, qubes.vm.adminvm.AdminVM):
  948. dest_vm = None
  949. if skip_passphrase:
  950. passphrase = None
  951. elif 'passphrase_text' in profile_data:
  952. passphrase = profile_data['passphrase_text']
  953. elif 'passphrase_vm' in profile_data:
  954. passphrase_vm_name = profile_data['passphrase_vm']
  955. try:
  956. passphrase_vm = self.app.domains[passphrase_vm_name]
  957. except KeyError:
  958. raise qubes.exc.QubesException(
  959. 'Invalid backup profile - invalid passphrase_vm')
  960. try:
  961. passphrase, _ = yield from passphrase_vm.run_service_for_stdio(
  962. 'qubes.BackupPassphrase+' + self.arg)
  963. # make it foolproof against "echo passphrase" implementation
  964. passphrase = passphrase.strip()
  965. assert b'\n' not in passphrase
  966. except subprocess.CalledProcessError:
  967. raise qubes.exc.QubesException(
  968. 'Failed to retrieve passphrase from \'{}\' VM'.format(
  969. passphrase_vm_name))
  970. else:
  971. raise qubes.exc.QubesException(
  972. 'Invalid backup profile - you need to '
  973. 'specify passphrase_text or passphrase_vm')
  974. # handle include
  975. vms_to_backup = set(vm for vm in self.app.domains
  976. if any(qubes.utils.match_vm_name_with_special(vm, name)
  977. for name in include_vms))
  978. # handle exclude
  979. vms_to_backup.difference_update(vm for vm in self.app.domains
  980. if any(qubes.utils.match_vm_name_with_special(vm, name)
  981. for name in exclude_vms))
  982. kwargs = {
  983. 'target_vm': dest_vm,
  984. 'target_dir': dest_path,
  985. 'compressed': bool(compression),
  986. 'passphrase': passphrase,
  987. }
  988. if isinstance(compression, str):
  989. kwargs['compression_filter'] = compression
  990. backup = qubes.backup.Backup(self.app, vms_to_backup, **kwargs)
  991. return backup
  992. def _backup_progress_callback(self, profile_name, progress):
  993. self.app.fire_event('backup-progress', backup_profile=profile_name,
  994. progress=progress)
  995. @qubes.api.method('admin.backup.Execute', no_payload=True,
  996. scope='global', read=True, execute=True)
  997. @asyncio.coroutine
  998. def backup_execute(self):
  999. assert self.dest.name == 'dom0'
  1000. assert self.arg
  1001. assert '/' not in self.arg
  1002. self.fire_event_for_permission()
  1003. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1004. self.arg + '.conf')
  1005. if not os.path.exists(profile_path):
  1006. raise qubes.api.PermissionDenied(
  1007. 'Backup profile {} does not exist'.format(self.arg))
  1008. if not hasattr(self.app, 'api_admin_running_backups'):
  1009. self.app.api_admin_running_backups = {}
  1010. backup = yield from self._load_backup_profile(self.arg)
  1011. backup.progress_callback = functools.partial(
  1012. self._backup_progress_callback, self.arg)
  1013. # forbid running the same backup operation twice at the time
  1014. assert self.arg not in self.app.api_admin_running_backups
  1015. backup_task = asyncio.ensure_future(backup.backup_do())
  1016. self.app.api_admin_running_backups[self.arg] = backup_task
  1017. try:
  1018. yield from backup_task
  1019. except asyncio.CancelledError:
  1020. raise qubes.exc.QubesException('Backup cancelled')
  1021. finally:
  1022. del self.app.api_admin_running_backups[self.arg]
  1023. @qubes.api.method('admin.backup.Cancel', no_payload=True,
  1024. scope='global', execute=True)
  1025. @asyncio.coroutine
  1026. def backup_cancel(self):
  1027. assert self.dest.name == 'dom0'
  1028. assert self.arg
  1029. assert '/' not in self.arg
  1030. self.fire_event_for_permission()
  1031. if not hasattr(self.app, 'api_admin_running_backups'):
  1032. self.app.api_admin_running_backups = {}
  1033. if self.arg not in self.app.api_admin_running_backups:
  1034. raise qubes.exc.QubesException('Backup operation not running')
  1035. self.app.api_admin_running_backups[self.arg].cancel()
  1036. @qubes.api.method('admin.backup.Info', no_payload=True,
  1037. scope='local', read=True)
  1038. @asyncio.coroutine
  1039. def backup_info(self):
  1040. assert self.dest.name == 'dom0'
  1041. assert self.arg
  1042. assert '/' not in self.arg
  1043. self.fire_event_for_permission()
  1044. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1045. self.arg + '.conf')
  1046. if not os.path.exists(profile_path):
  1047. raise qubes.api.PermissionDenied(
  1048. 'Backup profile {} does not exist'.format(self.arg))
  1049. backup = yield from self._load_backup_profile(self.arg,
  1050. skip_passphrase=True)
  1051. return backup.get_backup_summary()
  1052. def _send_stats_single(self, info_time, info, only_vm, filters,
  1053. id_to_name_map):
  1054. '''A single iteration of sending VM stats
  1055. :param info_time: time of previous iteration
  1056. :param info: information retrieved in previous iteration
  1057. :param only_vm: send information only about this VM
  1058. :param filters: filters to apply on stats before sending
  1059. :param id_to_name_map: ID->VM name map, may be modified
  1060. :return: tuple(info_time, info) - new information (to be passed to
  1061. the next iteration)
  1062. '''
  1063. (info_time, info) = self.app.host.get_vm_stats(info_time, info,
  1064. only_vm=only_vm)
  1065. for vm_id, vm_info in info.items():
  1066. if vm_id not in id_to_name_map:
  1067. try:
  1068. name = \
  1069. self.app.vmm.libvirt_conn.lookupByID(vm_id).name()
  1070. except libvirt.libvirtError as err:
  1071. if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
  1072. # stubdomain or so
  1073. name = None
  1074. else:
  1075. raise
  1076. id_to_name_map[vm_id] = name
  1077. else:
  1078. name = id_to_name_map[vm_id]
  1079. # skip VMs with unknown name
  1080. if name is None:
  1081. continue
  1082. if not list(qubes.api.apply_filters([name], filters)):
  1083. continue
  1084. self.send_event(name, 'vm-stats',
  1085. memory_kb=int(vm_info['memory_kb']),
  1086. cpu_time=int(vm_info['cpu_time'] / 1000000),
  1087. cpu_usage=int(vm_info['cpu_usage']))
  1088. return info_time, info
  1089. @qubes.api.method('admin.vm.Stats', no_payload=True,
  1090. scope='global', read=True)
  1091. @asyncio.coroutine
  1092. def vm_stats(self):
  1093. assert not self.arg
  1094. # run until client connection is terminated
  1095. self.cancellable = True
  1096. # cache event filters, to not call an event each time an event arrives
  1097. stats_filters = self.fire_event_for_permission()
  1098. only_vm = None
  1099. if self.dest.name != 'dom0':
  1100. only_vm = self.dest
  1101. self.send_event(self.app, 'connection-established')
  1102. info_time = None
  1103. info = None
  1104. id_to_name_map = {0: 'dom0'}
  1105. try:
  1106. while True:
  1107. info_time, info = self._send_stats_single(info_time, info,
  1108. only_vm, stats_filters, id_to_name_map)
  1109. yield from asyncio.sleep(self.app.stats_interval)
  1110. except asyncio.CancelledError:
  1111. # valid method to terminate this loop
  1112. pass