block.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. ''' Qubes block devices extensions '''
  21. import re
  22. import string
  23. import lxml.etree
  24. import qubes.devices
  25. import qubes.ext
  26. name_re = re.compile(r"^[a-z0-9-]{1,12}$")
  27. device_re = re.compile(r"^[a-z0-9/-]{1,64}$")
  28. # FIXME: any better idea of desc_re?
  29. desc_re = re.compile(r"^.{1,255}$")
  30. mode_re = re.compile(r"^[rw]$")
  31. # all frontends, prefer xvdi
  32. # TODO: get this from libvirt driver?
  33. AVAILABLE_FRONTENDS = ['xvd'+c for c in
  34. string.ascii_lowercase[8:]+string.ascii_lowercase[:8]]
  35. SYSTEM_DISKS = ('xvda', 'xvdb', 'xvdc', 'xvdd')
  36. class BlockDevice(qubes.devices.DeviceInfo):
  37. def __init__(self, backend_domain, ident):
  38. super(BlockDevice, self).__init__(backend_domain=backend_domain,
  39. ident=ident)
  40. self._description = None
  41. self._mode = None
  42. self._size = None
  43. @property
  44. def description(self):
  45. '''Human readable device description'''
  46. if self._description is None:
  47. if not self.backend_domain.is_running():
  48. return self.ident
  49. safe_set = {ord(c) for c in
  50. string.ascii_letters + string.digits + '()+,-.:=_/ '}
  51. untrusted_desc = self.backend_domain.untrusted_qdb.read(
  52. '/qubes-block-devices/{}/desc'.format(self.ident))
  53. desc = ''.join((chr(c) if c in safe_set else '_')
  54. for c in untrusted_desc)
  55. self._description = desc
  56. return self._description
  57. @property
  58. def mode(self):
  59. '''Device mode, either 'w' for read-write, or 'r' for read-only'''
  60. if self._mode is None:
  61. if not self.backend_domain.is_running():
  62. return 'w'
  63. untrusted_mode = self.backend_domain.untrusted_qdb.read(
  64. '/qubes-block-devices/{}/mode'.format(self.ident))
  65. if untrusted_mode is None:
  66. self._mode = 'w'
  67. elif untrusted_mode not in (b'w', b'r'):
  68. self.backend_domain.log.warning(
  69. 'Device {} has invalid mode'.format(self.ident))
  70. self._mode = 'w'
  71. else:
  72. self._mode = untrusted_mode.decode()
  73. return self._mode
  74. @property
  75. def size(self):
  76. '''Device size in bytes'''
  77. if self._size is None:
  78. if not self.backend_domain.is_running():
  79. return None
  80. untrusted_size = self.backend_domain.untrusted_qdb.read(
  81. '/qubes-block-devices/{}/size'.format(self.ident))
  82. if untrusted_size is None:
  83. self._size = 0
  84. elif not untrusted_size.isdigit():
  85. self.backend_domain.log.warning(
  86. 'Device {} has invalid size'.format(self.ident))
  87. self._size = 0
  88. else:
  89. self._size = int(untrusted_size)
  90. return self._size
  91. @property
  92. def device_node(self):
  93. '''Device node in backend domain'''
  94. return '/dev/' + self.ident.replace('_', '/')
  95. class BlockDeviceExtension(qubes.ext.Extension):
  96. @qubes.ext.handler('domain-init', 'domain-load')
  97. def on_domain_init_load(self, vm, event):
  98. '''Initialize watching for changes'''
  99. # pylint: disable=unused-argument,no-self-use
  100. vm.watch_qdb_path('/qubes-block-devices')
  101. @qubes.ext.handler('domain-qdb-change:/qubes-block-devices')
  102. def on_qdb_change(self, vm, event, path):
  103. '''A change in QubesDB means a change in device list'''
  104. # pylint: disable=unused-argument,no-self-use
  105. vm.fire_event('device-list-change:block')
  106. def device_get(self, vm, ident):
  107. # pylint: disable=no-self-use
  108. '''Read information about device from QubesDB
  109. :param vm: backend VM object
  110. :param ident: device identifier
  111. :returns BlockDevice'''
  112. untrusted_qubes_device_attrs = vm.untrusted_qdb.list(
  113. '/qubes-block-devices/{}/'.format(ident))
  114. if not untrusted_qubes_device_attrs:
  115. return None
  116. return BlockDevice(vm, ident)
  117. @qubes.ext.handler('device-list:block')
  118. def on_device_list_block(self, vm, event):
  119. # pylint: disable=unused-argument,no-self-use
  120. safe_set = string.ascii_letters + string.digits
  121. if not vm.is_running():
  122. return
  123. untrusted_qubes_devices = vm.untrusted_qdb.list('/qubes-block-devices/')
  124. untrusted_idents = set(untrusted_path.split('/', 3)[2]
  125. for untrusted_path in untrusted_qubes_devices)
  126. for untrusted_ident in untrusted_idents:
  127. if not all(c in safe_set for c in untrusted_ident):
  128. msg = ("%s vm's device path name contains unsafe characters. "
  129. "Skipping it.")
  130. vm.log.warning(msg % vm.name)
  131. continue
  132. ident = untrusted_ident
  133. device_info = self.device_get(vm, ident)
  134. if device_info:
  135. yield device_info
  136. @qubes.ext.handler('device-get:block')
  137. def on_device_get_block(self, vm, event, ident):
  138. # pylint: disable=unused-argument,no-self-use
  139. if not vm.is_running():
  140. return
  141. if not vm.app.vmm.offline_mode:
  142. yield self.device_get(vm, ident)
  143. @qubes.ext.handler('device-list-attached:block')
  144. def on_device_list_attached(self, vm, event, **kwargs):
  145. # pylint: disable=unused-argument,no-self-use
  146. if not vm.is_running():
  147. return
  148. xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
  149. for disk in xml_desc.findall('devices/disk'):
  150. if disk.get('type') != 'block':
  151. continue
  152. dev_path_node = disk.find('source')
  153. if dev_path_node is None:
  154. continue
  155. dev_path = dev_path_node.get('dev')
  156. target_node = disk.find('target')
  157. if target_node is not None:
  158. frontend_dev = target_node.get('dev')
  159. if not frontend_dev:
  160. continue
  161. if frontend_dev in SYSTEM_DISKS:
  162. continue
  163. else:
  164. continue
  165. backend_domain_node = disk.find('backenddomain')
  166. if backend_domain_node is not None:
  167. backend_domain = vm.app.domains[backend_domain_node.get('name')]
  168. else:
  169. backend_domain = vm.app.domains[0]
  170. options = {}
  171. read_only_node = disk.find('readonly')
  172. if read_only_node is not None:
  173. options['read-only'] = 'yes'
  174. else:
  175. options['read-only'] = 'no'
  176. options['frontend-dev'] = frontend_dev
  177. if dev_path.startswith('/dev/'):
  178. ident = dev_path[len('/dev/'):]
  179. else:
  180. ident = dev_path
  181. ident = ident.replace('/', '_')
  182. yield (BlockDevice(backend_domain, ident), options)
  183. def find_unused_frontend(self, vm):
  184. # pylint: disable=no-self-use
  185. '''Find unused block frontend device node for <target dev=.../>
  186. parameter'''
  187. assert vm.is_running()
  188. xml = vm.libvirt_domain.XMLDesc()
  189. parsed_xml = lxml.etree.fromstring(xml)
  190. used = [target.get('dev', None) for target in
  191. parsed_xml.xpath("//domain/devices/disk/target")]
  192. for dev in AVAILABLE_FRONTENDS:
  193. if dev not in used:
  194. return dev
  195. return None
  196. @qubes.ext.handler('device-pre-attach:block')
  197. def on_device_pre_attached_block(self, vm, event, device, options):
  198. # pylint: disable=unused-argument
  199. # validate options
  200. for option, value in options.items():
  201. if option == 'frontend-dev':
  202. if not value.startswith('xvd') and not value.startswith('sd'):
  203. raise qubes.exc.QubesValueError(
  204. 'Invalid frontend-dev option value: ' + value)
  205. elif option == 'read-only':
  206. if value not in ('yes', 'no'):
  207. raise qubes.exc.QubesValueError(
  208. 'read-only option can only have '
  209. '\'yes\' or \'no\' value')
  210. elif option == 'devtype':
  211. if value not in ('disk', 'cdrom'):
  212. raise qubes.exc.QubesValueError(
  213. 'devtype option can only have '
  214. '\'disk\' or \'cdrom\' value')
  215. else:
  216. raise qubes.exc.QubesValueError(
  217. 'Unsupported option {}'.format(option))
  218. if 'read-only' not in options:
  219. options['read-only'] = 'yes' if device.mode == 'r' else 'no'
  220. if options.get('read-only', 'no') == 'no' and device.mode == 'r':
  221. raise qubes.exc.QubesValueError(
  222. 'This device can be attached only read-only')
  223. if not vm.is_running():
  224. return
  225. if not device.backend_domain.is_running():
  226. raise qubes.exc.QubesVMNotRunningError(device.backend_domain,
  227. 'Domain {} needs to be running to attach device from '
  228. 'it'.format(device.backend_domain.name))
  229. if 'frontend-dev' not in options:
  230. options['frontend-dev'] = self.find_unused_frontend(vm)
  231. vm.libvirt_domain.attachDevice(
  232. vm.app.env.get_template('libvirt/devices/block.xml').render(
  233. device=device, vm=vm, options=options))
  234. @qubes.ext.handler('device-pre-detach:block')
  235. def on_device_pre_detached_block(self, vm, event, device):
  236. # pylint: disable=unused-argument,no-self-use
  237. if not vm.is_running():
  238. return
  239. # need to enumerate attached device to find frontend_dev option (at
  240. # least)
  241. for attached_device, options in self.on_device_list_attached(vm, event):
  242. if attached_device == device:
  243. vm.libvirt_domain.detachDevice(
  244. vm.app.env.get_template('libvirt/devices/block.xml').render(
  245. device=device, vm=vm, options=options))
  246. break