__init__.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2010-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2011-2015 Marek Marczykowski-Górecki
  8. # <marmarek@invisiblethingslab.com>
  9. # Copyright (C) 2014-2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  10. #
  11. # This program is free software; you can redistribute it and/or modify
  12. # it under the terms of the GNU General Public License as published by
  13. # the Free Software Foundation; either version 2 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License along
  22. # with this program; if not, write to the Free Software Foundation, Inc.,
  23. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  24. #
  25. '''Qubes Virtual Machines
  26. '''
  27. import ast
  28. import collections
  29. import datetime
  30. import functools
  31. import itertools
  32. import os
  33. import re
  34. import subprocess
  35. import sys
  36. import xml.parsers.expat
  37. import lxml.etree
  38. import qubes
  39. import qubes.log
  40. import qubes.events
  41. import qubes.tools.qvm_ls
  42. class Features(dict):
  43. '''Manager of the features.
  44. This class inherits from dict, but has most of the methods that manipulate
  45. the item disarmed (they raise NotImplementedError). The ones that are left
  46. fire appropriate events on the qube that owns an instance of this class.
  47. '''
  48. #
  49. # Those are the methods that affect contents. Either disarm them or make
  50. # them report appropriate events. Good approach is to rewrite them carefully
  51. # using official documentation, but use only our (overloaded) methods.
  52. #
  53. def __init__(self, vm, other=None, **kwargs):
  54. super(Features, self).__init__()
  55. self.vm = vm
  56. self.update(other, **kwargs)
  57. def __delitem__(self, key):
  58. super(Features, self).__delitem__(key)
  59. self.vm.fire_event('domain-feature-delete', key)
  60. def __setitem__(self, key, value):
  61. self.vm.fire_event('domain-feature-set', key, value)
  62. super(Features, self).__setitem__(key, value)
  63. def clear(self):
  64. for key in self:
  65. del self[key]
  66. def pop(self):
  67. '''Not implemented
  68. :raises: NotImplementedError
  69. '''
  70. raise NotImplementedError()
  71. def popitem(self):
  72. '''Not implemented
  73. :raises: NotImplementedError
  74. '''
  75. raise NotImplementedError()
  76. def setdefault(self):
  77. '''Not implemented
  78. :raises: NotImplementedError
  79. '''
  80. raise NotImplementedError()
  81. def update(self, other=None, **kwargs):
  82. if other is not None:
  83. if hasattr(other, 'keys'):
  84. for key in other:
  85. self[key] = other[key]
  86. else:
  87. for key, value in other:
  88. self[key] = value
  89. for key in kwargs:
  90. self[key] = kwargs[key]
  91. #
  92. # end of overriding
  93. #
  94. _NO_DEFAULT = object()
  95. def check_with_template(self, feature, default=_NO_DEFAULT):
  96. if feature in self:
  97. return self[feature]
  98. if hasattr(self.vm, 'template') and self.vm.template is not None \
  99. and feature in self.vm.template.features:
  100. return self.vm.template.features[feature]
  101. if default is self._NO_DEFAULT:
  102. raise KeyError(feature)
  103. return default
  104. class BaseVMMeta(qubes.events.EmitterMeta):
  105. '''Metaclass for :py:class:`.BaseVM`'''
  106. def __init__(cls, name, bases, dict_):
  107. super(BaseVMMeta, cls).__init__(name, bases, dict_)
  108. qubes.tools.qvm_ls.process_class(cls)
  109. class DeviceCollection(object):
  110. '''Bag for devices.
  111. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  112. :param vm: VM for which we manage devices
  113. :param class_: device class
  114. '''
  115. def __init__(self, vm, class_):
  116. self._vm = vm
  117. self._class = class_
  118. self._set = set()
  119. def attach(self, device):
  120. '''Attach (add) device to domain.
  121. :param str device: device identifier (format is class-dependent)
  122. '''
  123. if device in self:
  124. raise KeyError(
  125. 'device {!r} of class {} already attached to {!r}'.format(
  126. device, self._class, self._vm))
  127. self._vm.fire_event_pre('device-pre-attached:{}'.format(self._class),
  128. device)
  129. self._set.add(device)
  130. self._vm.fire_event('device-attached:{}'.format(self._class), device)
  131. def detach(self, device):
  132. '''Detach (remove) device from domain.
  133. :param str device: device identifier (format is class-dependent)
  134. '''
  135. if device not in self:
  136. raise KeyError(
  137. 'device {!r} of class {} not attached to {!r}'.format(
  138. device, self._class, self._vm))
  139. self._vm.fire_event_pre('device-pre-detached:{}'.format(self._class),
  140. device)
  141. self._set.remove(device)
  142. self._vm.fire_event('device-detached:{}'.format(self._class), device)
  143. def __iter__(self):
  144. return iter(self._set)
  145. def __contains__(self, item):
  146. return item in self._set
  147. def __len__(self):
  148. return len(self._set)
  149. class DeviceManager(dict):
  150. '''Device manager that hold all devices by their classess.
  151. :param vm: VM for which we manage devices
  152. '''
  153. def __init__(self, vm):
  154. super(DeviceManager, self).__init__()
  155. self._vm = vm
  156. def __missing__(self, key):
  157. self[key] = DeviceCollection(self._vm, key)
  158. return self[key]
  159. class BaseVM(qubes.PropertyHolder):
  160. '''Base class for all VMs
  161. :param app: Qubes application context
  162. :type app: :py:class:`qubes.Qubes`
  163. :param xml: xml node from which to deserialise
  164. :type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`
  165. This class is responsible for serializing and deserialising machines and
  166. provides basic framework. It contains no management logic. For that, see
  167. :py:class:`qubes.vm.qubesvm.QubesVM`.
  168. '''
  169. # pylint: disable=no-member
  170. __metaclass__ = BaseVMMeta
  171. def __init__(self, app, xml, features=None, devices=None, tags=None,
  172. **kwargs):
  173. # pylint: disable=redefined-outer-name
  174. # self.app must be set before super().__init__, because some property
  175. # setters need working .app attribute
  176. #: mother :py:class:`qubes.Qubes` object
  177. self.app = app
  178. super(BaseVM, self).__init__(xml, **kwargs)
  179. #: dictionary of features of this qube
  180. self.features = Features(self, features)
  181. #: :py:class:`DeviceManager` object keeping devices that are attached to
  182. #: this domain
  183. self.devices = DeviceManager(self) if devices is None else devices
  184. #: user-specified tags
  185. self.tags = tags or {}
  186. if self.xml is not None:
  187. # features
  188. for node in xml.xpath('./features/service'):
  189. self.features[node.get('name')] = node.text
  190. # devices (pci, usb, ...)
  191. for parent in xml.xpath('./devices'):
  192. devclass = parent.get('class')
  193. for node in parent.xpath('./device'):
  194. self.devices[devclass].attach(node.text)
  195. # tags
  196. for node in xml.xpath('./tags/tag'):
  197. self.tags[node.get('name')] = node.text
  198. # TODO: firewall, policy
  199. # check if properties are appropriate
  200. all_names = set(prop.__name__ for prop in self.property_list())
  201. for node in self.xml.xpath('./properties/property'):
  202. name = node.get('name')
  203. if not name in all_names:
  204. raise TypeError(
  205. 'property {!r} not applicable to {!r}'.format(
  206. name, self.__class__.__name__))
  207. #: logger instance for logging messages related to this VM
  208. self.log = None
  209. if hasattr(self, 'name'):
  210. self.init_log()
  211. def init_log(self):
  212. '''Initialise logger for this domain.'''
  213. self.log = qubes.log.get_vm_logger(self.name)
  214. def __xml__(self):
  215. element = lxml.etree.Element('domain')
  216. element.set('id', 'domain-' + str(self.qid))
  217. element.set('class', self.__class__.__name__)
  218. element.append(self.xml_properties())
  219. features = lxml.etree.Element('features')
  220. for feature in self.features:
  221. node = lxml.etree.Element('service', name=feature)
  222. node.text = self.features[feature] if feature else None
  223. features.append(node)
  224. element.append(features)
  225. for devclass in self.devices:
  226. devices = lxml.etree.Element('devices')
  227. devices.set('class', devclass)
  228. for device in self.devices[devclass]:
  229. node = lxml.etree.Element('device')
  230. node.text = device
  231. devices.append(node)
  232. element.append(devices)
  233. tags = lxml.etree.Element('tags')
  234. for tag in self.tags:
  235. node = lxml.etree.Element('tag', name=tag)
  236. node.text = self.tags[tag]
  237. tags.append(node)
  238. element.append(tags)
  239. return element
  240. def __repr__(self):
  241. proprepr = []
  242. for prop in self.property_list():
  243. try:
  244. proprepr.append('{}={!r}'.format(
  245. prop.__name__, getattr(self, prop.__name__)))
  246. except AttributeError:
  247. continue
  248. return '<{} object at {:#x} {}>'.format(
  249. self.__class__.__name__, id(self), ' '.join(proprepr))
  250. #
  251. # xml serialising methods
  252. #
  253. @staticmethod
  254. def lvxml_net_dev(ip, mac, backend):
  255. '''Return ``<interface>`` node for libvirt xml.
  256. This was previously _format_net_dev
  257. :param str ip: IP address of the frontend
  258. :param str mac: MAC (Ethernet) address of the frontend
  259. :param qubes.vm.qubesvm.QubesVM backend: Backend domain
  260. :rtype: lxml.etree._Element
  261. '''
  262. interface = lxml.etree.Element('interface', type='ethernet')
  263. interface.append(lxml.etree.Element('mac', address=mac))
  264. interface.append(lxml.etree.Element('ip', address=ip))
  265. interface.append(lxml.etree.Element('backenddomain', name=backend.name))
  266. interface.append(lxml.etree.Element('script', path="vif-route-qubes"))
  267. return interface
  268. @staticmethod
  269. def lvxml_pci_dev(address):
  270. '''Return ``<hostdev>`` node for libvirt xml.
  271. This was previously _format_pci_dev
  272. :param str ip: IP address of the frontend
  273. :param str mac: MAC (Ethernet) address of the frontend
  274. :param qubes.vm.qubesvm.QubesVM backend: Backend domain
  275. :rtype: lxml.etree._Element
  276. '''
  277. dev_match = re.match(r'([0-9a-f]+):([0-9a-f]+)\.([0-9a-f]+)', address)
  278. if not dev_match:
  279. raise ValueError('Invalid PCI device address: {!r}'.format(address))
  280. hostdev = lxml.etree.Element('hostdev', type='pci', managed='yes')
  281. source = lxml.etree.Element('source')
  282. source.append(lxml.etree.Element('address',
  283. bus='0x' + dev_match.group(1),
  284. slot='0x' + dev_match.group(2),
  285. function='0x' + dev_match.group(3)))
  286. hostdev.append(source)
  287. return hostdev
  288. #
  289. # old libvirt XML
  290. # TODO rewrite it to do proper XML synthesis via lxml.etree
  291. #
  292. def get_config_params(self):
  293. '''Return parameters for libvirt's XML domain config
  294. .. deprecated:: 3.0-alpha This will go away.
  295. '''
  296. args = {}
  297. args['name'] = self.name
  298. args['uuid'] = str(self.uuid)
  299. args['vmdir'] = self.dir_path
  300. args['pcidevs'] = ''.join(lxml.etree.tostring(self.lvxml_pci_dev(dev))
  301. for dev in self.devices['pci'])
  302. args['maxmem'] = str(self.maxmem)
  303. args['vcpus'] = str(self.vcpus)
  304. args['mem'] = str(min(self.memory, self.maxmem))
  305. # If dynamic memory management disabled, set maxmem=mem
  306. if not self.features.get('meminfo-writer', True):
  307. args['maxmem'] = args['mem']
  308. if self.netvm is not None:
  309. args['ip'] = self.ip
  310. args['mac'] = self.mac
  311. args['gateway'] = self.netvm.gateway
  312. for i, addr in zip(itertools.count(start=1), self.dns):
  313. args['dns{}'.format(i)] = addr
  314. args['netmask'] = self.netmask
  315. args['netdev'] = lxml.etree.tostring(
  316. self.lvxml_net_dev(self.ip, self.mac, self.netvm))
  317. args['network_begin'] = ''
  318. args['network_end'] = ''
  319. args['no_network_begin'] = '<!--'
  320. args['no_network_end'] = '-->'
  321. else:
  322. args['ip'] = ''
  323. args['mac'] = ''
  324. args['gateway'] = ''
  325. args['dns1'] = ''
  326. args['dns2'] = ''
  327. args['netmask'] = ''
  328. args['netdev'] = ''
  329. args['network_begin'] = '<!--'
  330. args['network_end'] = '-->'
  331. args['no_network_begin'] = ''
  332. args['no_network_end'] = ''
  333. args.update(self.storage.get_config_params())
  334. if hasattr(self, 'kernelopts'):
  335. args['kernelopts'] = self.kernelopts
  336. if self.debug:
  337. self.log.info(
  338. "Debug mode: adding 'earlyprintk=xen' to kernel opts")
  339. args['kernelopts'] += ' earlyprintk=xen'
  340. return args
  341. def create_config_file(self, file_path=None, prepare_dvm=False):
  342. '''Create libvirt's XML domain config file
  343. If :py:attr:`qubes.vm.qubesvm.QubesVM.uses_custom_config` is true, this
  344. does nothing.
  345. :param str file_path: Path to file to create \
  346. (default: :py:attr:`qubes.vm.qubesvm.QubesVM.conf_file`)
  347. :param bool prepare_dvm: If we are in the process of preparing \
  348. DisposableVM
  349. '''
  350. if file_path is None:
  351. file_path = self.conf_file
  352. # TODO
  353. # if self.uses_custom_config:
  354. # conf_appvm = open(file_path, "r")
  355. # domain_config = conf_appvm.read()
  356. # conf_appvm.close()
  357. # return domain_config
  358. f_conf_template = open(self.config_file_template, 'r')
  359. conf_template = f_conf_template.read()
  360. f_conf_template.close()
  361. template_params = self.get_config_params()
  362. if prepare_dvm:
  363. template_params['name'] = '%NAME%'
  364. template_params['privatedev'] = ''
  365. template_params['netdev'] = re.sub(r"address='[0-9.]*'",
  366. "address='%IP%'", template_params['netdev'])
  367. domain_config = conf_template.format(**template_params)
  368. # FIXME: This is only for debugging purposes
  369. old_umask = os.umask(002)
  370. try:
  371. conf_appvm = open(file_path, "w")
  372. conf_appvm.write(domain_config)
  373. conf_appvm.close()
  374. except: # pylint: disable=bare-except
  375. # Ignore errors
  376. pass
  377. finally:
  378. os.umask(old_umask)
  379. return domain_config
  380. #
  381. # firewall
  382. # TODO rewrite it, have <firewall/> node under <domain/>
  383. # and possibly integrate with generic policy framework
  384. #
  385. def write_firewall_conf(self, conf):
  386. '''Write firewall config file.
  387. '''
  388. defaults = self.get_firewall_conf()
  389. expiring_rules_present = False
  390. for item in defaults.keys():
  391. if item not in conf:
  392. conf[item] = defaults[item]
  393. root = lxml.etree.Element(
  394. "QubesFirewallRules",
  395. policy=("allow" if conf["allow"] else "deny"),
  396. dns=("allow" if conf["allowDns"] else "deny"),
  397. icmp=("allow" if conf["allowIcmp"] else "deny"),
  398. yumProxy=("allow" if conf["allowYumProxy"] else "deny"))
  399. for rule in conf["rules"]:
  400. # For backward compatibility
  401. if "proto" not in rule:
  402. if rule["portBegin"] is not None and rule["portBegin"] > 0:
  403. rule["proto"] = "tcp"
  404. else:
  405. rule["proto"] = "any"
  406. element = lxml.etree.Element(
  407. "rule",
  408. address=rule["address"],
  409. proto=str(rule["proto"]),
  410. )
  411. if rule["netmask"] is not None and rule["netmask"] != 32:
  412. element.set("netmask", str(rule["netmask"]))
  413. if rule.get("portBegin", None) is not None and \
  414. rule["portBegin"] > 0:
  415. element.set("port", str(rule["portBegin"]))
  416. if rule.get("portEnd", None) is not None and rule["portEnd"] > 0:
  417. element.set("toport", str(rule["portEnd"]))
  418. if "expire" in rule:
  419. element.set("expire", str(rule["expire"]))
  420. expiring_rules_present = True
  421. root.append(element)
  422. tree = lxml.etree.ElementTree(root)
  423. try:
  424. old_umask = os.umask(002)
  425. with open(self.firewall_conf, 'w') as fd:
  426. tree.write(fd, encoding="UTF-8", pretty_print=True)
  427. fd.close()
  428. os.umask(old_umask)
  429. except EnvironmentError as err: # pylint: disable=broad-except
  430. print >> sys.stderr, "{0}: save error: {1}".format(
  431. os.path.basename(sys.argv[0]), err)
  432. return False
  433. # Automatically enable/disable 'yum-proxy-setup' service based on
  434. # allowYumProxy
  435. if conf['allowYumProxy']:
  436. self.features['yum-proxy-setup'] = '1'
  437. else:
  438. try:
  439. del self.features['yum-proxy-setup']
  440. except KeyError:
  441. pass
  442. if expiring_rules_present:
  443. subprocess.call(["sudo", "systemctl", "start",
  444. "qubes-reload-firewall@%s.timer" % self.name])
  445. return True
  446. def has_firewall(self):
  447. return os.path.exists(self.firewall_conf)
  448. @staticmethod
  449. def get_firewall_defaults():
  450. return {
  451. 'rules': list(),
  452. 'allow': True,
  453. 'allowDns': True,
  454. 'allowIcmp': True,
  455. 'allowYumProxy': False}
  456. def get_firewall_conf(self):
  457. conf = self.get_firewall_defaults()
  458. try:
  459. tree = lxml.etree.parse(self.firewall_conf)
  460. root = tree.getroot()
  461. conf["allow"] = (root.get("policy") == "allow")
  462. conf["allowDns"] = (root.get("dns") == "allow")
  463. conf["allowIcmp"] = (root.get("icmp") == "allow")
  464. conf["allowYumProxy"] = (root.get("yumProxy") == "allow")
  465. for element in root:
  466. rule = {}
  467. attr_list = ("address", "netmask", "proto", "port", "toport",
  468. "expire")
  469. for attribute in attr_list:
  470. rule[attribute] = element.get(attribute)
  471. if rule["netmask"] is not None:
  472. rule["netmask"] = int(rule["netmask"])
  473. else:
  474. rule["netmask"] = 32
  475. if rule["port"] is not None:
  476. rule["portBegin"] = int(rule["port"])
  477. else:
  478. # backward compatibility
  479. rule["portBegin"] = 0
  480. # For backward compatibility
  481. if rule["proto"] is None:
  482. if rule["portBegin"] > 0:
  483. rule["proto"] = "tcp"
  484. else:
  485. rule["proto"] = "any"
  486. if rule["toport"] is not None:
  487. rule["portEnd"] = int(rule["toport"])
  488. else:
  489. rule["portEnd"] = None
  490. if rule["expire"] is not None:
  491. rule["expire"] = int(rule["expire"])
  492. if rule["expire"] <= int(datetime.datetime.now().strftime(
  493. "%s")):
  494. continue
  495. else:
  496. del rule["expire"]
  497. del rule["port"]
  498. del rule["toport"]
  499. conf["rules"].append(rule)
  500. except EnvironmentError as err: # pylint: disable=broad-except
  501. # problem accessing file, like ENOTFOUND, EPERM or sth
  502. # return default config
  503. return conf
  504. except (xml.parsers.expat.ExpatError,
  505. ValueError, LookupError) as err:
  506. # config is invalid
  507. print("{0}: load error: {1}".format(
  508. os.path.basename(sys.argv[0]), err))
  509. return None
  510. return conf