__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.devices
  40. import qubes.events
  41. import qubes.log
  42. import qubes.tools.qvm_ls
  43. class Features(dict):
  44. '''Manager of the features.
  45. Features can have three distinct values: no value (not present in mapping,
  46. which is closest thing to :py:obj:`None`), empty string (which is
  47. interpreted as :py:obj:`False`) and non-empty string, which is
  48. :py:obj:`True`. Anything assigned to the mapping is coerced to strings,
  49. however if you assign instances of :py:class:`bool`, they are converted as
  50. described above. Be aware that assigning the number `0` (which is considered
  51. false in Python) will result in string `'0'`, which is considered true.
  52. This class inherits from dict, but has most of the methods that manipulate
  53. the item disarmed (they raise NotImplementedError). The ones that are left
  54. fire appropriate events on the qube that owns an instance of this class.
  55. '''
  56. #
  57. # Those are the methods that affect contents. Either disarm them or make
  58. # them report appropriate events. Good approach is to rewrite them carefully
  59. # using official documentation, but use only our (overloaded) methods.
  60. #
  61. def __init__(self, vm, other=None, **kwargs):
  62. super(Features, self).__init__()
  63. self.vm = vm
  64. self.update(other, **kwargs)
  65. def __delitem__(self, key):
  66. super(Features, self).__delitem__(key)
  67. self.vm.fire_event('domain-feature-delete', key)
  68. def __setitem__(self, key, value):
  69. if value is None or isinstance(value, bool):
  70. value = '1' if value else ''
  71. else:
  72. value = str(value)
  73. self.vm.fire_event('domain-feature-set', key, value)
  74. super(Features, self).__setitem__(key, value)
  75. def clear(self):
  76. for key in self:
  77. del self[key]
  78. def pop(self):
  79. '''Not implemented
  80. :raises: NotImplementedError
  81. '''
  82. raise NotImplementedError()
  83. def popitem(self):
  84. '''Not implemented
  85. :raises: NotImplementedError
  86. '''
  87. raise NotImplementedError()
  88. def setdefault(self):
  89. '''Not implemented
  90. :raises: NotImplementedError
  91. '''
  92. raise NotImplementedError()
  93. def update(self, other=None, **kwargs):
  94. if other is not None:
  95. if hasattr(other, 'keys'):
  96. for key in other:
  97. self[key] = other[key]
  98. else:
  99. for key, value in other:
  100. self[key] = value
  101. for key in kwargs:
  102. self[key] = kwargs[key]
  103. #
  104. # end of overriding
  105. #
  106. _NO_DEFAULT = object()
  107. def check_with_template(self, feature, default=_NO_DEFAULT):
  108. if feature in self:
  109. return self[feature]
  110. if hasattr(self.vm, 'template') and self.vm.template is not None \
  111. and feature in self.vm.template.features:
  112. return self.vm.template.features[feature]
  113. if default is self._NO_DEFAULT:
  114. raise KeyError(feature)
  115. return default
  116. class BaseVMMeta(qubes.events.EmitterMeta):
  117. '''Metaclass for :py:class:`.BaseVM`'''
  118. def __init__(cls, name, bases, dict_):
  119. super(BaseVMMeta, cls).__init__(name, bases, dict_)
  120. qubes.tools.qvm_ls.process_class(cls)
  121. class BaseVM(qubes.PropertyHolder):
  122. '''Base class for all VMs
  123. :param app: Qubes application context
  124. :type app: :py:class:`qubes.Qubes`
  125. :param xml: xml node from which to deserialise
  126. :type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`
  127. This class is responsible for serializing and deserialising machines and
  128. provides basic framework. It contains no management logic. For that, see
  129. :py:class:`qubes.vm.qubesvm.QubesVM`.
  130. '''
  131. # pylint: disable=no-member
  132. __metaclass__ = BaseVMMeta
  133. def __init__(self, app, xml, features=None, devices=None, tags=None,
  134. **kwargs):
  135. # pylint: disable=redefined-outer-name
  136. # self.app must be set before super().__init__, because some property
  137. # setters need working .app attribute
  138. #: mother :py:class:`qubes.Qubes` object
  139. self.app = app
  140. super(BaseVM, self).__init__(xml, **kwargs)
  141. #: dictionary of features of this qube
  142. self.features = Features(self, features)
  143. #: :py:class:`DeviceManager` object keeping devices that are attached to
  144. #: this domain
  145. self.devices = devices or qubes.devices.DeviceManager(self)
  146. #: user-specified tags
  147. self.tags = tags or {}
  148. if self.xml is not None:
  149. # features
  150. for node in xml.xpath('./features/feature'):
  151. self.features[node.get('name')] = node.text
  152. # devices (pci, usb, ...)
  153. for parent in xml.xpath('./devices'):
  154. devclass = parent.get('class')
  155. for node in parent.xpath('./device'):
  156. self.devices[devclass].attach(node.text)
  157. # tags
  158. for node in xml.xpath('./tags/tag'):
  159. self.tags[node.get('name')] = node.text
  160. # TODO: firewall, policy
  161. # check if properties are appropriate
  162. all_names = set(prop.__name__ for prop in self.property_list())
  163. for node in self.xml.xpath('./properties/property'):
  164. name = node.get('name')
  165. if not name in all_names:
  166. raise TypeError(
  167. 'property {!r} not applicable to {!r}'.format(
  168. name, self.__class__.__name__))
  169. #: logger instance for logging messages related to this VM
  170. self.log = None
  171. if hasattr(self, 'name'):
  172. self.init_log()
  173. def init_log(self):
  174. '''Initialise logger for this domain.'''
  175. self.log = qubes.log.get_vm_logger(self.name)
  176. def __xml__(self):
  177. element = lxml.etree.Element('domain')
  178. element.set('id', 'domain-' + str(self.qid))
  179. element.set('class', self.__class__.__name__)
  180. element.append(self.xml_properties())
  181. features = lxml.etree.Element('features')
  182. for feature in self.features:
  183. node = lxml.etree.Element('feature', name=feature)
  184. node.text = self.features[feature]
  185. features.append(node)
  186. element.append(features)
  187. for devclass in self.devices:
  188. devices = lxml.etree.Element('devices')
  189. devices.set('class', devclass)
  190. for device in self.devices[devclass]:
  191. node = lxml.etree.Element('device')
  192. node.text = device
  193. devices.append(node)
  194. element.append(devices)
  195. tags = lxml.etree.Element('tags')
  196. for tag in self.tags:
  197. node = lxml.etree.Element('tag', name=tag)
  198. node.text = self.tags[tag]
  199. tags.append(node)
  200. element.append(tags)
  201. return element
  202. def __repr__(self):
  203. proprepr = []
  204. for prop in self.property_list():
  205. try:
  206. proprepr.append('{}={!s}'.format(
  207. prop.__name__, getattr(self, prop.__name__)))
  208. except AttributeError:
  209. continue
  210. return '<{} object at {:#x} {}>'.format(
  211. self.__class__.__name__, id(self), ' '.join(proprepr))
  212. #
  213. # xml serialising methods
  214. #
  215. @staticmethod
  216. def lvxml_net_dev(ip, mac, backend):
  217. '''Return ``<interface>`` node for libvirt xml.
  218. This was previously _format_net_dev
  219. :param str ip: IP address of the frontend
  220. :param str mac: MAC (Ethernet) address of the frontend
  221. :param qubes.vm.qubesvm.QubesVM backend: Backend domain
  222. :rtype: lxml.etree._Element
  223. '''
  224. interface = lxml.etree.Element('interface', type='ethernet')
  225. interface.append(lxml.etree.Element('mac', address=mac))
  226. interface.append(lxml.etree.Element('ip', address=ip))
  227. interface.append(lxml.etree.Element('backenddomain', name=backend.name))
  228. interface.append(lxml.etree.Element('script', path="vif-route-qubes"))
  229. return interface
  230. @staticmethod
  231. def lvxml_pci_dev(address):
  232. '''Return ``<hostdev>`` node for libvirt xml.
  233. This was previously _format_pci_dev
  234. :param str ip: IP address of the frontend
  235. :param str mac: MAC (Ethernet) address of the frontend
  236. :param qubes.vm.qubesvm.QubesVM backend: Backend domain
  237. :rtype: lxml.etree._Element
  238. '''
  239. dev_match = re.match(r'([0-9a-f]+):([0-9a-f]+)\.([0-9a-f]+)', address)
  240. if not dev_match:
  241. raise ValueError('Invalid PCI device address: {!r}'.format(address))
  242. hostdev = lxml.etree.Element('hostdev', type='pci', managed='yes')
  243. source = lxml.etree.Element('source')
  244. source.append(lxml.etree.Element('address',
  245. bus='0x' + dev_match.group(1),
  246. slot='0x' + dev_match.group(2),
  247. function='0x' + dev_match.group(3)))
  248. hostdev.append(source)
  249. return hostdev
  250. #
  251. # old libvirt XML
  252. # TODO rewrite it to do proper XML synthesis via lxml.etree
  253. #
  254. def get_config_params(self):
  255. '''Return parameters for libvirt's XML domain config
  256. .. deprecated:: 3.0-alpha This will go away.
  257. '''
  258. args = {}
  259. args['name'] = self.name
  260. args['uuid'] = str(self.uuid)
  261. args['vmdir'] = self.dir_path
  262. args['pcidevs'] = ''.join(lxml.etree.tostring(self.lvxml_pci_dev(dev))
  263. for dev in self.devices['pci'])
  264. args['maxmem'] = str(self.maxmem)
  265. args['vcpus'] = str(self.vcpus)
  266. args['mem'] = str(min(self.memory, self.maxmem))
  267. # If dynamic memory management disabled, set maxmem=mem
  268. if not self.features.get('meminfo-writer', True):
  269. args['maxmem'] = args['mem']
  270. if self.netvm is not None:
  271. args['ip'] = self.ip
  272. args['mac'] = self.mac
  273. args['gateway'] = self.netvm.gateway
  274. for i, addr in zip(itertools.count(start=1), self.dns):
  275. args['dns{}'.format(i)] = addr
  276. args['netmask'] = self.netmask
  277. args['netdev'] = lxml.etree.tostring(
  278. self.lvxml_net_dev(self.ip, self.mac, self.netvm))
  279. args['network_begin'] = ''
  280. args['network_end'] = ''
  281. args['no_network_begin'] = '<!--'
  282. args['no_network_end'] = '-->'
  283. else:
  284. args['ip'] = ''
  285. args['mac'] = ''
  286. args['gateway'] = ''
  287. args['dns1'] = ''
  288. args['dns2'] = ''
  289. args['netmask'] = ''
  290. args['netdev'] = ''
  291. args['network_begin'] = '<!--'
  292. args['network_end'] = '-->'
  293. args['no_network_begin'] = ''
  294. args['no_network_end'] = ''
  295. args.update(self.storage.get_config_params())
  296. if hasattr(self, 'kernelopts'):
  297. args['kernelopts'] = self.kernelopts
  298. if self.debug:
  299. self.log.info(
  300. "Debug mode: adding 'earlyprintk=xen' to kernel opts")
  301. args['kernelopts'] += ' earlyprintk=xen'
  302. return args
  303. def create_config_file(self, file_path=None, prepare_dvm=False):
  304. '''Create libvirt's XML domain config file
  305. If :py:attr:`qubes.vm.qubesvm.QubesVM.uses_custom_config` is true, this
  306. does nothing.
  307. :param str file_path: Path to file to create \
  308. (default: :py:attr:`qubes.vm.qubesvm.QubesVM.conf_file`)
  309. :param bool prepare_dvm: If we are in the process of preparing \
  310. DisposableVM
  311. '''
  312. if file_path is None:
  313. file_path = self.conf_file
  314. # TODO
  315. # if self.uses_custom_config:
  316. # conf_appvm = open(file_path, "r")
  317. # domain_config = conf_appvm.read()
  318. # conf_appvm.close()
  319. # return domain_config
  320. domain_config = self.app.env.get_template('libvirt/xen.xml').render(
  321. vm=self, prepare_dvm=prepare_dvm)
  322. # FIXME: This is only for debugging purposes
  323. old_umask = os.umask(002)
  324. try:
  325. conf_appvm = open(file_path, "w")
  326. conf_appvm.write(domain_config)
  327. conf_appvm.close()
  328. except: # pylint: disable=bare-except
  329. # Ignore errors
  330. pass
  331. finally:
  332. os.umask(old_umask)
  333. return domain_config
  334. #
  335. # firewall
  336. # TODO rewrite it, have <firewall/> node under <domain/>
  337. # and possibly integrate with generic policy framework
  338. #
  339. def write_firewall_conf(self, conf):
  340. '''Write firewall config file.
  341. '''
  342. defaults = self.get_firewall_conf()
  343. expiring_rules_present = False
  344. for item in defaults.keys():
  345. if item not in conf:
  346. conf[item] = defaults[item]
  347. root = lxml.etree.Element(
  348. "QubesFirewallRules",
  349. policy=("allow" if conf["allow"] else "deny"),
  350. dns=("allow" if conf["allowDns"] else "deny"),
  351. icmp=("allow" if conf["allowIcmp"] else "deny"),
  352. yumProxy=("allow" if conf["allowYumProxy"] else "deny"))
  353. for rule in conf["rules"]:
  354. # For backward compatibility
  355. if "proto" not in rule:
  356. if rule["portBegin"] is not None and rule["portBegin"] > 0:
  357. rule["proto"] = "tcp"
  358. else:
  359. rule["proto"] = "any"
  360. element = lxml.etree.Element(
  361. "rule",
  362. address=rule["address"],
  363. proto=str(rule["proto"]),
  364. )
  365. if rule["netmask"] is not None and rule["netmask"] != 32:
  366. element.set("netmask", str(rule["netmask"]))
  367. if rule.get("portBegin", None) is not None and \
  368. rule["portBegin"] > 0:
  369. element.set("port", str(rule["portBegin"]))
  370. if rule.get("portEnd", None) is not None and rule["portEnd"] > 0:
  371. element.set("toport", str(rule["portEnd"]))
  372. if "expire" in rule:
  373. element.set("expire", str(rule["expire"]))
  374. expiring_rules_present = True
  375. root.append(element)
  376. tree = lxml.etree.ElementTree(root)
  377. try:
  378. old_umask = os.umask(002)
  379. with open(os.path.join(self.dir_path,
  380. self.firewall_conf), 'w') as fd:
  381. tree.write(fd, encoding="UTF-8", pretty_print=True)
  382. fd.close()
  383. os.umask(old_umask)
  384. except EnvironmentError as err: # pylint: disable=broad-except
  385. print >> sys.stderr, "{0}: save error: {1}".format(
  386. os.path.basename(sys.argv[0]), err)
  387. return False
  388. # Automatically enable/disable 'updates-proxy-setup' service based on
  389. # allowYumProxy
  390. if conf['allowYumProxy']:
  391. self.features['updates-proxy-setup'] = '1'
  392. else:
  393. try:
  394. del self.features['updates-proxy-setup']
  395. except KeyError:
  396. pass
  397. if expiring_rules_present:
  398. subprocess.call(["sudo", "systemctl", "start",
  399. "qubes-reload-firewall@%s.timer" % self.name])
  400. # XXX any better idea? some arguments?
  401. self.fire_event('firewall-changed')
  402. return True
  403. def has_firewall(self):
  404. return os.path.exists(os.path.join(self.dir_path, self.firewall_conf))
  405. @staticmethod
  406. def get_firewall_defaults():
  407. return {
  408. 'rules': list(),
  409. 'allow': True,
  410. 'allowDns': True,
  411. 'allowIcmp': True,
  412. 'allowYumProxy': False}
  413. def get_firewall_conf(self):
  414. conf = self.get_firewall_defaults()
  415. try:
  416. tree = lxml.etree.parse(os.path.join(self.dir_path,
  417. self.firewall_conf))
  418. root = tree.getroot()
  419. conf["allow"] = (root.get("policy") == "allow")
  420. conf["allowDns"] = (root.get("dns") == "allow")
  421. conf["allowIcmp"] = (root.get("icmp") == "allow")
  422. conf["allowYumProxy"] = (root.get("yumProxy") == "allow")
  423. for element in root:
  424. rule = {}
  425. attr_list = ("address", "netmask", "proto", "port", "toport",
  426. "expire")
  427. for attribute in attr_list:
  428. rule[attribute] = element.get(attribute)
  429. if rule["netmask"] is not None:
  430. rule["netmask"] = int(rule["netmask"])
  431. else:
  432. rule["netmask"] = 32
  433. if rule["port"] is not None:
  434. rule["portBegin"] = int(rule["port"])
  435. else:
  436. # backward compatibility
  437. rule["portBegin"] = 0
  438. # For backward compatibility
  439. if rule["proto"] is None:
  440. if rule["portBegin"] > 0:
  441. rule["proto"] = "tcp"
  442. else:
  443. rule["proto"] = "any"
  444. if rule["toport"] is not None:
  445. rule["portEnd"] = int(rule["toport"])
  446. else:
  447. rule["portEnd"] = None
  448. if rule["expire"] is not None:
  449. rule["expire"] = int(rule["expire"])
  450. if rule["expire"] <= int(datetime.datetime.now().strftime(
  451. "%s")):
  452. continue
  453. else:
  454. del rule["expire"]
  455. del rule["port"]
  456. del rule["toport"]
  457. conf["rules"].append(rule)
  458. except EnvironmentError as err: # pylint: disable=broad-except
  459. # problem accessing file, like ENOTFOUND, EPERM or sth
  460. # return default config
  461. return conf
  462. except (xml.parsers.expat.ExpatError,
  463. ValueError, LookupError) as err:
  464. # config is invalid
  465. print("{0}: load error: {1}".format(
  466. os.path.basename(sys.argv[0]), err))
  467. return None
  468. return conf
  469. class VMProperty(qubes.property):
  470. '''Property that is referring to a VM
  471. :param type vmclass: class that returned VM is supposed to be instance of
  472. and all supported by :py:class:`property` with the exception of ``type`` \
  473. and ``setter``
  474. '''
  475. _none_value = ''
  476. def __init__(self, name, vmclass=BaseVM, allow_none=False,
  477. **kwargs):
  478. if 'type' in kwargs:
  479. raise TypeError(
  480. "'type' keyword parameter is unsupported in {}".format(
  481. self.__class__.__name__))
  482. if 'setter' in kwargs:
  483. raise TypeError(
  484. "'setter' keyword parameter is unsupported in {}".format(
  485. self.__class__.__name__))
  486. if not issubclass(vmclass, BaseVM):
  487. raise TypeError(
  488. "'vmclass' should specify a subclass of qubes.vm.BaseVM")
  489. super(VMProperty, self).__init__(name,
  490. saver=(lambda self_, prop, value:
  491. self._none_value if value is None else value.name),
  492. **kwargs)
  493. self.vmclass = vmclass
  494. self.allow_none = allow_none
  495. def __set__(self, instance, value):
  496. if value is self.__class__.DEFAULT:
  497. self.__delete__(instance)
  498. return
  499. if value == self._none_value:
  500. value = None
  501. if value is None:
  502. if self.allow_none:
  503. super(VMProperty, self).__set__(instance, value)
  504. return
  505. else:
  506. raise ValueError(
  507. 'Property {!r} does not allow setting to {!r}'.format(
  508. self.__name__, value))
  509. app = instance if isinstance(instance, qubes.Qubes) else instance.app
  510. try:
  511. vm = app.domains[value]
  512. except KeyError:
  513. raise qubes.exc.QubesVMNotFoundError(value)
  514. if not isinstance(vm, self.vmclass):
  515. raise TypeError('wrong VM class: domains[{!r}] if of type {!s} '
  516. 'and not {!s}'.format(value,
  517. vm.__class__.__name__,
  518. self.vmclass.__name__))
  519. super(VMProperty, self).__set__(instance, vm)