admin.py 56 KB

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