admin.py 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library 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 GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  18. #
  19. '''
  20. Qubes OS Management API
  21. '''
  22. import asyncio
  23. import functools
  24. import itertools
  25. import os
  26. import string
  27. import subprocess
  28. import pathlib
  29. import libvirt
  30. import lxml.etree
  31. import pkg_resources
  32. import yaml
  33. import qubes.api
  34. import qubes.backup
  35. import qubes.config
  36. import qubes.devices
  37. import qubes.firewall
  38. import qubes.storage
  39. import qubes.utils
  40. import qubes.vm
  41. import qubes.vm.adminvm
  42. import qubes.vm.qubesvm
  43. class QubesMgmtEventsDispatcher:
  44. def __init__(self, filters, send_event):
  45. self.filters = filters
  46. self.send_event = send_event
  47. def vm_handler(self, subject, event, **kwargs):
  48. # do not send internal events
  49. if event.startswith('admin-permission:'):
  50. return
  51. if event.startswith('device-get:'):
  52. return
  53. if event.startswith('device-list:'):
  54. return
  55. if event.startswith('device-list-attached:'):
  56. return
  57. if event in ('domain-is-fully-usable',):
  58. return
  59. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  60. self.filters)):
  61. return
  62. self.send_event(subject, event, **kwargs)
  63. def app_handler(self, subject, event, **kwargs):
  64. if not list(qubes.api.apply_filters([(subject, event, kwargs)],
  65. self.filters)):
  66. return
  67. self.send_event(subject, event, **kwargs)
  68. def on_domain_add(self, subject, event, vm):
  69. # pylint: disable=unused-argument
  70. vm.add_handler('*', self.vm_handler)
  71. def on_domain_delete(self, subject, event, vm):
  72. # pylint: disable=unused-argument
  73. vm.remove_handler('*', self.vm_handler)
  74. class QubesAdminAPI(qubes.api.AbstractQubesAPI):
  75. """Implementation of Qubes Management API calls
  76. This class contains all the methods available in the main API.
  77. .. seealso::
  78. https://www.qubes-os.org/doc/mgmt1/
  79. """
  80. SOCKNAME = '/var/run/qubesd.sock'
  81. @qubes.api.method('admin.vmclass.List', no_payload=True,
  82. scope='global', read=True)
  83. @asyncio.coroutine
  84. def vmclass_list(self):
  85. """List all VM classes"""
  86. self.enforce(not self.arg)
  87. self.enforce(self.dest.name == 'dom0')
  88. entrypoints = self.fire_event_for_filter(
  89. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  90. return ''.join('{}\n'.format(ep.name)
  91. for ep in entrypoints)
  92. @qubes.api.method('admin.vm.List', no_payload=True,
  93. scope='global', read=True)
  94. @asyncio.coroutine
  95. def vm_list(self):
  96. """List all the domains"""
  97. self.enforce(not self.arg)
  98. if self.dest.name == 'dom0':
  99. domains = self.fire_event_for_filter(self.app.domains)
  100. else:
  101. domains = self.fire_event_for_filter([self.dest])
  102. return ''.join('{} class={} state={}\n'.format(
  103. vm.name,
  104. vm.__class__.__name__,
  105. vm.get_power_state())
  106. for vm in sorted(domains))
  107. @qubes.api.method('admin.vm.property.List', no_payload=True,
  108. scope='local', read=True)
  109. @asyncio.coroutine
  110. def vm_property_list(self):
  111. """List all properties on a qube"""
  112. return self._property_list(self.dest)
  113. @qubes.api.method('admin.property.List', no_payload=True,
  114. scope='global', read=True)
  115. @asyncio.coroutine
  116. def property_list(self):
  117. """List all global properties"""
  118. self.enforce(self.dest.name == 'dom0')
  119. return self._property_list(self.app)
  120. def _property_list(self, dest):
  121. self.enforce(not self.arg)
  122. properties = self.fire_event_for_filter(dest.property_list())
  123. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  124. @qubes.api.method('admin.vm.property.Get', no_payload=True,
  125. scope='local', read=True)
  126. @asyncio.coroutine
  127. def vm_property_get(self):
  128. """Get a value of one property"""
  129. return self._property_get(self.dest)
  130. @qubes.api.method('admin.property.Get', no_payload=True,
  131. scope='global', read=True)
  132. @asyncio.coroutine
  133. def property_get(self):
  134. """Get a value of one global property"""
  135. self.enforce(self.dest.name == 'dom0')
  136. return self._property_get(self.app)
  137. def _property_get(self, dest):
  138. if self.arg not in dest.property_list():
  139. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  140. self.fire_event_for_permission()
  141. return self._serialize_property(dest, self.arg)
  142. @staticmethod
  143. def _serialize_property(dest, prop):
  144. property_def = dest.property_get_def(prop)
  145. # explicit list to be sure that it matches protocol spec
  146. if isinstance(property_def, qubes.vm.VMProperty):
  147. property_type = 'vm'
  148. elif property_def.type is int:
  149. property_type = 'int'
  150. elif property_def.type is bool:
  151. property_type = 'bool'
  152. elif prop == 'label':
  153. property_type = 'label'
  154. else:
  155. property_type = 'str'
  156. try:
  157. value = getattr(dest, str(prop))
  158. except AttributeError:
  159. return 'default=True type={} '.format(property_type)
  160. else:
  161. return 'default={} type={} {}'.format(
  162. str(dest.property_is_default(prop)),
  163. property_type,
  164. str(value) if value is not None else '')
  165. @qubes.api.method('admin.vm.property.GetAll', no_payload=True,
  166. scope='local', read=True)
  167. @asyncio.coroutine
  168. def vm_property_get_all(self):
  169. """Get values of all VM properties"""
  170. return self._property_get_all(self.dest)
  171. @qubes.api.method('admin.property.GetAll', no_payload=True,
  172. scope='global', read=True)
  173. @asyncio.coroutine
  174. def property_get_all(self):
  175. """Get value all global properties"""
  176. self.enforce(self.dest.name == 'dom0')
  177. return self._property_get_all(self.app)
  178. def _property_get_all(self, dest):
  179. self.enforce(not self.arg)
  180. properties = dest.property_list()
  181. properties = self.fire_event_for_filter(properties)
  182. return ''.join(
  183. '{} {}\n'.format(str(prop),
  184. self._serialize_property(dest, prop).
  185. replace('\\', '\\\\').replace('\n', '\\n'))
  186. for prop in sorted(properties))
  187. @qubes.api.method('admin.vm.property.GetDefault', no_payload=True,
  188. scope='local', read=True)
  189. @asyncio.coroutine
  190. def vm_property_get_default(self):
  191. """Get a value of one property"""
  192. return self._property_get_default(self.dest)
  193. @qubes.api.method('admin.property.GetDefault', no_payload=True,
  194. scope='global', read=True)
  195. @asyncio.coroutine
  196. def property_get_default(self):
  197. """Get a value of one global property"""
  198. self.enforce(self.dest.name == 'dom0')
  199. return self._property_get_default(self.app)
  200. def _property_get_default(self, dest):
  201. if self.arg not in dest.property_list():
  202. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  203. self.fire_event_for_permission()
  204. property_def = dest.property_get_def(self.arg)
  205. # explicit list to be sure that it matches protocol spec
  206. if isinstance(property_def, qubes.vm.VMProperty):
  207. property_type = 'vm'
  208. elif property_def.type is int:
  209. property_type = 'int'
  210. elif property_def.type is bool:
  211. property_type = 'bool'
  212. elif self.arg == 'label':
  213. property_type = 'label'
  214. else:
  215. property_type = 'str'
  216. try:
  217. value = property_def.get_default(dest)
  218. except AttributeError:
  219. return None
  220. else:
  221. return 'type={} {}'.format(
  222. property_type,
  223. str(value) if value is not None else '')
  224. @qubes.api.method('admin.vm.property.Set',
  225. scope='local', write=True)
  226. @asyncio.coroutine
  227. def vm_property_set(self, untrusted_payload):
  228. """Set property value"""
  229. return self._property_set(self.dest,
  230. untrusted_payload=untrusted_payload)
  231. @qubes.api.method('admin.property.Set',
  232. scope='global', write=True)
  233. @asyncio.coroutine
  234. def property_set(self, untrusted_payload):
  235. """Set property value"""
  236. self.enforce(self.dest.name == 'dom0')
  237. return self._property_set(self.app,
  238. untrusted_payload=untrusted_payload)
  239. def _property_set(self, dest, untrusted_payload):
  240. if self.arg not in dest.property_list():
  241. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  242. property_def = dest.property_get_def(self.arg)
  243. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  244. self.fire_event_for_permission(newvalue=newvalue)
  245. setattr(dest, self.arg, newvalue)
  246. self.app.save()
  247. @qubes.api.method('admin.vm.property.Help', no_payload=True,
  248. scope='local', read=True)
  249. @asyncio.coroutine
  250. def vm_property_help(self):
  251. """Get help for one property"""
  252. return self._property_help(self.dest)
  253. @qubes.api.method('admin.property.Help', no_payload=True,
  254. scope='global', read=True)
  255. @asyncio.coroutine
  256. def property_help(self):
  257. """Get help for one property"""
  258. self.enforce(self.dest.name == 'dom0')
  259. return self._property_help(self.app)
  260. def _property_help(self, dest):
  261. if self.arg not in dest.property_list():
  262. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  263. self.fire_event_for_permission()
  264. try:
  265. doc = dest.property_get_def(self.arg).__doc__
  266. except AttributeError:
  267. return ''
  268. return qubes.utils.format_doc(doc)
  269. @qubes.api.method('admin.vm.property.Reset', no_payload=True,
  270. scope='local', write=True)
  271. @asyncio.coroutine
  272. def vm_property_reset(self):
  273. """Reset a property to a default value"""
  274. return self._property_reset(self.dest)
  275. @qubes.api.method('admin.property.Reset', no_payload=True,
  276. scope='global', write=True)
  277. @asyncio.coroutine
  278. def property_reset(self):
  279. """Reset a property to a default value"""
  280. self.enforce(self.dest.name == 'dom0')
  281. return self._property_reset(self.app)
  282. def _property_reset(self, dest):
  283. if self.arg not in dest.property_list():
  284. raise qubes.exc.QubesNoSuchPropertyError(dest, self.arg)
  285. self.fire_event_for_permission()
  286. delattr(dest, self.arg)
  287. self.app.save()
  288. @qubes.api.method('admin.vm.volume.List', no_payload=True,
  289. scope='local', read=True)
  290. @asyncio.coroutine
  291. def vm_volume_list(self):
  292. self.enforce(not self.arg)
  293. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  294. return ''.join('{}\n'.format(name) for name in volume_names)
  295. @qubes.api.method('admin.vm.volume.Info', no_payload=True,
  296. scope='local', read=True)
  297. @asyncio.coroutine
  298. def vm_volume_info(self):
  299. self.enforce(self.arg in self.dest.volumes.keys())
  300. self.fire_event_for_permission()
  301. volume = self.dest.volumes[self.arg]
  302. # properties defined in API
  303. volume_properties = [
  304. 'pool', 'vid', 'size', 'usage', 'rw', 'source', 'path',
  305. 'save_on_stop', 'snap_on_start', 'revisions_to_keep']
  306. def _serialize(value):
  307. if callable(value):
  308. value = value()
  309. if value is None:
  310. value = ''
  311. return str(value)
  312. info = ''.join('{}={}\n'.format(key, _serialize(getattr(volume, key)))
  313. for key in volume_properties)
  314. try:
  315. info += 'is_outdated={}\n'.format(volume.is_outdated())
  316. except NotImplementedError:
  317. pass
  318. return info
  319. @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True,
  320. scope='local', read=True)
  321. @asyncio.coroutine
  322. def vm_volume_listsnapshots(self):
  323. self.enforce(self.arg in self.dest.volumes.keys())
  324. volume = self.dest.volumes[self.arg]
  325. id_to_timestamp = volume.revisions
  326. revisions = sorted(id_to_timestamp, key=id_to_timestamp.__getitem__)
  327. revisions = self.fire_event_for_filter(revisions)
  328. return ''.join('{}\n'.format(revision) for revision in revisions)
  329. @qubes.api.method('admin.vm.volume.Revert',
  330. scope='local', write=True)
  331. @asyncio.coroutine
  332. def vm_volume_revert(self, untrusted_payload):
  333. self.enforce(self.arg in self.dest.volumes.keys())
  334. untrusted_revision = untrusted_payload.decode('ascii').strip()
  335. del untrusted_payload
  336. volume = self.dest.volumes[self.arg]
  337. snapshots = volume.revisions
  338. self.enforce(untrusted_revision in snapshots)
  339. revision = untrusted_revision
  340. self.fire_event_for_permission(volume=volume, revision=revision)
  341. yield from qubes.utils.coro_maybe(volume.revert(revision))
  342. self.app.save()
  343. # write=True because this allow to clone VM - and most likely modify that
  344. # one - still having the same data
  345. @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True,
  346. scope='local', write=True)
  347. @asyncio.coroutine
  348. def vm_volume_clone_from(self):
  349. self.enforce(self.arg in self.dest.volumes.keys())
  350. volume = self.dest.volumes[self.arg]
  351. self.fire_event_for_permission(volume=volume)
  352. token = qubes.utils.random_string(32)
  353. # save token on self.app, as self is not persistent
  354. if not hasattr(self.app, 'api_admin_pending_clone'):
  355. self.app.api_admin_pending_clone = {}
  356. # don't handle collisions any better - if someone is so much out of
  357. # luck, can try again anyway
  358. self.enforce(token not in self.app.api_admin_pending_clone)
  359. self.app.api_admin_pending_clone[token] = volume
  360. return token
  361. @qubes.api.method('admin.vm.volume.CloneTo',
  362. scope='local', write=True)
  363. @asyncio.coroutine
  364. def vm_volume_clone_to(self, untrusted_payload):
  365. self.enforce(self.arg in self.dest.volumes.keys())
  366. untrusted_token = untrusted_payload.decode('ascii').strip()
  367. del untrusted_payload
  368. self.enforce(
  369. untrusted_token in getattr(self.app, 'api_admin_pending_clone', {}))
  370. token = untrusted_token
  371. del untrusted_token
  372. src_volume = self.app.api_admin_pending_clone[token]
  373. del self.app.api_admin_pending_clone[token]
  374. # make sure the volume still exists, but invalidate token anyway
  375. self.enforce(str(src_volume.pool) in self.app.pools)
  376. self.enforce(src_volume in self.app.pools[str(src_volume.pool)].volumes)
  377. dst_volume = self.dest.volumes[self.arg]
  378. self.fire_event_for_permission(src_volume=src_volume,
  379. dst_volume=dst_volume)
  380. self.dest.volumes[self.arg] = yield from qubes.utils.coro_maybe(
  381. dst_volume.import_volume(src_volume))
  382. self.app.save()
  383. @qubes.api.method('admin.vm.volume.Resize',
  384. scope='local', write=True)
  385. @asyncio.coroutine
  386. def vm_volume_resize(self, untrusted_payload):
  387. self.enforce(self.arg in self.dest.volumes.keys())
  388. untrusted_size = untrusted_payload.decode('ascii').strip()
  389. del untrusted_payload
  390. self.enforce(untrusted_size.isdigit()) # only digits, forbid '-' too
  391. self.enforce(len(untrusted_size) <= 20) # limit to about 2^64
  392. size = int(untrusted_size)
  393. self.fire_event_for_permission(size=size)
  394. try:
  395. yield from self.dest.storage.resize(self.arg, size)
  396. finally: # even if calling qubes.ResizeDisk inside the VM failed
  397. self.app.save()
  398. @qubes.api.method('admin.vm.volume.Clear', no_payload=True,
  399. scope='local', write=True)
  400. @asyncio.coroutine
  401. def vm_volume_clear(self):
  402. self.enforce(self.arg in self.dest.volumes.keys())
  403. self.fire_event_for_permission()
  404. volume = self.dest.volumes[self.arg]
  405. size = volume.size
  406. # Clear the volume by importing empty data into it
  407. path = yield from self.dest.storage.import_data(self.arg, size)
  408. self.dest.fire_event('domain-volume-import-begin',
  409. volume=self.arg, size=size)
  410. pathlib.Path(path).touch()
  411. try:
  412. yield from self.dest.storage.import_data_end(self.arg, True)
  413. except:
  414. self.dest.fire_event('domain-volume-import-end',
  415. volume=self.arg, success=False)
  416. raise
  417. self.dest.fire_event('domain-volume-import-end',
  418. volume=self.arg, success=True)
  419. self.app.save()
  420. @qubes.api.method('admin.vm.volume.Set.revisions_to_keep',
  421. scope='local', write=True)
  422. @asyncio.coroutine
  423. def vm_volume_set_revisions_to_keep(self, untrusted_payload):
  424. self.enforce(self.arg in self.dest.volumes.keys())
  425. try:
  426. untrusted_value = int(untrusted_payload.decode('ascii'))
  427. except (UnicodeDecodeError, ValueError):
  428. raise qubes.api.ProtocolError('Invalid value')
  429. del untrusted_payload
  430. self.enforce(untrusted_value >= 0)
  431. newvalue = untrusted_value
  432. del untrusted_value
  433. self.fire_event_for_permission(newvalue=newvalue)
  434. self.dest.volumes[self.arg].revisions_to_keep = newvalue
  435. self.app.save()
  436. @qubes.api.method('admin.vm.volume.Set.rw',
  437. scope='local', write=True)
  438. @asyncio.coroutine
  439. def vm_volume_set_rw(self, untrusted_payload):
  440. self.enforce(self.arg in self.dest.volumes.keys())
  441. try:
  442. newvalue = qubes.property.bool(None, None,
  443. untrusted_payload.decode('ascii'))
  444. except (UnicodeDecodeError, ValueError):
  445. raise qubes.api.ProtocolError('Invalid value')
  446. del untrusted_payload
  447. self.fire_event_for_permission(newvalue=newvalue)
  448. if not self.dest.is_halted():
  449. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  450. self.dest.volumes[self.arg].rw = newvalue
  451. self.app.save()
  452. @qubes.api.method('admin.vm.tag.List', no_payload=True,
  453. scope='local', read=True)
  454. @asyncio.coroutine
  455. def vm_tag_list(self):
  456. self.enforce(not self.arg)
  457. tags = self.dest.tags
  458. tags = self.fire_event_for_filter(tags)
  459. return ''.join('{}\n'.format(tag) for tag in sorted(tags))
  460. @qubes.api.method('admin.vm.tag.Get', no_payload=True,
  461. scope='local', read=True)
  462. @asyncio.coroutine
  463. def vm_tag_get(self):
  464. qubes.vm.Tags.validate_tag(self.arg)
  465. self.fire_event_for_permission()
  466. return '1' if self.arg in self.dest.tags else '0'
  467. @qubes.api.method('admin.vm.tag.Set', no_payload=True,
  468. scope='local', write=True)
  469. @asyncio.coroutine
  470. def vm_tag_set(self):
  471. qubes.vm.Tags.validate_tag(self.arg)
  472. self.fire_event_for_permission()
  473. self.dest.tags.add(self.arg)
  474. self.app.save()
  475. @qubes.api.method('admin.vm.tag.Remove', no_payload=True,
  476. scope='local', write=True)
  477. @asyncio.coroutine
  478. def vm_tag_remove(self):
  479. qubes.vm.Tags.validate_tag(self.arg)
  480. self.fire_event_for_permission()
  481. try:
  482. self.dest.tags.remove(self.arg)
  483. except KeyError:
  484. raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
  485. self.app.save()
  486. @qubes.api.method('admin.vm.Console', no_payload=True,
  487. scope='local', write=True)
  488. @asyncio.coroutine
  489. def vm_console(self):
  490. self.enforce(not self.arg)
  491. self.fire_event_for_permission()
  492. if not self.dest.is_running():
  493. raise qubes.exc.QubesVMNotRunningError(self.dest)
  494. xml_desc = lxml.etree.fromstring(self.dest.libvirt_domain.XMLDesc())
  495. ttypath = xml_desc.xpath('string(/domain/devices/console/@tty)')
  496. # this value is returned to /etc/qubes-rpc/admin.vm.Console script,
  497. # which will call socat on it
  498. return ttypath
  499. @qubes.api.method('admin.pool.List', no_payload=True,
  500. scope='global', read=True)
  501. @asyncio.coroutine
  502. def pool_list(self):
  503. self.enforce(not self.arg)
  504. self.enforce(self.dest.name == 'dom0')
  505. pools = self.fire_event_for_filter(self.app.pools)
  506. return ''.join('{}\n'.format(pool) for pool in pools)
  507. @qubes.api.method('admin.pool.ListDrivers', no_payload=True,
  508. scope='global', read=True)
  509. @asyncio.coroutine
  510. def pool_listdrivers(self):
  511. self.enforce(self.dest.name == 'dom0')
  512. self.enforce(not self.arg)
  513. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  514. return ''.join('{} {}\n'.format(
  515. driver,
  516. ' '.join(qubes.storage.driver_parameters(driver)))
  517. for driver in drivers)
  518. @qubes.api.method('admin.pool.Info', no_payload=True,
  519. scope='global', read=True)
  520. @asyncio.coroutine
  521. def pool_info(self):
  522. self.enforce(self.dest.name == 'dom0')
  523. self.enforce(self.arg in self.app.pools.keys())
  524. pool = self.app.pools[self.arg]
  525. self.fire_event_for_permission(pool=pool)
  526. other_info = ''
  527. # Deprecated: remove this when all tools using this call are updated
  528. pool_size = pool.size
  529. if pool_size is not None:
  530. other_info += 'size={}\n'.format(pool_size)
  531. pool_usage = pool.usage
  532. if pool_usage is not None:
  533. other_info += 'usage={}\n'.format(pool_usage)
  534. try:
  535. included_in = pool.included_in(self.app)
  536. if included_in:
  537. other_info += 'included_in={}\n'.format(str(included_in))
  538. except NotImplementedError:
  539. pass
  540. return ''.join('{}={}\n'.format(prop, val)
  541. for prop, val in sorted(pool.config.items())) + \
  542. other_info
  543. @qubes.api.method('admin.pool.UsageDetails', no_payload=True,
  544. scope='global', read=True)
  545. @asyncio.coroutine
  546. def pool_usage(self):
  547. self.enforce(self.dest.name == 'dom0')
  548. self.enforce(self.arg in self.app.pools.keys())
  549. pool = self.app.pools[self.arg]
  550. self.fire_event_for_permission(pool=pool)
  551. usage = ''
  552. pool_details = pool.usage_details
  553. for name in sorted(pool_details):
  554. usage += '{}={}\n'.format(name, pool_details[name])
  555. return usage
  556. @qubes.api.method('admin.pool.Add',
  557. scope='global', write=True)
  558. @asyncio.coroutine
  559. def pool_add(self, untrusted_payload):
  560. self.enforce(self.dest.name == 'dom0')
  561. drivers = qubes.storage.pool_drivers()
  562. self.enforce(self.arg in drivers)
  563. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  564. del untrusted_payload
  565. self.enforce(all(('=' in line) for line in untrusted_pool_config))
  566. # pairs of (option, value)
  567. untrusted_pool_config = [line.split('=', 1)
  568. for line in untrusted_pool_config]
  569. # reject duplicated options
  570. self.enforce(
  571. len(set(x[0] for x in untrusted_pool_config)) ==
  572. len([x[0] for x in untrusted_pool_config]))
  573. # and convert to dict
  574. untrusted_pool_config = dict(untrusted_pool_config)
  575. self.enforce('name' in untrusted_pool_config)
  576. untrusted_pool_name = untrusted_pool_config.pop('name')
  577. allowed_chars = string.ascii_letters + string.digits + '-_.'
  578. self.enforce(all(c in allowed_chars for c in untrusted_pool_name))
  579. pool_name = untrusted_pool_name
  580. self.enforce(pool_name not in self.app.pools)
  581. driver_parameters = qubes.storage.driver_parameters(self.arg)
  582. unexpected_parameters = [key for key in untrusted_pool_config
  583. if key not in driver_parameters]
  584. if unexpected_parameters:
  585. raise qubes.exc.QubesException(
  586. 'unexpected driver options: ' + ' '.join(unexpected_parameters))
  587. pool_config = untrusted_pool_config
  588. self.fire_event_for_permission(name=pool_name,
  589. pool_config=pool_config)
  590. yield from self.app.add_pool(name=pool_name, driver=self.arg,
  591. **pool_config)
  592. self.app.save()
  593. @qubes.api.method('admin.pool.Remove', no_payload=True,
  594. scope='global', write=True)
  595. @asyncio.coroutine
  596. def pool_remove(self):
  597. self.enforce(self.dest.name == 'dom0')
  598. self.enforce(self.arg in self.app.pools.keys())
  599. self.fire_event_for_permission()
  600. yield from self.app.remove_pool(self.arg)
  601. self.app.save()
  602. @qubes.api.method('admin.pool.volume.List', no_payload=True,
  603. scope='global', read=True)
  604. @asyncio.coroutine
  605. def pool_volume_list(self):
  606. self.enforce(self.dest.name == 'dom0')
  607. self.enforce(self.arg in self.app.pools.keys())
  608. pool = self.app.pools[self.arg]
  609. volume_names = self.fire_event_for_filter(pool.volumes.keys())
  610. return ''.join('{}\n'.format(name) for name in volume_names)
  611. @qubes.api.method('admin.pool.Set.revisions_to_keep',
  612. scope='global', write=True)
  613. @asyncio.coroutine
  614. def pool_set_revisions_to_keep(self, untrusted_payload):
  615. self.enforce(self.dest.name == 'dom0')
  616. self.enforce(self.arg in self.app.pools.keys())
  617. pool = self.app.pools[self.arg]
  618. try:
  619. untrusted_value = int(untrusted_payload.decode('ascii'))
  620. except (UnicodeDecodeError, ValueError):
  621. raise qubes.api.ProtocolError('Invalid value')
  622. del untrusted_payload
  623. self.enforce(untrusted_value >= 0)
  624. newvalue = untrusted_value
  625. del untrusted_value
  626. self.fire_event_for_permission(newvalue=newvalue)
  627. pool.revisions_to_keep = newvalue
  628. self.app.save()
  629. @qubes.api.method('admin.label.List', no_payload=True,
  630. scope='global', read=True)
  631. @asyncio.coroutine
  632. def label_list(self):
  633. self.enforce(self.dest.name == 'dom0')
  634. self.enforce(not self.arg)
  635. labels = self.fire_event_for_filter(self.app.labels.values())
  636. return ''.join('{}\n'.format(label.name) for label in labels)
  637. @qubes.api.method('admin.label.Get', no_payload=True,
  638. scope='global', read=True)
  639. @asyncio.coroutine
  640. def label_get(self):
  641. self.enforce(self.dest.name == 'dom0')
  642. try:
  643. label = self.app.get_label(self.arg)
  644. except KeyError:
  645. raise qubes.exc.QubesValueError
  646. self.fire_event_for_permission(label=label)
  647. return label.color
  648. @qubes.api.method('admin.label.Index', no_payload=True,
  649. scope='global', read=True)
  650. @asyncio.coroutine
  651. def label_index(self):
  652. self.enforce(self.dest.name == 'dom0')
  653. try:
  654. label = self.app.get_label(self.arg)
  655. except KeyError:
  656. raise qubes.exc.QubesValueError
  657. self.fire_event_for_permission(label=label)
  658. return str(label.index)
  659. @qubes.api.method('admin.label.Create',
  660. scope='global', write=True)
  661. @asyncio.coroutine
  662. def label_create(self, untrusted_payload):
  663. self.enforce(self.dest.name == 'dom0')
  664. # don't confuse label name with label index
  665. self.enforce(not self.arg.isdigit())
  666. allowed_chars = string.ascii_letters + string.digits + '-_.'
  667. self.enforce(all(c in allowed_chars for c in self.arg))
  668. try:
  669. self.app.get_label(self.arg)
  670. except KeyError:
  671. # ok, no such label yet
  672. pass
  673. else:
  674. raise qubes.exc.QubesValueError('label already exists')
  675. untrusted_payload = untrusted_payload.decode('ascii').strip()
  676. self.enforce(len(untrusted_payload) == 8)
  677. self.enforce(untrusted_payload.startswith('0x'))
  678. # besides prefix, only hex digits are allowed
  679. self.enforce(all(x in string.hexdigits for x in untrusted_payload[2:]))
  680. # SEE: #2732
  681. color = untrusted_payload
  682. self.fire_event_for_permission(color=color)
  683. # allocate new index, but make sure it's outside of default labels set
  684. new_index = max(
  685. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  686. label = qubes.Label(new_index, color, self.arg)
  687. self.app.labels[new_index] = label
  688. self.app.save()
  689. @qubes.api.method('admin.label.Remove', no_payload=True,
  690. scope='global', write=True)
  691. @asyncio.coroutine
  692. def label_remove(self):
  693. self.enforce(self.dest.name == 'dom0')
  694. try:
  695. label = self.app.get_label(self.arg)
  696. except KeyError:
  697. raise qubes.exc.QubesValueError
  698. # don't allow removing default labels
  699. self.enforce(label.index > qubes.config.max_default_label)
  700. # FIXME: this should be in app.add_label()
  701. for vm in self.app.domains:
  702. if vm.label == label:
  703. raise qubes.exc.QubesException('label still in use')
  704. self.fire_event_for_permission(label=label)
  705. del self.app.labels[label.index]
  706. self.app.save()
  707. @qubes.api.method('admin.vm.Start', no_payload=True,
  708. scope='local', execute=True)
  709. @asyncio.coroutine
  710. def vm_start(self):
  711. self.enforce(not self.arg)
  712. self.fire_event_for_permission()
  713. try:
  714. yield from self.dest.start()
  715. except libvirt.libvirtError as e:
  716. # change to QubesException, so will be reported to the user
  717. raise qubes.exc.QubesException('Start failed: ' + str(e) +
  718. ', see /var/log/libvirt/libxl/libxl-driver.log for details')
  719. @qubes.api.method('admin.vm.Shutdown', no_payload=True,
  720. scope='local', execute=True)
  721. @asyncio.coroutine
  722. def vm_shutdown(self):
  723. if self.arg:
  724. args = self.arg.split('+')
  725. else:
  726. args = []
  727. self.enforce(all(arg in ('force', 'wait') for arg in args))
  728. force = ('force' in args)
  729. wait = ('wait' in args)
  730. self.fire_event_for_permission(force=force, wait=wait)
  731. yield from self.dest.shutdown(force=force, wait=wait)
  732. @qubes.api.method('admin.vm.Pause', no_payload=True,
  733. scope='local', execute=True)
  734. @asyncio.coroutine
  735. def vm_pause(self):
  736. self.enforce(not self.arg)
  737. self.fire_event_for_permission()
  738. yield from self.dest.pause()
  739. @qubes.api.method('admin.vm.Unpause', no_payload=True,
  740. scope='local', execute=True)
  741. @asyncio.coroutine
  742. def vm_unpause(self):
  743. self.enforce(not self.arg)
  744. self.fire_event_for_permission()
  745. yield from self.dest.unpause()
  746. @qubes.api.method('admin.vm.Kill', no_payload=True,
  747. scope='local', execute=True)
  748. @asyncio.coroutine
  749. def vm_kill(self):
  750. self.enforce(not self.arg)
  751. self.fire_event_for_permission()
  752. yield from self.dest.kill()
  753. @qubes.api.method('admin.Events', no_payload=True,
  754. scope='global', read=True)
  755. @asyncio.coroutine
  756. def events(self):
  757. self.enforce(not self.arg)
  758. # run until client connection is terminated
  759. self.cancellable = True
  760. wait_for_cancel = asyncio.get_event_loop().create_future()
  761. # cache event filters, to not call an event each time an event arrives
  762. event_filters = self.fire_event_for_permission()
  763. dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
  764. if self.dest.name == 'dom0':
  765. self.app.add_handler('*', dispatcher.app_handler)
  766. self.app.add_handler('domain-add', dispatcher.on_domain_add)
  767. self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
  768. for vm in self.app.domains:
  769. vm.add_handler('*', dispatcher.vm_handler)
  770. else:
  771. self.dest.add_handler('*', dispatcher.vm_handler)
  772. # send artificial event as a confirmation that connection is established
  773. self.send_event(self.app, 'connection-established')
  774. try:
  775. yield from wait_for_cancel
  776. except asyncio.CancelledError:
  777. # the above waiting was already interrupted, this is all we need
  778. pass
  779. if self.dest.name == 'dom0':
  780. self.app.remove_handler('*', dispatcher.app_handler)
  781. self.app.remove_handler('domain-add', dispatcher.on_domain_add)
  782. self.app.remove_handler('domain-delete',
  783. dispatcher.on_domain_delete)
  784. for vm in self.app.domains:
  785. vm.remove_handler('*', dispatcher.vm_handler)
  786. else:
  787. self.dest.remove_handler('*', dispatcher.vm_handler)
  788. @qubes.api.method('admin.vm.feature.List', no_payload=True,
  789. scope='local', read=True)
  790. @asyncio.coroutine
  791. def vm_feature_list(self):
  792. self.enforce(not self.arg)
  793. features = self.fire_event_for_filter(self.dest.features.keys())
  794. return ''.join('{}\n'.format(feature) for feature in features)
  795. @qubes.api.method('admin.vm.feature.Get', no_payload=True,
  796. scope='local', read=True)
  797. @asyncio.coroutine
  798. def vm_feature_get(self):
  799. # validation of self.arg done by qrexec-policy is enough
  800. self.fire_event_for_permission()
  801. try:
  802. value = self.dest.features[self.arg]
  803. except KeyError:
  804. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  805. return value
  806. @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True,
  807. scope='local', read=True)
  808. @asyncio.coroutine
  809. def vm_feature_checkwithtemplate(self):
  810. # validation of self.arg done by qrexec-policy is enough
  811. self.fire_event_for_permission()
  812. try:
  813. value = self.dest.features.check_with_template(self.arg)
  814. except KeyError:
  815. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  816. return value
  817. @qubes.api.method('admin.vm.feature.CheckWithNetvm', no_payload=True,
  818. scope='local', read=True)
  819. @asyncio.coroutine
  820. def vm_feature_checkwithnetvm(self):
  821. # validation of self.arg done by qrexec-policy is enough
  822. self.fire_event_for_permission()
  823. try:
  824. value = self.dest.features.check_with_netvm(self.arg)
  825. except KeyError:
  826. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  827. return value
  828. @qubes.api.method('admin.vm.feature.CheckWithAdminVM', no_payload=True,
  829. scope='local', read=True)
  830. @asyncio.coroutine
  831. def vm_feature_checkwithadminvm(self):
  832. # validation of self.arg done by qrexec-policy is enough
  833. self.fire_event_for_permission()
  834. try:
  835. value = self.dest.features.check_with_adminvm(self.arg)
  836. except KeyError:
  837. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  838. return value
  839. @qubes.api.method('admin.vm.feature.CheckWithTemplateAndAdminVM',
  840. no_payload=True, scope='local', read=True)
  841. @asyncio.coroutine
  842. def vm_feature_checkwithtpladminvm(self):
  843. # validation of self.arg done by qrexec-policy is enough
  844. self.fire_event_for_permission()
  845. try:
  846. value = self.dest.features.check_with_template_and_adminvm(self.arg)
  847. except KeyError:
  848. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  849. return value
  850. @qubes.api.method('admin.vm.feature.Remove', no_payload=True,
  851. scope='local', write=True)
  852. @asyncio.coroutine
  853. def vm_feature_remove(self):
  854. # validation of self.arg done by qrexec-policy is enough
  855. self.fire_event_for_permission()
  856. try:
  857. del self.dest.features[self.arg]
  858. except KeyError:
  859. raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
  860. self.app.save()
  861. @qubes.api.method('admin.vm.feature.Set',
  862. scope='local', write=True)
  863. @asyncio.coroutine
  864. def vm_feature_set(self, untrusted_payload):
  865. # validation of self.arg done by qrexec-policy is enough
  866. value = untrusted_payload.decode('ascii', errors='strict')
  867. del untrusted_payload
  868. self.fire_event_for_permission(value=value)
  869. self.dest.features[self.arg] = value
  870. self.app.save()
  871. @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
  872. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  873. scope='global', write=True)
  874. @asyncio.coroutine
  875. def vm_create(self, endpoint, untrusted_payload=None):
  876. return self._vm_create(endpoint, allow_pool=False,
  877. untrusted_payload=untrusted_payload)
  878. @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
  879. for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
  880. scope='global', write=True)
  881. @asyncio.coroutine
  882. def vm_create_in_pool(self, endpoint, untrusted_payload=None):
  883. return self._vm_create(endpoint, allow_pool=True,
  884. untrusted_payload=untrusted_payload)
  885. def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
  886. self.enforce(self.dest.name == 'dom0')
  887. kwargs = {}
  888. pool = None
  889. pools = {}
  890. # this will raise exception if none is found
  891. vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
  892. vm_type)
  893. # if argument is given, it needs to be a valid template, and only
  894. # when given VM class do need a template
  895. if self.arg:
  896. if hasattr(vm_class, 'template'):
  897. if self.arg not in self.app.domains:
  898. raise qubes.api.PermissionDenied(
  899. 'Template {} does not exist'.format(self.arg))
  900. kwargs['template'] = self.app.domains[self.arg]
  901. else:
  902. raise qubes.exc.QubesValueError(
  903. '{} cannot be based on template'.format(vm_type))
  904. for untrusted_param in untrusted_payload.decode('ascii',
  905. errors='strict').split(' '):
  906. untrusted_key, untrusted_value = untrusted_param.split('=', 1)
  907. if untrusted_key in kwargs:
  908. raise qubes.api.ProtocolError('duplicated parameters')
  909. if untrusted_key == 'name':
  910. qubes.vm.validate_name(None, None, untrusted_value)
  911. kwargs['name'] = untrusted_value
  912. elif untrusted_key == 'label':
  913. # don't confuse label name with label index
  914. self.enforce(not untrusted_value.isdigit())
  915. allowed_chars = string.ascii_letters + string.digits + '-_.'
  916. self.enforce(all(c in allowed_chars for c in untrusted_value))
  917. try:
  918. kwargs['label'] = self.app.get_label(untrusted_value)
  919. except KeyError:
  920. raise qubes.exc.QubesValueError
  921. elif untrusted_key == 'pool' and allow_pool:
  922. if pool is not None:
  923. raise qubes.api.ProtocolError('duplicated pool parameter')
  924. pool = self.app.get_pool(untrusted_value)
  925. elif untrusted_key.startswith('pool:') and allow_pool:
  926. untrusted_volume = untrusted_key.split(':', 1)[1]
  927. # kind of ugly, but actual list of volumes is available only
  928. # after creating a VM
  929. self.enforce(untrusted_volume in [
  930. 'root', 'private', 'volatile', 'kernel'])
  931. volume = untrusted_volume
  932. if volume in pools:
  933. raise qubes.api.ProtocolError(
  934. 'duplicated pool:{} parameter'.format(volume))
  935. pools[volume] = self.app.get_pool(untrusted_value)
  936. else:
  937. raise qubes.api.ProtocolError('Invalid param name')
  938. del untrusted_payload
  939. if 'name' not in kwargs or 'label' not in kwargs:
  940. raise qubes.api.ProtocolError('Missing name or label')
  941. if pool and pools:
  942. raise qubes.api.ProtocolError(
  943. 'Only one of \'pool=\' and \'pool:volume=\' can be used')
  944. if kwargs['name'] in self.app.domains:
  945. raise qubes.exc.QubesValueError(
  946. 'VM {} already exists'.format(kwargs['name']))
  947. self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
  948. vm = self.app.add_new_vm(vm_class, **kwargs)
  949. # TODO: move this to extension (in race-free fashion)
  950. vm.tags.add('created-by-' + str(self.src))
  951. try:
  952. yield from vm.create_on_disk(pool=pool, pools=pools)
  953. except:
  954. del self.app.domains[vm]
  955. raise
  956. self.app.save()
  957. @qubes.api.method('admin.vm.CreateDisposable', no_payload=True,
  958. scope='global', write=True)
  959. @asyncio.coroutine
  960. def create_disposable(self):
  961. self.enforce(not self.arg)
  962. if self.dest.name == 'dom0':
  963. dispvm_template = self.src.default_dispvm
  964. else:
  965. dispvm_template = self.dest
  966. self.fire_event_for_permission(dispvm_template=dispvm_template)
  967. dispvm = yield from qubes.vm.dispvm.DispVM.from_appvm(dispvm_template)
  968. # TODO: move this to extension (in race-free fashion, better than here)
  969. dispvm.tags.add('disp-created-by-' + str(self.src))
  970. return dispvm.name
  971. @qubes.api.method('admin.vm.Remove', no_payload=True,
  972. scope='global', write=True)
  973. async def vm_remove(self):
  974. self.enforce(not self.arg)
  975. self.fire_event_for_permission()
  976. async with self.dest.startup_lock:
  977. if not self.dest.is_halted():
  978. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  979. if self.dest.installed_by_rpm:
  980. raise qubes.exc.QubesVMInUseError(self.dest,
  981. "VM installed by package manager: " + self.dest.name)
  982. del self.app.domains[self.dest]
  983. try:
  984. await self.dest.remove_from_disk()
  985. except: # pylint: disable=bare-except
  986. self.app.log.exception('Error while removing VM \'%s\' files',
  987. self.dest.name)
  988. self.app.save()
  989. @qubes.api.method('admin.deviceclass.List', no_payload=True,
  990. scope='global', read=True)
  991. @asyncio.coroutine
  992. def deviceclass_list(self):
  993. """List all DEVICES classes"""
  994. self.enforce(not self.arg)
  995. self.enforce(self.dest.name == 'dom0')
  996. entrypoints = self.fire_event_for_filter(
  997. pkg_resources.iter_entry_points('qubes.devices'))
  998. return ''.join('{}\n'.format(ep.name) for ep in entrypoints)
  999. @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
  1000. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1001. no_payload=True,
  1002. scope='local', read=True)
  1003. @asyncio.coroutine
  1004. def vm_device_available(self, endpoint):
  1005. devclass = endpoint
  1006. devices = self.dest.devices[devclass].available()
  1007. if self.arg:
  1008. devices = [dev for dev in devices if dev.ident == self.arg]
  1009. # no duplicated devices, but device may not exists, in which case
  1010. # the list is empty
  1011. self.enforce(len(devices) <= 1)
  1012. devices = self.fire_event_for_filter(devices, devclass=devclass)
  1013. dev_info = {}
  1014. for dev in devices:
  1015. non_default_attrs = set(attr for attr in dir(dev) if
  1016. not attr.startswith('_')).difference((
  1017. 'backend_domain', 'ident', 'frontend_domain',
  1018. 'description', 'options', 'regex'))
  1019. properties_txt = ' '.join(
  1020. '{}={!s}'.format(prop, value) for prop, value
  1021. in itertools.chain(
  1022. ((key, getattr(dev, key)) for key in non_default_attrs),
  1023. # keep description as the last one, according to API
  1024. # specification
  1025. (('description', dev.description),)
  1026. ))
  1027. self.enforce('\n' not in properties_txt)
  1028. dev_info[dev.ident] = properties_txt
  1029. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  1030. for ident in sorted(dev_info))
  1031. @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
  1032. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1033. no_payload=True,
  1034. scope='local', read=True)
  1035. @asyncio.coroutine
  1036. def vm_device_list(self, endpoint):
  1037. devclass = endpoint
  1038. device_assignments = self.dest.devices[devclass].assignments()
  1039. if self.arg:
  1040. select_backend, select_ident = self.arg.split('+', 1)
  1041. device_assignments = [dev for dev in device_assignments
  1042. if (str(dev.backend_domain), dev.ident)
  1043. == (select_backend, select_ident)]
  1044. # no duplicated devices, but device may not exists, in which case
  1045. # the list is empty
  1046. self.enforce(len(device_assignments) <= 1)
  1047. device_assignments = self.fire_event_for_filter(device_assignments,
  1048. devclass=devclass)
  1049. dev_info = {}
  1050. for dev in device_assignments:
  1051. properties_txt = ' '.join(
  1052. '{}={!s}'.format(opt, value) for opt, value
  1053. in itertools.chain(
  1054. dev.options.items(),
  1055. (('persistent', 'yes' if dev.persistent else 'no'),)
  1056. ))
  1057. self.enforce('\n' not in properties_txt)
  1058. ident = '{!s}+{!s}'.format(dev.backend_domain, dev.ident)
  1059. dev_info[ident] = properties_txt
  1060. return ''.join('{} {}\n'.format(ident, dev_info[ident])
  1061. for ident in sorted(dev_info))
  1062. # Attach/Detach action can both modify persistent state (with
  1063. # persistent=True) and volatile state of running VM (with persistent=False).
  1064. # For this reason, write=True + execute=True
  1065. @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
  1066. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1067. scope='local', write=True, execute=True)
  1068. @asyncio.coroutine
  1069. def vm_device_attach(self, endpoint, untrusted_payload):
  1070. devclass = endpoint
  1071. options = {}
  1072. persistent = False
  1073. for untrusted_option in untrusted_payload.decode('ascii').split():
  1074. try:
  1075. untrusted_key, untrusted_value = untrusted_option.split('=', 1)
  1076. except ValueError:
  1077. raise qubes.api.ProtocolError('Invalid options format')
  1078. if untrusted_key == 'persistent':
  1079. persistent = qubes.property.bool(None, None, untrusted_value)
  1080. else:
  1081. allowed_chars_key = string.digits + string.ascii_letters + '-_.'
  1082. allowed_chars_value = allowed_chars_key + ',+:'
  1083. if any(x not in allowed_chars_key for x in untrusted_key):
  1084. raise qubes.api.ProtocolError(
  1085. 'Invalid chars in option name')
  1086. if any(x not in allowed_chars_value for x in untrusted_value):
  1087. raise qubes.api.ProtocolError(
  1088. 'Invalid chars in option value')
  1089. options[untrusted_key] = untrusted_value
  1090. # qrexec already verified that no strange characters are in self.arg
  1091. backend_domain, ident = self.arg.split('+', 1)
  1092. # may raise KeyError, either on domain or ident
  1093. dev = self.app.domains[backend_domain].devices[devclass][ident]
  1094. self.fire_event_for_permission(device=dev,
  1095. devclass=devclass, persistent=persistent,
  1096. options=options)
  1097. assignment = qubes.devices.DeviceAssignment(
  1098. dev.backend_domain, dev.ident,
  1099. options=options, persistent=persistent)
  1100. yield from self.dest.devices[devclass].attach(assignment)
  1101. self.app.save()
  1102. # Attach/Detach action can both modify persistent state (with
  1103. # persistent=True) and volatile state of running VM (with persistent=False).
  1104. # For this reason, write=True + execute=True
  1105. @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
  1106. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1107. no_payload=True,
  1108. scope='local', write=True, execute=True)
  1109. @asyncio.coroutine
  1110. def vm_device_detach(self, endpoint):
  1111. devclass = endpoint
  1112. # qrexec already verified that no strange characters are in self.arg
  1113. backend_domain, ident = self.arg.split('+', 1)
  1114. # may raise KeyError; if device isn't found, it will be UnknownDevice
  1115. # instance - but allow it, otherwise it will be impossible to detach
  1116. # already removed device
  1117. dev = self.app.domains[backend_domain].devices[devclass][ident]
  1118. self.fire_event_for_permission(device=dev,
  1119. devclass=devclass)
  1120. assignment = qubes.devices.DeviceAssignment(
  1121. dev.backend_domain, dev.ident)
  1122. yield from self.dest.devices[devclass].detach(assignment)
  1123. self.app.save()
  1124. # Attach/Detach action can both modify persistent state (with
  1125. # persistent=True) and volatile state of running VM (with persistent=False).
  1126. # For this reason, write=True + execute=True
  1127. @qubes.api.method('admin.vm.device.{endpoint}.Set.persistent',
  1128. endpoints=(ep.name
  1129. for ep in pkg_resources.iter_entry_points('qubes.devices')),
  1130. scope='local', write=True, execute=True)
  1131. @asyncio.coroutine
  1132. def vm_device_set_persistent(self, endpoint, untrusted_payload):
  1133. devclass = endpoint
  1134. self.enforce(untrusted_payload in (b'True', b'False'))
  1135. persistent = untrusted_payload == b'True'
  1136. del untrusted_payload
  1137. # qrexec already verified that no strange characters are in self.arg
  1138. backend_domain, ident = self.arg.split('+', 1)
  1139. # device must be already attached
  1140. matching_devices = [dev for dev
  1141. in self.dest.devices[devclass].attached()
  1142. if dev.backend_domain.name == backend_domain and dev.ident == ident]
  1143. self.enforce(len(matching_devices) == 1)
  1144. dev = matching_devices[0]
  1145. self.fire_event_for_permission(device=dev,
  1146. persistent=persistent)
  1147. self.dest.devices[devclass].update_persistent(dev, persistent)
  1148. self.app.save()
  1149. @qubes.api.method('admin.vm.firewall.Get', no_payload=True,
  1150. scope='local', read=True)
  1151. @asyncio.coroutine
  1152. def vm_firewall_get(self):
  1153. self.enforce(not self.arg)
  1154. self.fire_event_for_permission()
  1155. return ''.join('{}\n'.format(rule.api_rule)
  1156. for rule in self.dest.firewall.rules
  1157. if rule.api_rule is not None)
  1158. @qubes.api.method('admin.vm.firewall.Set',
  1159. scope='local', write=True)
  1160. @asyncio.coroutine
  1161. def vm_firewall_set(self, untrusted_payload):
  1162. self.enforce(not self.arg)
  1163. rules = []
  1164. for untrusted_line in untrusted_payload.decode('ascii',
  1165. errors='strict').splitlines():
  1166. rule = qubes.firewall.Rule.from_api_string(
  1167. untrusted_rule=untrusted_line)
  1168. rules.append(rule)
  1169. self.fire_event_for_permission(rules=rules)
  1170. self.dest.firewall.rules = rules
  1171. self.dest.firewall.save()
  1172. @qubes.api.method('admin.vm.firewall.Reload', no_payload=True,
  1173. scope='local', execute=True)
  1174. @asyncio.coroutine
  1175. def vm_firewall_reload(self):
  1176. self.enforce(not self.arg)
  1177. self.fire_event_for_permission()
  1178. self.dest.fire_event('firewall-changed')
  1179. @asyncio.coroutine
  1180. def _load_backup_profile(self, profile_name, skip_passphrase=False):
  1181. """Load backup profile and return :py:class:`qubes.backup.Backup`
  1182. instance
  1183. :param profile_name: name of the profile
  1184. :param skip_passphrase: do not load passphrase - only backup summary
  1185. can be retrieved when this option is in use
  1186. """
  1187. profile_path = os.path.join(
  1188. qubes.config.backup_profile_dir, profile_name + '.conf')
  1189. with open(profile_path) as profile_file:
  1190. profile_data = yaml.safe_load(profile_file)
  1191. try:
  1192. dest_vm = profile_data['destination_vm']
  1193. dest_path = profile_data['destination_path']
  1194. include_vms = profile_data['include']
  1195. if include_vms is not None:
  1196. # convert old keywords to new keywords
  1197. include_vms = [vm.replace('$', '@') for vm in include_vms]
  1198. exclude_vms = profile_data.get('exclude', [])
  1199. # convert old keywords to new keywords
  1200. exclude_vms = [vm.replace('$', '@') for vm in exclude_vms]
  1201. compression = profile_data.get('compression', True)
  1202. except KeyError as err:
  1203. raise qubes.exc.QubesException(
  1204. 'Invalid backup profile - missing {}'.format(err))
  1205. try:
  1206. dest_vm = self.app.domains[dest_vm]
  1207. except KeyError:
  1208. raise qubes.exc.QubesException(
  1209. 'Invalid destination_vm specified in backup profile')
  1210. if isinstance(dest_vm, qubes.vm.adminvm.AdminVM):
  1211. dest_vm = None
  1212. if skip_passphrase:
  1213. passphrase = None
  1214. elif 'passphrase_text' in profile_data:
  1215. passphrase = profile_data['passphrase_text']
  1216. elif 'passphrase_vm' in profile_data:
  1217. passphrase_vm_name = profile_data['passphrase_vm']
  1218. try:
  1219. passphrase_vm = self.app.domains[passphrase_vm_name]
  1220. except KeyError:
  1221. raise qubes.exc.QubesException(
  1222. 'Invalid backup profile - invalid passphrase_vm')
  1223. try:
  1224. passphrase, _ = yield from passphrase_vm.run_service_for_stdio(
  1225. 'qubes.BackupPassphrase+' + self.arg)
  1226. # make it foolproof against "echo passphrase" implementation
  1227. passphrase = passphrase.strip()
  1228. self.enforce(b'\n' not in passphrase)
  1229. except subprocess.CalledProcessError:
  1230. raise qubes.exc.QubesException(
  1231. 'Failed to retrieve passphrase from \'{}\' VM'.format(
  1232. passphrase_vm_name))
  1233. else:
  1234. raise qubes.exc.QubesException(
  1235. 'Invalid backup profile - you need to '
  1236. 'specify passphrase_text or passphrase_vm')
  1237. # handle include
  1238. if include_vms is None:
  1239. vms_to_backup = None
  1240. else:
  1241. vms_to_backup = set(vm for vm in self.app.domains
  1242. if any(qubes.utils.match_vm_name_with_special(vm, name)
  1243. for name in include_vms))
  1244. # handle exclude
  1245. vms_to_exclude = set(vm.name for vm in self.app.domains
  1246. if any(qubes.utils.match_vm_name_with_special(vm, name)
  1247. for name in exclude_vms))
  1248. kwargs = {
  1249. 'target_vm': dest_vm,
  1250. 'target_dir': dest_path,
  1251. 'compressed': bool(compression),
  1252. 'passphrase': passphrase,
  1253. }
  1254. if isinstance(compression, str):
  1255. kwargs['compression_filter'] = compression
  1256. backup = qubes.backup.Backup(self.app, vms_to_backup, vms_to_exclude,
  1257. **kwargs)
  1258. return backup
  1259. def _backup_progress_callback(self, profile_name, progress):
  1260. self.app.fire_event('backup-progress', backup_profile=profile_name,
  1261. progress=progress)
  1262. @qubes.api.method('admin.backup.Execute', no_payload=True,
  1263. scope='global', read=True, execute=True)
  1264. @asyncio.coroutine
  1265. def backup_execute(self):
  1266. self.enforce(self.dest.name == 'dom0')
  1267. self.enforce(self.arg)
  1268. self.enforce('/' not in self.arg)
  1269. self.fire_event_for_permission()
  1270. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1271. self.arg + '.conf')
  1272. if not os.path.exists(profile_path):
  1273. raise qubes.api.PermissionDenied(
  1274. 'Backup profile {} does not exist'.format(self.arg))
  1275. if not hasattr(self.app, 'api_admin_running_backups'):
  1276. self.app.api_admin_running_backups = {}
  1277. backup = yield from self._load_backup_profile(self.arg)
  1278. backup.progress_callback = functools.partial(
  1279. self._backup_progress_callback, self.arg)
  1280. # forbid running the same backup operation twice at the time
  1281. if self.arg in self.app.api_admin_running_backups:
  1282. raise qubes.exc.BackupAlreadyRunningError()
  1283. backup_task = asyncio.ensure_future(backup.backup_do())
  1284. self.app.api_admin_running_backups[self.arg] = backup_task
  1285. try:
  1286. yield from backup_task
  1287. except asyncio.CancelledError:
  1288. raise qubes.exc.QubesException('Backup cancelled')
  1289. finally:
  1290. del self.app.api_admin_running_backups[self.arg]
  1291. @qubes.api.method('admin.backup.Cancel', no_payload=True,
  1292. scope='global', execute=True)
  1293. @asyncio.coroutine
  1294. def backup_cancel(self):
  1295. self.enforce(self.dest.name == 'dom0')
  1296. self.enforce(self.arg)
  1297. self.enforce('/' not in self.arg)
  1298. self.fire_event_for_permission()
  1299. if not hasattr(self.app, 'api_admin_running_backups'):
  1300. self.app.api_admin_running_backups = {}
  1301. if self.arg not in self.app.api_admin_running_backups:
  1302. raise qubes.exc.QubesException('Backup operation not running')
  1303. self.app.api_admin_running_backups[self.arg].cancel()
  1304. @qubes.api.method('admin.backup.Info', no_payload=True,
  1305. scope='local', read=True)
  1306. @asyncio.coroutine
  1307. def backup_info(self):
  1308. self.enforce(self.dest.name == 'dom0')
  1309. self.enforce(self.arg)
  1310. self.enforce('/' not in self.arg)
  1311. self.fire_event_for_permission()
  1312. profile_path = os.path.join(qubes.config.backup_profile_dir,
  1313. self.arg + '.conf')
  1314. if not os.path.exists(profile_path):
  1315. raise qubes.api.PermissionDenied(
  1316. 'Backup profile {} does not exist'.format(self.arg))
  1317. backup = yield from self._load_backup_profile(self.arg,
  1318. skip_passphrase=True)
  1319. return backup.get_backup_summary()
  1320. def _send_stats_single(self, info_time, info, only_vm, filters,
  1321. id_to_name_map):
  1322. """A single iteration of sending VM stats
  1323. :param info_time: time of previous iteration
  1324. :param info: information retrieved in previous iteration
  1325. :param only_vm: send information only about this VM
  1326. :param filters: filters to apply on stats before sending
  1327. :param id_to_name_map: ID->VM name map, may be modified
  1328. :return: tuple(info_time, info) - new information (to be passed to
  1329. the next iteration)
  1330. """
  1331. (info_time, info) = self.app.host.get_vm_stats(info_time, info,
  1332. only_vm=only_vm)
  1333. for vm_id, vm_info in info.items():
  1334. if vm_id not in id_to_name_map:
  1335. try:
  1336. name = \
  1337. self.app.vmm.libvirt_conn.lookupByID(vm_id).name()
  1338. except libvirt.libvirtError as err:
  1339. if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
  1340. # stubdomain or so
  1341. name = None
  1342. else:
  1343. raise
  1344. id_to_name_map[vm_id] = name
  1345. else:
  1346. name = id_to_name_map[vm_id]
  1347. # skip VMs with unknown name
  1348. if name is None:
  1349. continue
  1350. if not list(qubes.api.apply_filters([name], filters)):
  1351. continue
  1352. self.send_event(name, 'vm-stats',
  1353. memory_kb=int(vm_info['memory_kb']),
  1354. cpu_time=int(vm_info['cpu_time'] / 1000000),
  1355. cpu_usage=int(vm_info['cpu_usage']),
  1356. cpu_usage_raw=int(vm_info['cpu_usage_raw']))
  1357. return info_time, info
  1358. @qubes.api.method('admin.vm.Stats', no_payload=True,
  1359. scope='global', read=True)
  1360. @asyncio.coroutine
  1361. def vm_stats(self):
  1362. self.enforce(not self.arg)
  1363. # run until client connection is terminated
  1364. self.cancellable = True
  1365. # cache event filters, to not call an event each time an event arrives
  1366. stats_filters = self.fire_event_for_permission()
  1367. only_vm = None
  1368. if self.dest.name != 'dom0':
  1369. only_vm = self.dest
  1370. self.send_event(self.app, 'connection-established')
  1371. info_time = None
  1372. info = None
  1373. id_to_name_map = {0: 'dom0'}
  1374. try:
  1375. while True:
  1376. info_time, info = self._send_stats_single(info_time, info,
  1377. only_vm, stats_filters, id_to_name_map)
  1378. yield from asyncio.sleep(self.app.stats_interval)
  1379. except asyncio.CancelledError:
  1380. # valid method to terminate this loop
  1381. pass
  1382. @qubes.api.method('admin.vm.CurrentState', no_payload=True,
  1383. scope='local', read=True)
  1384. @asyncio.coroutine
  1385. def vm_current_state(self):
  1386. self.enforce(not self.arg)
  1387. self.fire_event_for_permission()
  1388. state = {
  1389. 'mem': self.dest.get_mem(),
  1390. 'mem_static_max': self.dest.get_mem_static_max(),
  1391. 'cputime': self.dest.get_cputime(),
  1392. 'power_state': self.dest.get_power_state(),
  1393. }
  1394. return ' '.join('{}={}'.format(k, v) for k, v in state.items())