block.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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 library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library 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 GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://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. if not untrusted_desc:
  54. return ''
  55. desc = ''.join((chr(c) if c in safe_set else '_')
  56. for c in untrusted_desc)
  57. self._description = desc
  58. return self._description
  59. @property
  60. def mode(self):
  61. '''Device mode, either 'w' for read-write, or 'r' for read-only'''
  62. if self._mode is None:
  63. if not self.backend_domain.is_running():
  64. return 'w'
  65. untrusted_mode = self.backend_domain.untrusted_qdb.read(
  66. '/qubes-block-devices/{}/mode'.format(self.ident))
  67. if untrusted_mode is None:
  68. self._mode = 'w'
  69. elif untrusted_mode not in (b'w', b'r'):
  70. self.backend_domain.log.warning(
  71. 'Device {} has invalid mode'.format(self.ident))
  72. self._mode = 'w'
  73. else:
  74. self._mode = untrusted_mode.decode()
  75. return self._mode
  76. @property
  77. def size(self):
  78. '''Device size in bytes'''
  79. if self._size is None:
  80. if not self.backend_domain.is_running():
  81. return None
  82. untrusted_size = self.backend_domain.untrusted_qdb.read(
  83. '/qubes-block-devices/{}/size'.format(self.ident))
  84. if untrusted_size is None:
  85. self._size = 0
  86. elif not untrusted_size.isdigit():
  87. self.backend_domain.log.warning(
  88. 'Device {} has invalid size'.format(self.ident))
  89. self._size = 0
  90. else:
  91. self._size = int(untrusted_size)
  92. return self._size
  93. @property
  94. def device_node(self):
  95. '''Device node in backend domain'''
  96. return '/dev/' + self.ident.replace('_', '/')
  97. class BlockDeviceExtension(qubes.ext.Extension):
  98. @qubes.ext.handler('domain-init', 'domain-load')
  99. def on_domain_init_load(self, vm, event):
  100. '''Initialize watching for changes'''
  101. # pylint: disable=unused-argument,no-self-use
  102. vm.watch_qdb_path('/qubes-block-devices')
  103. @qubes.ext.handler('domain-qdb-change:/qubes-block-devices')
  104. def on_qdb_change(self, vm, event, path):
  105. '''A change in QubesDB means a change in device list'''
  106. # pylint: disable=unused-argument,no-self-use
  107. vm.fire_event('device-list-change:block')
  108. def device_get(self, vm, ident):
  109. # pylint: disable=no-self-use
  110. '''Read information about device from QubesDB
  111. :param vm: backend VM object
  112. :param ident: device identifier
  113. :returns BlockDevice'''
  114. untrusted_qubes_device_attrs = vm.untrusted_qdb.list(
  115. '/qubes-block-devices/{}/'.format(ident))
  116. if not untrusted_qubes_device_attrs:
  117. return None
  118. return BlockDevice(vm, ident)
  119. @qubes.ext.handler('device-list:block')
  120. def on_device_list_block(self, vm, event):
  121. # pylint: disable=unused-argument,no-self-use
  122. if not vm.is_running():
  123. return
  124. untrusted_qubes_devices = vm.untrusted_qdb.list('/qubes-block-devices/')
  125. untrusted_idents = set(untrusted_path.split('/', 3)[2]
  126. for untrusted_path in untrusted_qubes_devices)
  127. for untrusted_ident in untrusted_idents:
  128. if not name_re.match(untrusted_ident):
  129. msg = ("%s vm's device path name contains unsafe characters. "
  130. "Skipping it.")
  131. vm.log.warning(msg % vm.name)
  132. continue
  133. ident = untrusted_ident
  134. device_info = self.device_get(vm, ident)
  135. if device_info:
  136. yield device_info
  137. @qubes.ext.handler('device-get:block')
  138. def on_device_get_block(self, vm, event, ident):
  139. # pylint: disable=unused-argument,no-self-use
  140. if not vm.is_running():
  141. return
  142. if not vm.app.vmm.offline_mode:
  143. device_info = self.device_get(vm, ident)
  144. if device_info:
  145. yield device_info
  146. @qubes.ext.handler('device-list-attached:block')
  147. def on_device_list_attached(self, vm, event, **kwargs):
  148. # pylint: disable=unused-argument,no-self-use
  149. if not vm.is_running():
  150. return
  151. xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
  152. for disk in xml_desc.findall('devices/disk'):
  153. if disk.get('type') != 'block':
  154. continue
  155. dev_path_node = disk.find('source')
  156. if dev_path_node is None:
  157. continue
  158. dev_path = dev_path_node.get('dev')
  159. target_node = disk.find('target')
  160. if target_node is not None:
  161. frontend_dev = target_node.get('dev')
  162. if not frontend_dev:
  163. continue
  164. if frontend_dev in SYSTEM_DISKS:
  165. continue
  166. else:
  167. continue
  168. backend_domain_node = disk.find('backenddomain')
  169. if backend_domain_node is not None:
  170. backend_domain = vm.app.domains[backend_domain_node.get('name')]
  171. else:
  172. backend_domain = vm.app.domains[0]
  173. options = {}
  174. read_only_node = disk.find('readonly')
  175. if read_only_node is not None:
  176. options['read-only'] = 'yes'
  177. else:
  178. options['read-only'] = 'no'
  179. options['frontend-dev'] = frontend_dev
  180. if disk.get('device') != 'disk':
  181. options['devtype'] = disk.get('device')
  182. if dev_path.startswith('/dev/'):
  183. ident = dev_path[len('/dev/'):]
  184. else:
  185. ident = dev_path
  186. ident = ident.replace('/', '_')
  187. yield (BlockDevice(backend_domain, ident), options)
  188. def find_unused_frontend(self, vm):
  189. # pylint: disable=no-self-use
  190. '''Find unused block frontend device node for <target dev=.../>
  191. parameter'''
  192. assert vm.is_running()
  193. xml = vm.libvirt_domain.XMLDesc()
  194. parsed_xml = lxml.etree.fromstring(xml)
  195. used = [target.get('dev', None) for target in
  196. parsed_xml.xpath("//domain/devices/disk/target")]
  197. for dev in AVAILABLE_FRONTENDS:
  198. if dev not in used:
  199. return dev
  200. return None
  201. @qubes.ext.handler('device-pre-attach:block')
  202. def on_device_pre_attached_block(self, vm, event, device, options):
  203. # pylint: disable=unused-argument
  204. # validate options
  205. for option, value in options.items():
  206. if option == 'frontend-dev':
  207. if not value.startswith('xvd') and not value.startswith('sd'):
  208. raise qubes.exc.QubesValueError(
  209. 'Invalid frontend-dev option value: ' + value)
  210. elif option == 'read-only':
  211. options[option] = (
  212. 'yes' if qubes.property.bool(None, None, value) else 'no')
  213. elif option == 'devtype':
  214. if value not in ('disk', 'cdrom'):
  215. raise qubes.exc.QubesValueError(
  216. 'devtype option can only have '
  217. '\'disk\' or \'cdrom\' value')
  218. else:
  219. raise qubes.exc.QubesValueError(
  220. 'Unsupported option {}'.format(option))
  221. if 'read-only' not in options:
  222. options['read-only'] = 'yes' if device.mode == 'r' else 'no'
  223. if options.get('read-only', 'no') == 'no' and device.mode == 'r':
  224. raise qubes.exc.QubesValueError(
  225. 'This device can be attached only read-only')
  226. if not vm.is_running():
  227. return
  228. if not device.backend_domain.is_running():
  229. raise qubes.exc.QubesVMNotRunningError(device.backend_domain,
  230. 'Domain {} needs to be running to attach device from '
  231. 'it'.format(device.backend_domain.name))
  232. if 'frontend-dev' not in options:
  233. options['frontend-dev'] = self.find_unused_frontend(vm)
  234. vm.libvirt_domain.attachDevice(
  235. vm.app.env.get_template('libvirt/devices/block.xml').render(
  236. device=device, vm=vm, options=options))
  237. @qubes.ext.handler('device-pre-detach:block')
  238. def on_device_pre_detached_block(self, vm, event, device):
  239. # pylint: disable=unused-argument,no-self-use
  240. if not vm.is_running():
  241. return
  242. # need to enumerate attached device to find frontend_dev option (at
  243. # least)
  244. for attached_device, options in self.on_device_list_attached(vm, event):
  245. if attached_device == device:
  246. vm.libvirt_domain.detachDevice(
  247. vm.app.env.get_template('libvirt/devices/block.xml').render(
  248. device=device, vm=vm, options=options))
  249. break