block.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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.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.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.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. def device_get(self, vm, ident):
  97. # pylint: disable=no-self-use
  98. '''Read information about device from QubesDB
  99. :param vm: backend VM object
  100. :param ident: device identifier
  101. :returns BlockDevice'''
  102. untrusted_qubes_device_attrs = vm.qdb.list(
  103. '/qubes-block-devices/{}/'.format(ident))
  104. if not untrusted_qubes_device_attrs:
  105. return None
  106. return BlockDevice(vm, ident)
  107. @qubes.ext.handler('device-list:block')
  108. def on_device_list_block(self, vm, event):
  109. # pylint: disable=unused-argument,no-self-use
  110. safe_set = {ord(c) for c in
  111. string.ascii_letters + string.digits}
  112. if not vm.is_running():
  113. return
  114. untrusted_qubes_devices = vm.qdb.list('/qubes-block-devices/')
  115. untrusted_idents = set(untrusted_path.split(b'/', 3)[2]
  116. for untrusted_path in untrusted_qubes_devices)
  117. for untrusted_ident in untrusted_idents:
  118. if not all(c in safe_set for c in untrusted_ident):
  119. msg = ("%s vm's device path name contains unsafe characters. "
  120. "Skipping it.")
  121. vm.log.warning(msg % vm.name)
  122. continue
  123. ident = untrusted_ident.decode('ascii', errors='strict')
  124. device_info = self.device_get(vm, ident)
  125. if device_info:
  126. yield device_info
  127. @qubes.ext.handler('device-get:block')
  128. def on_device_get_block(self, vm, event, ident):
  129. # pylint: disable=unused-argument,no-self-use
  130. if not vm.is_running():
  131. return
  132. if not vm.app.vmm.offline_mode:
  133. yield self.device_get(vm, ident)
  134. @qubes.ext.handler('device-list-attached:block')
  135. def on_device_list_attached(self, vm, event, **kwargs):
  136. # pylint: disable=unused-argument,no-self-use
  137. if not vm.is_running():
  138. return
  139. xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
  140. for disk in xml_desc.findall('devices/disk'):
  141. if disk.get('type') != 'block':
  142. continue
  143. dev_path_node = disk.find('source')
  144. if dev_path_node is None:
  145. continue
  146. dev_path = dev_path_node.get('dev')
  147. target_node = disk.find('target')
  148. if target_node is not None:
  149. frontend_dev = target_node.get('dev')
  150. if not frontend_dev:
  151. continue
  152. if frontend_dev in SYSTEM_DISKS:
  153. continue
  154. else:
  155. continue
  156. backend_domain_node = disk.find('backenddomain')
  157. if backend_domain_node is not None:
  158. backend_domain = vm.app.domains[backend_domain_node.get('name')]
  159. else:
  160. backend_domain = vm.app.domains[0]
  161. options = {}
  162. read_only_node = disk.find('readonly')
  163. if read_only_node is not None:
  164. options['read-only'] = 'yes'
  165. else:
  166. options['read-only'] = 'no'
  167. options['frontend-dev'] = frontend_dev
  168. if dev_path.startswith('/dev/'):
  169. ident = dev_path[len('/dev/'):]
  170. else:
  171. ident = dev_path
  172. ident = ident.replace('/', '_')
  173. yield (BlockDevice(backend_domain, ident), options)
  174. def find_unused_frontend(self, vm):
  175. # pylint: disable=no-self-use
  176. '''Find unused block frontend device node for <target dev=.../>
  177. parameter'''
  178. assert vm.is_running()
  179. xml = vm.libvirt_domain.XMLDesc()
  180. parsed_xml = lxml.etree.fromstring(xml)
  181. used = [target.get('dev', None) for target in
  182. parsed_xml.xpath("//domain/devices/disk/target")]
  183. for dev in AVAILABLE_FRONTENDS:
  184. if dev not in used:
  185. return dev
  186. return None
  187. @qubes.ext.handler('device-pre-attach:block')
  188. def on_device_pre_attached_block(self, vm, event, device, options):
  189. # pylint: disable=unused-argument
  190. # validate options
  191. for option, value in options.items():
  192. if option == 'frontend-dev':
  193. if not value.startswith('xvd') and not value.startswith('sd'):
  194. raise qubes.exc.QubesValueError(
  195. 'Invalid frontend-dev option value: ' + value)
  196. elif option == 'read-only':
  197. if value not in ('yes', 'no'):
  198. raise qubes.exc.QubesValueError(
  199. 'read-only option can only have '
  200. '\'yes\' or \'no\' value')
  201. else:
  202. raise qubes.exc.QubesValueError(
  203. 'Unsupported option {}'.format(option))
  204. if 'read-only' not in options:
  205. options['read-only'] = 'yes' if device.mode == 'r' else 'no'
  206. if options.get('read-only', 'no') == 'no' and device.mode == 'r':
  207. raise qubes.exc.QubesValueError(
  208. 'This device can be attached only read-only')
  209. if not vm.is_running():
  210. return
  211. if not device.backend_domain.is_running():
  212. raise qubes.exc.QubesVMNotRunningError(device.backend_domain,
  213. 'Domain {} needs to be running to attach device from '
  214. 'it'.format(device.backend_domain.name))
  215. if 'frontend-dev' not in options:
  216. options['frontend-dev'] = self.find_unused_frontend(vm)
  217. vm.libvirt_domain.attachDevice(
  218. vm.app.env.get_template('libvirt/devices/block.xml').render(
  219. device=device, vm=vm, options=options))
  220. @qubes.ext.handler('device-pre-detach:block')
  221. def on_device_pre_detached_block(self, vm, event, device):
  222. # pylint: disable=unused-argument,no-self-use
  223. if not vm.is_running():
  224. return
  225. # need to enumerate attached device to find frontend_dev option (at
  226. # least)
  227. for attached_device, options in self.on_device_list_attached(vm, event):
  228. if attached_device == device:
  229. vm.libvirt_domain.detachDevice(
  230. vm.app.env.get_template('libvirt/devices/block.xml').render(
  231. device=device, vm=vm, options=options))
  232. break