mgmt.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along
  17. # with this program; if not, write to the Free Software Foundation, Inc.,
  18. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. #
  20. '''
  21. Qubes OS Management API
  22. '''
  23. import functools
  24. import string
  25. import pkg_resources
  26. import qubes.vm
  27. import qubes.vm.qubesvm
  28. import qubes.storage
  29. class ProtocolError(AssertionError):
  30. '''Raised when something is wrong with data received'''
  31. pass
  32. class PermissionDenied(Exception):
  33. '''Raised deliberately by handlers when we decide not to cooperate'''
  34. pass
  35. def api(name, *, no_payload=False):
  36. '''Decorator factory for methods intended to appear in API.
  37. The decorated method can be called from public API using a child of
  38. :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
  39. called using remote management interface.
  40. :param str name: qrexec rpc method name
  41. :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
  42. also will not pass payload at all to the method
  43. The expected function method should have one argument (other than usual
  44. *self*), ``untrusted_payload``, which will contain the payload.
  45. .. warning::
  46. This argument has to be named such, to remind the programmer that the
  47. content of this variable is indeed untrusted.
  48. If *no_payload* is true, then the method is called with no arguments.
  49. '''
  50. # TODO regexp for vm/dev classess; supply regexp groups as untrusted_ kwargs
  51. def decorator(func):
  52. if no_payload:
  53. # the following assignment is needed for how closures work in Python
  54. _func = func
  55. @functools.wraps(_func)
  56. def wrapper(self, untrusted_payload):
  57. if untrusted_payload != b'':
  58. raise ProtocolError('unexpected payload')
  59. return _func(self)
  60. func = wrapper
  61. func._rpcname = name # pylint: disable=protected-access
  62. return func
  63. return decorator
  64. class AbstractQubesMgmt(object):
  65. '''Common code for Qubes Management Protocol handling
  66. Different interfaces can expose different API call sets, however they share
  67. common protocol and common implementation framework. This class is the
  68. latter.
  69. To implement a new interface, inherit from this class and write at least one
  70. method and decorate it with :py:func:`api` decorator. It will have access to
  71. pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
  72. :py:attr:`arg` and :py:attr:`method`.
  73. There are also two helper functions for firing events associated with API
  74. calls.
  75. '''
  76. def __init__(self, app, src, method, dest, arg):
  77. #: :py:class:`qubes.Qubes` object
  78. self.app = app
  79. #: source qube
  80. self.src = self.app.domains[src.decode('ascii')]
  81. #: destination qube
  82. self.dest = self.app.domains[dest.decode('ascii')]
  83. #: argument
  84. self.arg = arg.decode('ascii')
  85. #: name of the method
  86. self.method = method.decode('ascii')
  87. untrusted_candidates = []
  88. for attr in dir(self):
  89. untrusted_func = getattr(self, attr)
  90. if not callable(untrusted_func):
  91. continue
  92. try:
  93. # pylint: disable=protected-access
  94. if untrusted_func._rpcname != self.method:
  95. continue
  96. except AttributeError:
  97. continue
  98. untrusted_candidates.append(untrusted_func)
  99. if not untrusted_candidates:
  100. raise ProtocolError('no such method: {!r}'.format(self.method))
  101. assert len(untrusted_candidates) == 1, \
  102. 'multiple candidates for method {!r}'.format(self.method)
  103. #: the method to execute
  104. self.execute = untrusted_candidates[0]
  105. del untrusted_candidates
  106. def fire_event_for_permission(self, **kwargs):
  107. '''Fire an event on the source qube to check for permission'''
  108. return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
  109. dest=self.dest, arg=self.arg, **kwargs)
  110. def fire_event_for_filter(self, iterable, **kwargs):
  111. '''Fire an event on the source qube to filter for permission'''
  112. for selector in self.fire_event_for_permission(**kwargs):
  113. iterable = filter(selector, iterable)
  114. return iterable
  115. class QubesMgmt(AbstractQubesMgmt):
  116. '''Implementation of Qubes Management API calls
  117. This class contains all the methods available in the main API.
  118. .. seealso::
  119. https://www.qubes-os.org/doc/mgmt1/
  120. '''
  121. @api('mgmt.vmclass.List', no_payload=True)
  122. async def vmclass_list(self):
  123. '''List all VM classes'''
  124. assert not self.arg
  125. assert self.dest.name == 'dom0'
  126. entrypoints = self.fire_event_for_filter(
  127. pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
  128. return ''.join('{}\n'.format(ep.name)
  129. for ep in entrypoints)
  130. @api('mgmt.vm.List', no_payload=True)
  131. async def vm_list(self):
  132. '''List all the domains'''
  133. assert not self.arg
  134. if self.dest.name == 'dom0':
  135. domains = self.fire_event_for_filter(self.app.domains)
  136. else:
  137. domains = self.fire_event_for_filter([self.dest])
  138. return ''.join('{} class={} state={}\n'.format(
  139. vm.name,
  140. vm.__class__.__name__,
  141. vm.get_power_state())
  142. for vm in sorted(domains))
  143. @api('mgmt.vm.property.List', no_payload=True)
  144. async def vm_property_list(self):
  145. '''List all properties on a qube'''
  146. assert not self.arg
  147. properties = self.fire_event_for_filter(self.dest.property_list())
  148. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  149. @api('mgmt.vm.property.Get', no_payload=True)
  150. async def vm_property_get(self):
  151. '''Get a value of one property'''
  152. assert self.arg in self.dest.property_list()
  153. self.fire_event_for_permission()
  154. property_def = self.dest.property_get_def(self.arg)
  155. # explicit list to be sure that it matches protocol spec
  156. if isinstance(property_def, qubes.vm.VMProperty):
  157. property_type = 'vm'
  158. elif property_def.type is int:
  159. property_type = 'int'
  160. elif property_def.type is bool:
  161. property_type = 'bool'
  162. elif self.arg == 'label':
  163. property_type = 'label'
  164. else:
  165. property_type = 'str'
  166. try:
  167. value = getattr(self.dest, self.arg)
  168. except AttributeError:
  169. return 'default=True type={} '.format(property_type)
  170. else:
  171. return 'default={} type={} {}'.format(
  172. str(self.dest.property_is_default(self.arg)),
  173. property_type,
  174. str(value) if value is not None else '')
  175. @api('mgmt.vm.property.Set')
  176. async def vm_property_set(self, untrusted_payload):
  177. assert self.arg in self.dest.property_list()
  178. property_def = self.dest.property_get_def(self.arg)
  179. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  180. self.fire_event_for_permission(newvalue=newvalue)
  181. setattr(self.dest, self.arg, newvalue)
  182. self.app.save()
  183. @api('mgmt.vm.property.Help', no_payload=True)
  184. async def vm_property_help(self):
  185. '''Get help for one property'''
  186. assert self.arg in self.dest.property_list()
  187. self.fire_event_for_permission()
  188. try:
  189. doc = self.dest.property_get_def(self.arg).__doc__
  190. except AttributeError:
  191. return ''
  192. return qubes.utils.format_doc(doc)
  193. @api('mgmt.vm.property.Reset', no_payload=True)
  194. async def vm_property_reset(self):
  195. '''Reset a property to a default value'''
  196. assert self.arg in self.dest.property_list()
  197. self.fire_event_for_permission()
  198. delattr(self.dest, self.arg)
  199. self.app.save()
  200. @api('mgmt.vm.volume.List', no_payload=True)
  201. async def vm_volume_list(self):
  202. assert not self.arg
  203. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  204. return ''.join('{}\n'.format(name) for name in volume_names)
  205. @api('mgmt.vm.volume.Info', no_payload=True)
  206. async def vm_volume_info(self):
  207. assert self.arg in self.dest.volumes.keys()
  208. self.fire_event_for_permission()
  209. volume = self.dest.volumes[self.arg]
  210. # properties defined in API
  211. volume_properties = [
  212. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  213. 'save_on_stop', 'snap_on_start']
  214. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  215. volume_properties)
  216. @api('mgmt.vm.volume.ListSnapshots', no_payload=True)
  217. async def vm_volume_listsnapshots(self):
  218. assert self.arg in self.dest.volumes.keys()
  219. volume = self.dest.volumes[self.arg]
  220. revisions = [revision for revision in volume.revisions]
  221. revisions = self.fire_event_for_filter(revisions)
  222. return ''.join('{}\n'.format(revision) for revision in revisions)
  223. @api('mgmt.vm.volume.Revert')
  224. async def vm_volume_revert(self, untrusted_payload):
  225. assert self.arg in self.dest.volumes.keys()
  226. untrusted_revision = untrusted_payload.decode('ascii').strip()
  227. del untrusted_payload
  228. volume = self.dest.volumes[self.arg]
  229. snapshots = volume.revisions
  230. assert untrusted_revision in snapshots
  231. revision = untrusted_revision
  232. self.fire_event_for_permission(revision=revision)
  233. self.dest.storage.get_pool(volume).revert(revision)
  234. self.app.save()
  235. @api('mgmt.vm.volume.Resize')
  236. async def vm_volume_resize(self, untrusted_payload):
  237. assert self.arg in self.dest.volumes.keys()
  238. untrusted_size = untrusted_payload.decode('ascii').strip()
  239. del untrusted_payload
  240. assert untrusted_size.isdigit() # only digits, forbid '-' too
  241. assert len(untrusted_size) <= 20 # limit to about 2^64
  242. size = int(untrusted_size)
  243. self.fire_event_for_permission(size=size)
  244. self.dest.storage.resize(self.arg, size)
  245. self.app.save()
  246. @api('mgmt.pool.List', no_payload=True)
  247. async def pool_list(self):
  248. assert not self.arg
  249. assert self.dest.name == 'dom0'
  250. pools = self.fire_event_for_filter(self.app.pools)
  251. return ''.join('{}\n'.format(pool) for pool in pools)
  252. @api('mgmt.pool.ListDrivers', no_payload=True)
  253. async def pool_listdrivers(self):
  254. assert self.dest.name == 'dom0'
  255. assert not self.arg
  256. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  257. return ''.join('{} {}\n'.format(
  258. driver,
  259. ' '.join(qubes.storage.driver_parameters(driver)))
  260. for driver in drivers)
  261. @api('mgmt.pool.Info', no_payload=True)
  262. async def pool_info(self):
  263. assert self.dest.name == 'dom0'
  264. assert self.arg in self.app.pools.keys()
  265. pool = self.app.pools[self.arg]
  266. self.fire_event_for_permission(pool=pool)
  267. return ''.join('{}={}\n'.format(prop, val)
  268. for prop, val in sorted(pool.config.items()))
  269. @api('mgmt.pool.Add')
  270. async def pool_add(self, untrusted_payload):
  271. assert self.dest.name == 'dom0'
  272. drivers = qubes.storage.pool_drivers()
  273. assert self.arg in drivers
  274. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  275. del untrusted_payload
  276. assert all(('=' in line) for line in untrusted_pool_config)
  277. # pairs of (option, value)
  278. untrusted_pool_config = [line.split('=', 1)
  279. for line in untrusted_pool_config]
  280. # reject duplicated options
  281. assert len(set(x[0] for x in untrusted_pool_config)) == \
  282. len([x[0] for x in untrusted_pool_config])
  283. # and convert to dict
  284. untrusted_pool_config = dict(untrusted_pool_config)
  285. assert 'name' in untrusted_pool_config
  286. untrusted_pool_name = untrusted_pool_config.pop('name')
  287. allowed_chars = string.ascii_letters + string.digits + '-_.'
  288. assert all(c in allowed_chars for c in untrusted_pool_name)
  289. pool_name = untrusted_pool_name
  290. assert pool_name not in self.app.pools
  291. driver_parameters = qubes.storage.driver_parameters(self.arg)
  292. assert all(key in driver_parameters for key in untrusted_pool_config)
  293. pool_config = untrusted_pool_config
  294. self.fire_event_for_permission(name=pool_name,
  295. pool_config=pool_config)
  296. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  297. self.app.save()
  298. @api('mgmt.pool.Remove', no_payload=True)
  299. async def pool_remove(self):
  300. assert self.dest.name == 'dom0'
  301. assert self.arg in self.app.pools.keys()
  302. self.fire_event_for_permission()
  303. self.app.remove_pool(self.arg)
  304. self.app.save()
  305. @api('mgmt.label.List', no_payload=True)
  306. async def label_list(self):
  307. assert self.dest.name == 'dom0'
  308. assert not self.arg
  309. labels = self.fire_event_for_filter(self.app.labels.values())
  310. return ''.join('{}\n'.format(label.name) for label in labels)
  311. @api('mgmt.label.Get', no_payload=True)
  312. async def label_get(self):
  313. assert self.dest.name == 'dom0'
  314. try:
  315. label = self.app.get_label(self.arg)
  316. except KeyError:
  317. raise qubes.exc.QubesValueError
  318. self.fire_event_for_permission(label=label)
  319. return label.color
  320. @api('mgmt.label.Create')
  321. async def label_create(self, untrusted_payload):
  322. assert self.dest.name == 'dom0'
  323. # don't confuse label name with label index
  324. assert not self.arg.isdigit()
  325. allowed_chars = string.ascii_letters + string.digits + '-_.'
  326. assert all(c in allowed_chars for c in self.arg)
  327. try:
  328. self.app.get_label(self.arg)
  329. except KeyError:
  330. # ok, no such label yet
  331. pass
  332. else:
  333. raise qubes.exc.QubesValueError('label already exists')
  334. untrusted_payload = untrusted_payload.decode('ascii').strip()
  335. assert len(untrusted_payload) == 8
  336. assert untrusted_payload.startswith('0x')
  337. # besides prefix, only hex digits are allowed
  338. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  339. # SEE: #2732
  340. color = untrusted_payload
  341. self.fire_event_for_permission(color=color)
  342. # allocate new index, but make sure it's outside of default labels set
  343. new_index = max(
  344. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  345. label = qubes.Label(new_index, color, self.arg)
  346. self.app.labels[new_index] = label
  347. self.app.save()
  348. @api('mgmt.label.Remove', no_payload=True)
  349. async def label_remove(self):
  350. assert self.dest.name == 'dom0'
  351. try:
  352. label = self.app.get_label(self.arg)
  353. except KeyError:
  354. raise qubes.exc.QubesValueError
  355. # don't allow removing default labels
  356. assert label.index > qubes.config.max_default_label
  357. # FIXME: this should be in app.add_label()
  358. for vm in self.app.domains:
  359. if vm.label == label:
  360. raise qubes.exc.QubesException('label still in use')
  361. self.fire_event_for_permission(label=label)
  362. del self.app.labels[label.index]
  363. self.app.save()
  364. @api('mgmt.vm.Start', no_payload=True)
  365. async def vm_start(self):
  366. assert not self.arg
  367. self.fire_event_for_permission()
  368. await self.dest.start()
  369. @api('mgmt.vm.Shutdown', no_payload=True)
  370. async def vm_shutdown(self):
  371. assert not self.arg
  372. self.fire_event_for_permission()
  373. await self.dest.shutdown()
  374. @api('mgmt.vm.Pause', no_payload=True)
  375. async def vm_pause(self):
  376. assert not self.arg
  377. self.fire_event_for_permission()
  378. await self.dest.pause()
  379. @api('mgmt.vm.Unpause', no_payload=True)
  380. async def vm_unpause(self):
  381. assert not self.arg
  382. self.fire_event_for_permission()
  383. await self.dest.unpause()
  384. @api('mgmt.vm.Kill', no_payload=True)
  385. async def vm_kill(self):
  386. assert not self.arg
  387. self.fire_event_for_permission()
  388. await self.dest.kill()