qubesutils.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2011 Marek Marczykowski <marmarek@invisiblethingslab.com>
  7. # Copyright (C) 2014 Wojciech Porczyk <wojciech@porczyk.eu>
  8. #
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. #
  23. #
  24. from __future__ import absolute_import
  25. import string
  26. from lxml import etree
  27. from lxml.etree import ElementTree, SubElement, Element
  28. from qubes.qubes import QubesException
  29. from qubes.qubes import vmm
  30. from qubes.qubes import system_path,vm_files
  31. import sys
  32. import os
  33. import subprocess
  34. import re
  35. import time
  36. import stat
  37. import libvirt
  38. from qubes.qdb import QubesDB,Error,DisconnectedError
  39. import xen.lowlevel.xc
  40. import xen.lowlevel.xs
  41. BLKSIZE = 512
  42. # all frontends, prefer xvdi
  43. # TODO: get this from libvirt driver?
  44. AVAILABLE_FRONTENDS = ['xvd'+c for c in
  45. string.lowercase[8:]+string.lowercase[:8]]
  46. def mbytes_to_kmg(size):
  47. if size > 1024:
  48. return "%d GiB" % (size/1024)
  49. else:
  50. return "%d MiB" % size
  51. def kbytes_to_kmg(size):
  52. if size > 1024:
  53. return mbytes_to_kmg(size/1024)
  54. else:
  55. return "%d KiB" % size
  56. def bytes_to_kmg(size):
  57. if size > 1024:
  58. return kbytes_to_kmg(size/1024)
  59. else:
  60. return "%d B" % size
  61. def size_to_human (size):
  62. """Humane readable size, with 1/10 precission"""
  63. if size < 1024:
  64. return str (size);
  65. elif size < 1024*1024:
  66. return str(round(size/1024.0,1)) + ' KiB'
  67. elif size < 1024*1024*1024:
  68. return str(round(size/(1024.0*1024),1)) + ' MiB'
  69. else:
  70. return str(round(size/(1024.0*1024*1024),1)) + ' GiB'
  71. def parse_size(size):
  72. units = [ ('K', 1024), ('KB', 1024),
  73. ('M', 1024*1024), ('MB', 1024*1024),
  74. ('G', 1024*1024*1024), ('GB', 1024*1024*1024),
  75. ]
  76. size = size.strip().upper()
  77. if size.isdigit():
  78. return int(size)
  79. for unit, multiplier in units:
  80. if size.endswith(unit):
  81. size = size[:-len(unit)].strip()
  82. return int(size)*multiplier
  83. raise QubesException("Invalid size: {0}.".format(size))
  84. def get_disk_usage_one(st):
  85. try:
  86. return st.st_blocks * BLKSIZE
  87. except AttributeError:
  88. return st.st_size
  89. def get_disk_usage(path):
  90. try:
  91. st = os.lstat(path)
  92. except OSError:
  93. return 0
  94. ret = get_disk_usage_one(st)
  95. # if path is not a directory, this is skipped
  96. for dirpath, dirnames, filenames in os.walk(path):
  97. for name in dirnames + filenames:
  98. ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
  99. return ret
  100. def print_stdout(text):
  101. print (text)
  102. def print_stderr(text):
  103. print >> sys.stderr, (text)
  104. ###### Block devices ########
  105. def block_devid_to_name(devid):
  106. major = devid / 256
  107. minor = devid % 256
  108. dev_class = ""
  109. if major == 202:
  110. dev_class = "xvd"
  111. elif major == 8:
  112. dev_class = "sd"
  113. else:
  114. raise QubesException("Unknown device class %d" % major)
  115. if minor % 16 == 0:
  116. return "%s%c" % (dev_class, ord('a')+minor/16)
  117. else:
  118. return "%s%c%d" % (dev_class, ord('a')+minor/16, minor%16)
  119. def block_name_to_majorminor(name):
  120. # check if it is already devid
  121. if isinstance(name, int):
  122. return (name / 256, name % 256)
  123. if name.isdigit():
  124. return (int(name) / 256, int(name) % 256)
  125. if os.path.exists('/dev/%s' % name):
  126. blk_info = os.stat(os.path.realpath('/dev/%s' % name))
  127. if stat.S_ISBLK(blk_info.st_mode):
  128. return (blk_info.st_rdev / 256, blk_info.st_rdev % 256)
  129. major = 0
  130. minor = 0
  131. dXpY_style = False
  132. disk = True
  133. if name.startswith("xvd"):
  134. major = 202
  135. elif name.startswith("sd"):
  136. major = 8
  137. elif name.startswith("mmcblk"):
  138. dXpY_style = True
  139. major = 179
  140. elif name.startswith("scd"):
  141. disk = False
  142. major = 11
  143. elif name.startswith("sr"):
  144. disk = False
  145. major = 11
  146. elif name.startswith("loop"):
  147. dXpY_style = True
  148. disk = False
  149. major = 7
  150. elif name.startswith("md"):
  151. dXpY_style = True
  152. major = 9
  153. elif name.startswith("dm-"):
  154. disk = False
  155. major = 253
  156. else:
  157. # Unknown device
  158. return (0, 0)
  159. if not dXpY_style:
  160. name_match = re.match(r"^([a-z]+)([a-z-])([0-9]*)$", name)
  161. else:
  162. name_match = re.match(r"^([a-z]+)([0-9]*)(?:p([0-9]+))?$", name)
  163. if not name_match:
  164. raise QubesException("Invalid device name: %s" % name)
  165. if disk:
  166. if dXpY_style:
  167. minor = int(name_match.group(2))*8
  168. else:
  169. minor = (ord(name_match.group(2))-ord('a')) * 16
  170. else:
  171. minor = 0
  172. if name_match.group(3):
  173. minor += int(name_match.group(3))
  174. return (major, minor)
  175. def block_name_to_devid(name):
  176. # check if it is already devid
  177. if isinstance(name, int):
  178. return name
  179. if name.isdigit():
  180. return int(name)
  181. (major, minor) = block_name_to_majorminor(name)
  182. return major << 8 | minor
  183. def block_find_unused_frontend(vm = None):
  184. assert vm is not None
  185. assert vm.is_running()
  186. xml = vm.libvirt_domain.XMLDesc()
  187. parsed_xml = etree.fromstring(xml)
  188. used = [target.get('dev', None) for target in
  189. parsed_xml.xpath("//domain/devices/disk/target")]
  190. for dev in AVAILABLE_FRONTENDS:
  191. if dev not in used:
  192. return dev
  193. return None
  194. def block_list_vm(vm, system_disks = False):
  195. name_re = re.compile(r"^[a-z0-9-]{1,12}$")
  196. device_re = re.compile(r"^[a-z0-9/-]{1,64}$")
  197. # FIXME: any better idea of desc_re?
  198. desc_re = re.compile(r"^.{1,255}$")
  199. mode_re = re.compile(r"^[rw]$")
  200. assert vm is not None
  201. if not vm.is_running():
  202. return []
  203. devices_list = {}
  204. try:
  205. untrusted_devices = vm.qdb.multiread('/qubes-block-devices/')
  206. except Error:
  207. vm.refresh()
  208. return {}
  209. def get_dev_item(dev, item):
  210. return untrusted_devices.get(
  211. '/qubes-block-devices/%s/%s' % (dev, item),
  212. None)
  213. untrusted_devices_names = list(set(map(lambda x: x.split("/")[2],
  214. untrusted_devices.keys())))
  215. for untrusted_dev_name in untrusted_devices_names:
  216. if name_re.match(untrusted_dev_name):
  217. dev_name = untrusted_dev_name
  218. untrusted_device_size = get_dev_item(dev_name, 'size')
  219. untrusted_device_desc = get_dev_item(dev_name, 'desc')
  220. untrusted_device_mode = get_dev_item(dev_name, 'mode')
  221. untrusted_device_device = get_dev_item(dev_name, 'device')
  222. if untrusted_device_desc is None or untrusted_device_mode is None\
  223. or untrusted_device_size is None:
  224. print >>sys.stderr, "Missing field in %s device parameters" %\
  225. dev_name
  226. continue
  227. if untrusted_device_device is None:
  228. untrusted_device_device = '/dev/' + dev_name
  229. if not device_re.match(untrusted_device_device):
  230. print >> sys.stderr, "Invalid %s device path in VM '%s'" % (
  231. dev_name, vm.name)
  232. continue
  233. device_device = untrusted_device_device
  234. if not untrusted_device_size.isdigit():
  235. print >> sys.stderr, "Invalid %s device size in VM '%s'" % (
  236. dev_name, vm.name)
  237. continue
  238. device_size = int(untrusted_device_size)
  239. if not desc_re.match(untrusted_device_desc):
  240. print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (
  241. dev_name, vm.name)
  242. continue
  243. device_desc = untrusted_device_desc
  244. if not mode_re.match(untrusted_device_mode):
  245. print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (
  246. dev_name, vm.name)
  247. continue
  248. device_mode = untrusted_device_mode
  249. if not system_disks:
  250. if vm.qid == 0 and device_desc.startswith(system_path[
  251. "qubes_base_dir"]):
  252. continue
  253. visible_name = "%s:%s" % (vm.name, dev_name)
  254. devices_list[visible_name] = {
  255. "name": visible_name,
  256. "vm": vm.name,
  257. "device": device_device,
  258. "size": device_size,
  259. "desc": device_desc,
  260. "mode": device_mode
  261. }
  262. return devices_list
  263. def block_list(qvmc = None, vm = None, system_disks = False):
  264. if vm is not None:
  265. if not vm.is_running():
  266. return []
  267. else:
  268. vm_list = [ vm ]
  269. else:
  270. if qvmc is None:
  271. raise QubesException("You must pass either qvm or vm argument")
  272. vm_list = qvmc.values()
  273. devices_list = {}
  274. for vm in vm_list:
  275. devices_list.update(block_list_vm(vm, system_disks))
  276. return devices_list
  277. def block_check_attached(qvmc, device):
  278. """
  279. @type qvmc: QubesVmCollection
  280. """
  281. if qvmc is None:
  282. # TODO: ValueError
  283. raise QubesException("You need to pass qvmc argument")
  284. for vm in qvmc.values():
  285. if vm.qid == 0:
  286. # Connecting devices to dom0 not supported
  287. continue
  288. if not vm.is_running():
  289. continue
  290. try:
  291. libvirt_domain = vm.libvirt_domain
  292. if libvirt_domain:
  293. xml = libvirt_domain.XMLDesc()
  294. else:
  295. xml = None
  296. except libvirt.libvirtError:
  297. if vmm.libvirt_conn.virConnGetLastError()[0] == libvirt.VIR_ERR_NO_DOMAIN:
  298. xml = None
  299. else:
  300. raise
  301. if xml:
  302. parsed_xml = etree.fromstring(xml)
  303. disks = parsed_xml.xpath("//domain/devices/disk")
  304. for disk in disks:
  305. backend_name = 'dom0'
  306. if disk.find('backenddomain') is not None:
  307. backend_name = disk.find('backenddomain').get('name')
  308. source = disk.find('source')
  309. if disk.get('type') == 'file':
  310. path = source.get('file')
  311. elif disk.get('type') == 'block':
  312. path = source.get('dev')
  313. else:
  314. # TODO: logger
  315. print >>sys.stderr, "Unknown disk type '%s' attached to " \
  316. "VM '%s'" % (source.get('type'),
  317. vm.name)
  318. continue
  319. if backend_name == device['vm'] and path == device['device']:
  320. return {
  321. "frontend": disk.find('target').get('dev'),
  322. "vm": vm}
  323. return None
  324. def device_attach_check(vm, backend_vm, device, frontend, mode):
  325. """ Checks all the parameters, dies on errors """
  326. if not vm.is_running():
  327. raise QubesException("VM %s not running" % vm.name)
  328. if not backend_vm.is_running():
  329. raise QubesException("VM %s not running" % backend_vm.name)
  330. if device['mode'] == 'r' and mode == 'w':
  331. raise QubesException("Cannot attach read-only device in read-write "
  332. "mode")
  333. def block_attach(qvmc, vm, device, frontend=None, mode="w", auto_detach=False, wait=True):
  334. backend_vm = qvmc.get_vm_by_name(device['vm'])
  335. device_attach_check(vm, backend_vm, device, frontend, mode)
  336. if frontend is None:
  337. frontend = block_find_unused_frontend(vm)
  338. if frontend is None:
  339. raise QubesException("No unused frontend found")
  340. else:
  341. # Check if any device attached at this frontend
  342. xml = vm.libvirt_domain.XMLDesc()
  343. parsed_xml = etree.fromstring(xml)
  344. disks = parsed_xml.xpath("//domain/devices/disk/target[@dev='%s']" %
  345. frontend)
  346. if len(disks):
  347. raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
  348. # Check if this device is attached to some domain
  349. attached_vm = block_check_attached(qvmc, device)
  350. if attached_vm:
  351. if auto_detach:
  352. block_detach(attached_vm['vm'], attached_vm['frontend'])
  353. else:
  354. raise QubesException("Device %s from %s already connected to VM "
  355. "%s as %s" % (device['device'],
  356. backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
  357. disk = Element("disk")
  358. disk.set('type', 'block')
  359. disk.set('device', 'disk')
  360. SubElement(disk, 'driver').set('name', 'phy')
  361. SubElement(disk, 'source').set('dev', device['device'])
  362. SubElement(disk, 'target').set('dev', frontend)
  363. if backend_vm.qid != 0:
  364. SubElement(disk, 'backenddomain').set('name', device['vm'])
  365. vm.libvirt_domain.attachDevice(etree.tostring(disk, encoding='utf-8'))
  366. try:
  367. # trigger watches to update device status
  368. # FIXME: this should be removed once libvirt will report such
  369. # events itself
  370. vm.qdb.write('/qubes-block-devices', '')
  371. except Error:
  372. pass
  373. def block_detach(vm, frontend = "xvdi"):
  374. xml = vm.libvirt_domain.XMLDesc()
  375. parsed_xml = etree.fromstring(xml)
  376. attached = parsed_xml.xpath("//domain/devices/disk")
  377. for disk in attached:
  378. if frontend is not None and disk.find('target').get('dev') != frontend:
  379. # Not the device we are looking for
  380. continue
  381. if frontend is None:
  382. # ignore system disks
  383. if disk.find('domain') == None and \
  384. disk.find('source').get('dev').startswith(system_path[
  385. "qubes_base_dir"]):
  386. continue
  387. vm.libvirt_domain.detachDevice(etree.tostring(disk, encoding='utf-8'))
  388. try:
  389. # trigger watches to update device status
  390. # FIXME: this should be removed once libvirt will report such
  391. # events itself
  392. vm.qdb.write('/qubes-block-devices', '')
  393. except Error:
  394. pass
  395. def block_detach_all(vm):
  396. """ Detach all non-system devices"""
  397. block_detach(vm, None)
  398. ####### USB devices ######
  399. usb_ver_re = re.compile(r"^(1|2)$")
  400. usb_device_re = re.compile(r"^[0-9]+-[0-9]+(_[0-9]+)?$")
  401. usb_port_re = re.compile(r"^$|^[0-9]+-[0-9]+(\.[0-9]+)?$")
  402. def usb_setup(backend_vm_xid, vm_xid, devid, usb_ver):
  403. """
  404. Attach frontend to the backend.
  405. backend_vm_xid - id of the backend domain
  406. vm_xid - id of the frontend domain
  407. devid - id of the pvusb controller
  408. """
  409. num_ports = 8
  410. trans = vmm.xs.transaction_start()
  411. be_path = "/local/domain/%d/backend/vusb/%d/%d" % (backend_vm_xid, vm_xid, devid)
  412. fe_path = "/local/domain/%d/device/vusb/%d" % (vm_xid, devid)
  413. be_perm = [{'dom': backend_vm_xid}, {'dom': vm_xid, 'read': True} ]
  414. fe_perm = [{'dom': vm_xid}, {'dom': backend_vm_xid, 'read': True} ]
  415. # Create directories and set permissions
  416. vmm.xs.write(trans, be_path, "")
  417. vmm.xs.set_permissions(trans, be_path, be_perm)
  418. vmm.xs.write(trans, fe_path, "")
  419. vmm.xs.set_permissions(trans, fe_path, fe_perm)
  420. # Write backend information into the location that frontend looks for
  421. vmm.xs.write(trans, "%s/backend-id" % fe_path, str(backend_vm_xid))
  422. vmm.xs.write(trans, "%s/backend" % fe_path, be_path)
  423. # Write frontend information into the location that backend looks for
  424. vmm.xs.write(trans, "%s/frontend-id" % be_path, str(vm_xid))
  425. vmm.xs.write(trans, "%s/frontend" % be_path, fe_path)
  426. # Write USB Spec version field.
  427. vmm.xs.write(trans, "%s/usb-ver" % be_path, usb_ver)
  428. # Write virtual root hub field.
  429. vmm.xs.write(trans, "%s/num-ports" % be_path, str(num_ports))
  430. for port in range(1, num_ports+1):
  431. # Set all port to disconnected state
  432. vmm.xs.write(trans, "%s/port/%d" % (be_path, port), "")
  433. # Set state to XenbusStateInitialising
  434. vmm.xs.write(trans, "%s/state" % fe_path, "1")
  435. vmm.xs.write(trans, "%s/state" % be_path, "1")
  436. vmm.xs.write(trans, "%s/online" % be_path, "1")
  437. vmm.xs.transaction_end(trans)
  438. def usb_decode_device_from_xs(xs_encoded_device):
  439. """ recover actual device name (xenstore doesn't allow dot in key names, so it was translated to underscore) """
  440. return xs_encoded_device.replace('_', '.')
  441. def usb_encode_device_for_xs(device):
  442. """ encode actual device name (xenstore doesn't allow dot in key names, so translated it into underscore) """
  443. return device.replace('.', '_')
  444. def usb_list():
  445. """
  446. Returns a dictionary of USB devices (for PVUSB backends running in all VM).
  447. The dictionary is keyed by 'name' (see below), each element is a dictionary itself:
  448. vm = name of the backend domain
  449. xid = xid of the backend domain
  450. device = <frontend device number>-<frontend port number>
  451. name = <name of backend domain>:<frontend device number>-<frontend port number>
  452. desc = description
  453. """
  454. # FIXME: any better idea of desc_re?
  455. desc_re = re.compile(r"^.{1,255}$")
  456. devices_list = {}
  457. xs_trans = vmm.xs.transaction_start()
  458. vm_list = vmm.xs.ls(xs_trans, '/local/domain')
  459. for xid in vm_list:
  460. vm_name = vmm.xs.read(xs_trans, '/local/domain/%s/name' % xid)
  461. vm_devices = vmm.xs.ls(xs_trans, '/local/domain/%s/qubes-usb-devices' % xid)
  462. if vm_devices is None:
  463. continue
  464. # when listing devices in xenstore we get encoded names
  465. for xs_encoded_device in vm_devices:
  466. # Sanitize device id
  467. if not usb_device_re.match(xs_encoded_device):
  468. print >> sys.stderr, "Invalid device id in backend VM '%s'" % vm_name
  469. continue
  470. device = usb_decode_device_from_xs(xs_encoded_device)
  471. device_desc = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/desc' % (xid, xs_encoded_device))
  472. if not desc_re.match(device_desc):
  473. print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (device, vm_name)
  474. continue
  475. visible_name = "%s:%s" % (vm_name, device)
  476. # grab version
  477. usb_ver = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/usb-ver' % (xid, xs_encoded_device))
  478. if usb_ver is None or not usb_ver_re.match(usb_ver):
  479. print >> sys.stderr, "Invalid %s device USB version in VM '%s'" % (device, vm_name)
  480. continue
  481. devices_list[visible_name] = {"name": visible_name, "xid":int(xid),
  482. "vm": vm_name, "device":device,
  483. "desc":device_desc,
  484. "usb_ver":usb_ver}
  485. vmm.xs.transaction_end(xs_trans)
  486. return devices_list
  487. def usb_check_attached(xs_trans, backend_vm, device):
  488. """
  489. Checks if the given device in the given backend attached to any frontend.
  490. Parameters:
  491. backend_vm - xid of the backend domain
  492. device - device name in the backend domain
  493. Returns None or a dictionary:
  494. vm - the name of the frontend domain
  495. xid - xid of the frontend domain
  496. frontend - frontend device number FIXME
  497. devid - frontend port number FIXME
  498. """
  499. # sample xs content: /local/domain/0/backend/vusb/4/0/port/1 = "7-5"
  500. attached_dev = None
  501. vms = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb' % backend_vm)
  502. if vms is None:
  503. return None
  504. for vm in vms:
  505. if not vm.isdigit():
  506. print >> sys.stderr, "Invalid VM id"
  507. continue
  508. frontend_devs = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%s' % (backend_vm, vm))
  509. if frontend_devs is None:
  510. continue
  511. for frontend_dev in frontend_devs:
  512. if not frontend_dev.isdigit():
  513. print >> sys.stderr, "Invalid frontend in VM %s" % vm
  514. continue
  515. ports = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%s/%s/port' % (backend_vm, vm, frontend_dev))
  516. if ports is None:
  517. continue
  518. for port in ports:
  519. # FIXME: refactor, see similar loop in usb_find_unused_frontend(), use usb_list() instead?
  520. if not port.isdigit():
  521. print >> sys.stderr, "Invalid port in VM %s frontend %s" % (vm, frontend)
  522. continue
  523. dev = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%s/%s/port/%s' % (backend_vm, vm, frontend_dev, port))
  524. if dev == "":
  525. continue
  526. # Sanitize device id
  527. if not usb_port_re.match(dev):
  528. print >> sys.stderr, "Invalid device id in backend VM %d @ %s/%s/port/%s" % \
  529. (backend_vm, vm, frontend_dev, port)
  530. continue
  531. if dev == device:
  532. frontend = "%s-%s" % (frontend_dev, port)
  533. #TODO
  534. vm_name = xl_ctx.domid_to_name(int(vm))
  535. if vm_name is None:
  536. # FIXME: should we wipe references to frontends running on nonexistent VMs?
  537. continue
  538. attached_dev = {"xid":int(vm), "frontend": frontend, "devid": device, "vm": vm_name}
  539. break
  540. return attached_dev
  541. #def usb_check_frontend_busy(vm, front_dev, port):
  542. # devport = frontend.split("-")
  543. # if len(devport) != 2:
  544. # raise QubesException("Malformed frontend syntax, must be in device-port format")
  545. # # FIXME:
  546. # # return vmm.xs.read('', '/local/domain/%d/device/vusb/%d/state' % (vm.xid, frontend)) == '4'
  547. # return False
  548. def usb_find_unused_frontend(xs_trans, backend_vm_xid, vm_xid, usb_ver):
  549. """
  550. Find an unused frontend/port to link the given backend with the given frontend.
  551. Creates new frontend if needed.
  552. Returns frontend specification in <device>-<port> format.
  553. """
  554. # This variable holds an index of last frontend scanned by the loop below.
  555. # If nothing found, this value will be used to derive the index of a new frontend.
  556. last_frontend_dev = -1
  557. frontend_devs = vmm.xs.ls(xs_trans, "/local/domain/%d/device/vusb" % vm_xid)
  558. if frontend_devs is not None:
  559. for frontend_dev in frontend_devs:
  560. if not frontend_dev.isdigit():
  561. print >> sys.stderr, "Invalid frontend_dev in VM %d" % vm_xid
  562. continue
  563. frontend_dev = int(frontend_dev)
  564. fe_path = "/local/domain/%d/device/vusb/%d" % (vm_xid, frontend_dev)
  565. if vmm.xs.read(xs_trans, "%s/backend-id" % fe_path) == str(backend_vm_xid):
  566. if vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%d/%d/usb-ver' % (backend_vm_xid, vm_xid, frontend_dev)) != usb_ver:
  567. last_frontend_dev = frontend_dev
  568. continue
  569. # here: found an existing frontend already connected to right backend using an appropriate USB version
  570. ports = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%d/%d/port' % (backend_vm_xid, vm_xid, frontend_dev))
  571. if ports is None:
  572. print >> sys.stderr, "No ports in VM %d frontend_dev %d?" % (vm_xid, frontend_dev)
  573. last_frontend_dev = frontend_dev
  574. continue
  575. for port in ports:
  576. # FIXME: refactor, see similar loop in usb_check_attached(), use usb_list() instead?
  577. if not port.isdigit():
  578. print >> sys.stderr, "Invalid port in VM %d frontend_dev %d" % (vm_xid, frontend_dev)
  579. continue
  580. port = int(port)
  581. dev = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%d/%s/port/%s' % (backend_vm_xid, vm_xid, frontend_dev, port))
  582. # Sanitize device id
  583. if not usb_port_re.match(dev):
  584. print >> sys.stderr, "Invalid device id in backend VM %d @ %d/%d/port/%d" % \
  585. (backend_vm_xid, vm_xid, frontend_dev, port)
  586. continue
  587. if dev == "":
  588. return '%d-%d' % (frontend_dev, port)
  589. last_frontend_dev = frontend_dev
  590. # create a new frontend_dev and link it to the backend
  591. frontend_dev = last_frontend_dev + 1
  592. usb_setup(backend_vm_xid, vm_xid, frontend_dev, usb_ver)
  593. return '%d-%d' % (frontend_dev, 1)
  594. def usb_attach(vm, backend_vm, device, frontend=None, auto_detach=False, wait=True):
  595. device_attach_check(vm, backend_vm, device, frontend)
  596. xs_trans = vmm.xs.transaction_start()
  597. xs_encoded_device = usb_encode_device_for_xs(device)
  598. usb_ver = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/usb-ver' % (backend_vm.xid, xs_encoded_device))
  599. if usb_ver is None or not usb_ver_re.match(usb_ver):
  600. vmm.xs.transaction_end(xs_trans)
  601. raise QubesException("Invalid %s device USB version in VM '%s'" % (device, backend_vm.name))
  602. if frontend is None:
  603. frontend = usb_find_unused_frontend(xs_trans, backend_vm.xid, vm.xid, usb_ver)
  604. else:
  605. # Check if any device attached at this frontend
  606. #if usb_check_frontend_busy(vm, frontend):
  607. # raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
  608. vmm.xs.transaction_end(xs_trans)
  609. raise NotImplementedError("Explicit USB frontend specification is not implemented yet")
  610. # Check if this device is attached to some domain
  611. attached_vm = usb_check_attached(xs_trans, backend_vm.xid, device)
  612. vmm.xs.transaction_end(xs_trans)
  613. if attached_vm:
  614. if auto_detach:
  615. usb_detach(backend_vm, attached_vm)
  616. else:
  617. raise QubesException("Device %s from %s already connected to VM %s as %s" % (device, backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
  618. # Run helper script
  619. xl_cmd = [ '/usr/lib/qubes/xl-qvm-usb-attach.py', str(vm.xid), device, frontend, str(backend_vm.xid) ]
  620. subprocess.check_call(xl_cmd)
  621. def usb_detach(backend_vm, attachment):
  622. xl_cmd = [ '/usr/lib/qubes/xl-qvm-usb-detach.py', str(attachment['xid']), attachment['devid'], attachment['frontend'], str(backend_vm.xid) ]
  623. subprocess.check_call(xl_cmd)
  624. def usb_detach_all(vm):
  625. raise NotImplementedError("Detaching all devices from a given VM is not implemented yet")
  626. ####### QubesWatch ######
  627. def only_in_first_list(l1, l2):
  628. ret=[]
  629. for i in l1:
  630. if not i in l2:
  631. ret.append(i)
  632. return ret
  633. class QubesWatch(object):
  634. def __init__(self):
  635. self._qdb = {}
  636. self._qdb_events = {}
  637. self.block_callback = None
  638. self.meminfo_callback = None
  639. self.domain_callback = None
  640. vmm.libvirt_conn.domainEventRegisterAny(
  641. None,
  642. libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
  643. self._domain_list_changed, None)
  644. vmm.libvirt_conn.domainEventRegisterAny(
  645. None,
  646. libvirt.VIR_DOMAIN_EVENT_ID_DEVICE_REMOVED,
  647. self._device_removed, None)
  648. # TODO: device attach libvirt event
  649. for vm in vmm.libvirt_conn.listAllDomains():
  650. try:
  651. if vm.isActive():
  652. self._register_watches(vm)
  653. except libvirt.libvirtError:
  654. # this will happen if we loose a race with another tool,
  655. # which can just remove the domain
  656. if vmm.libvirt_conn.virConnGetLastError()[0] == libvirt.VIR_ERR_NO_DOMAIN:
  657. pass
  658. raise
  659. # and for dom0
  660. self._register_watches(None)
  661. def _qdb_handler(self, watch, fd, events, domain_name):
  662. try:
  663. path = self._qdb[domain_name].read_watch()
  664. except DisconnectedError:
  665. libvirt.virEventRemoveHandle(watch)
  666. del(self._qdb_events[domain_name])
  667. self._qdb[domain_name].close()
  668. del(self._qdb[domain_name])
  669. return
  670. if path.startswith('/qubes-block-devices'):
  671. if self.block_callback is not None:
  672. self.block_callback(domain_name)
  673. def setup_block_watch(self, callback):
  674. self.block_callback = callback
  675. def setup_meminfo_watch(self, callback):
  676. raise NotImplementedError
  677. def setup_domain_watch(self, callback):
  678. self.domain_callback = callback
  679. def get_meminfo_key(self, xid):
  680. return '/local/domain/%s/memory/meminfo' % xid
  681. def _register_watches(self, libvirt_domain):
  682. if libvirt_domain:
  683. name = libvirt_domain.name()
  684. if name in self._qdb:
  685. return
  686. # open separate connection to Qubes DB:
  687. # 1. to not confuse pull() with responses to real commands sent from
  688. # other threads (like read, write etc) with watch events
  689. # 2. to not think whether QubesDB is thread-safe (it isn't)
  690. while libvirt_domain.isActive() and name not in self._qdb:
  691. try:
  692. self._qdb[name] = QubesDB(name)
  693. except Error as e:
  694. if e.args[0] != 2:
  695. raise
  696. time.sleep(0.5)
  697. if name not in self._qdb:
  698. # domain no longer active
  699. return
  700. else:
  701. name = "dom0"
  702. self._qdb[name] = QubesDB(name)
  703. self._qdb[name].watch('/qubes-block-devices')
  704. self._qdb_events[name] = libvirt.virEventAddHandle(
  705. self._qdb[name].watch_fd(),
  706. libvirt.VIR_EVENT_HANDLE_READABLE,
  707. self._qdb_handler, name)
  708. def _unregister_watches(self, libvirt_domain):
  709. name = libvirt_domain.name()
  710. if name in self._qdb_events:
  711. libvirt.virEventRemoveHandle(self._qdb_events[name])
  712. del(self._qdb_events[name])
  713. if name in self._qdb:
  714. self._qdb[name].close()
  715. del(self._qdb[name])
  716. def _domain_list_changed(self, conn, domain, event, reason, param):
  717. if event == libvirt.VIR_DOMAIN_EVENT_STARTED:
  718. self._register_watches(domain)
  719. elif event == libvirt.VIR_DOMAIN_EVENT_STOPPED:
  720. self._unregister_watches(domain)
  721. else:
  722. # ignore other events for now
  723. return None
  724. if self.domain_callback:
  725. self.domain_callback(name=domain.name(), uuid=domain.UUID())
  726. def _device_removed(self, conn, domain, device, param):
  727. if self.block_callback is not None:
  728. self.block_callback(domain.name())
  729. def watch_loop(self):
  730. while True:
  731. libvirt.virEventRunDefaultImpl()
  732. ##### updates check #####
  733. UPDATES_DOM0_DISABLE_FLAG='/var/lib/qubes/updates/disable-updates'
  734. UPDATES_DEFAULT_VM_DISABLE_FLAG=\
  735. '/var/lib/qubes/updates/vm-default-disable-updates'
  736. def updates_vms_toggle(qvm_collection, value):
  737. # Flag for new VMs
  738. if value:
  739. if os.path.exists(UPDATES_DEFAULT_VM_DISABLE_FLAG):
  740. os.unlink(UPDATES_DEFAULT_VM_DISABLE_FLAG)
  741. else:
  742. open(UPDATES_DEFAULT_VM_DISABLE_FLAG, "w").close()
  743. # Change for existing VMs
  744. for vm in qvm_collection.values():
  745. if vm.qid == 0:
  746. continue
  747. if value:
  748. vm.services.pop('qubes-update-check', None)
  749. if vm.is_running():
  750. try:
  751. vm.run("systemctl start qubes-update-check.timer",
  752. user="root")
  753. except:
  754. pass
  755. else:
  756. vm.services['qubes-update-check'] = False
  757. if vm.is_running():
  758. try:
  759. vm.run("systemctl stop qubes-update-check.timer",
  760. user="root")
  761. except:
  762. pass
  763. def updates_dom0_toggle(qvm_collection, value):
  764. if value:
  765. if os.path.exists(UPDATES_DOM0_DISABLE_FLAG):
  766. os.unlink(UPDATES_DOM0_DISABLE_FLAG)
  767. else:
  768. open(UPDATES_DOM0_DISABLE_FLAG, "w").close()
  769. def updates_dom0_status(qvm_collection):
  770. return not os.path.exists(UPDATES_DOM0_DISABLE_FLAG)
  771. def updates_vms_status(qvm_collection):
  772. # default value:
  773. status = not os.path.exists(UPDATES_DEFAULT_VM_DISABLE_FLAG)
  774. # check if all the VMs uses the default value
  775. for vm in qvm_collection.values():
  776. if vm.qid == 0:
  777. continue
  778. if vm.services.get('qubes-update-check', True) != status:
  779. # "mixed"
  780. return None
  781. return status
  782. # vim:sw=4:et: