__init__.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. #!/usr/bin/python2 -O
  2. '''Qubes Virtual Machines
  3. Main public classes
  4. -------------------
  5. .. autoclass:: BaseVM
  6. :members:
  7. :show-inheritance:
  8. Helper classes and functions
  9. ----------------------------
  10. .. autoclass:: BaseVMMeta
  11. :members:
  12. :show-inheritance:
  13. Particular VM classes
  14. ---------------------
  15. Main types:
  16. .. toctree::
  17. :maxdepth: 1
  18. qubesvm
  19. appvm
  20. templatevm
  21. Special VM types:
  22. .. toctree::
  23. :maxdepth: 1
  24. netvm
  25. proxyvm
  26. dispvm
  27. adminvm
  28. HVMs:
  29. .. toctree::
  30. :maxdepth: 1
  31. hvm
  32. templatehvm
  33. '''
  34. import ast
  35. import collections
  36. import functools
  37. import sys
  38. import dateutil.parser
  39. import lxml.etree
  40. import qubes
  41. import qubes.events
  42. import qubes.plugins
  43. class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
  44. '''Metaclass for :py:class:`.BaseVM`'''
  45. def __init__(cls, name, bases, dict_):
  46. super(BaseVMMeta, cls).__init__(name, bases, dict_)
  47. cls.__hooks__ = collections.defaultdict(list)
  48. class DeviceCollection(object):
  49. '''Bag for devices.
  50. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  51. :param vm: VM for which we manage devices
  52. :param class_: device class
  53. '''
  54. def __init__(self, vm, class_):
  55. self._vm = vm
  56. self._class = class_
  57. self._set = set()
  58. def attach(self, device):
  59. '''Attach (add) device to domain.
  60. :param str device: device identifier (format is class-dependent)
  61. '''
  62. if device in self:
  63. raise KeyError(
  64. 'device {!r} of class {} already attached to {!r}'.format(
  65. device, self._class, self._vm))
  66. self._vm.fire_event_pre('device-pre-attached:{}'.format(self._class), device)
  67. self._set.add(device)
  68. self._vm.fire_event('device-attached:{}'.format(self._class), device)
  69. def detach(self, device):
  70. '''Detach (remove) device from domain.
  71. :param str device: device identifier (format is class-dependent)
  72. '''
  73. if device not in self:
  74. raise KeyError(
  75. 'device {!r} of class {} not attached to {!r}'.format(
  76. device, self._class, self._vm))
  77. self._vm.fire_event_pre('device-pre-detached:{}'.format(self._class), device)
  78. self._set.remove(device)
  79. self._vm.fire_event('device-detached:{}'.format(self._class), device)
  80. def __iter__(self):
  81. return iter(self._set)
  82. def __contains__(self, item):
  83. return item in self._set
  84. class DeviceManager(dict):
  85. '''Device manager that hold all devices by their classess.
  86. :param vm: VM for which we manage devices
  87. '''
  88. def __init__(self, vm):
  89. super(DeviceManager, self).__init__()
  90. self._vm = vm
  91. def __missing__(self, key):
  92. return DeviceCollection(self._vm, key)
  93. class BaseVM(qubes.PropertyHolder):
  94. '''Base class for all VMs
  95. :param app: Qubes application context
  96. :type app: :py:class:`qubes.Qubes`
  97. :param xml: xml node from which to deserialise
  98. :type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`
  99. This class is responsible for serialising and deserialising machines and
  100. provides basic framework. It contains no management logic. For that, see
  101. :py:class:`qubes.vm.qubesvm.QubesVM`.
  102. '''
  103. __metaclass__ = BaseVMMeta
  104. def __init__(self, app, xml, load_stage=2, services={}, devices=None,
  105. tags={}, *args, **kwargs):
  106. self.app = app
  107. self.services = services
  108. self.devices = DeviceManager(self) if devices is None else devices
  109. self.tags = tags
  110. self.events_enabled = False
  111. all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2))
  112. for key in list(kwargs.keys()):
  113. if not key in all_names:
  114. raise AttributeError(
  115. 'No property {!r} found in {!r}'.format(
  116. key, self.__class__))
  117. setattr(self, key, kwargs[key])
  118. del kwargs[key]
  119. super(BaseVM, self).__init__(xml, *args, **kwargs)
  120. self.events_enabled = True
  121. self.fire_event('property-load')
  122. def add_new_vm(self, vm):
  123. '''Add new Virtual Machine to colletion
  124. '''
  125. vm_cls = QubesVmClasses[vm_type]
  126. if 'template' in kwargs:
  127. if not vm_cls.is_template_compatible(kwargs['template']):
  128. raise QubesException("Template not compatible with selected "
  129. "VM type")
  130. vm = vm_cls(qid=qid, collection=self, **kwargs)
  131. if not self.verify_new_vm(vm):
  132. raise QubesException("Wrong VM description!")
  133. self[vm.qid] = vm
  134. # make first created NetVM the default one
  135. if self.default_fw_netvm_qid is None and vm.is_netvm():
  136. self.set_default_fw_netvm(vm)
  137. if self.default_netvm_qid is None and vm.is_proxyvm():
  138. self.set_default_netvm(vm)
  139. # make first created TemplateVM the default one
  140. if self.default_template_qid is None and vm.is_template():
  141. self.set_default_template(vm)
  142. # make first created ProxyVM the UpdateVM
  143. if self.updatevm_qid is None and vm.is_proxyvm():
  144. self.set_updatevm_vm(vm)
  145. # by default ClockVM is the first NetVM
  146. if self.clockvm_qid is None and vm.is_netvm():
  147. self.set_clockvm_vm(vm)
  148. return vm
  149. @classmethod
  150. def fromxml(cls, app, xml, load_stage=2):
  151. '''Create VM from XML node
  152. :param qubes.Qubes app: :py:class:`qubes.Qubes` application instance
  153. :param lxml.etree._Element xml: XML node reference
  154. :param int load_stage: do not change the default (2) unless you know, what you are doing
  155. '''
  156. # sys.stderr.write('{}.fromxml(app={!r}, xml={!r}, load_stage={})\n'.format(
  157. # cls.__name__, app, xml, load_stage))
  158. if xml is None:
  159. return cls(app)
  160. services = {}
  161. devices = collections.defaultdict(list)
  162. tags = {}
  163. # services
  164. for node in xml.xpath('./services/service'):
  165. services[node.text] = bool(ast.literal_eval(node.get('enabled', 'True')))
  166. # devices (pci, usb, ...)
  167. for parent in xml.xpath('./devices'):
  168. devclass = parent.get('class')
  169. for node in parent.xpath('./device'):
  170. devices[devclass].append(node.text)
  171. # tags
  172. for node in xml.xpath('./tags/tag'):
  173. tags[node.get('name')] = node.text
  174. # properties
  175. self = cls(app, xml=xml, services=services, devices=devices, tags=tags)
  176. self.load_properties(load_stage=load_stage)
  177. # TODO: firewall, policy
  178. # sys.stderr.write('{}.fromxml return\n'.format(cls.__name__))
  179. return self
  180. def __xml__(self):
  181. element = lxml.etree.Element('domain', id='domain-' + str(self.qid))
  182. element.append(self.save_properties())
  183. services = lxml.etree.Element('services')
  184. for service in self.services:
  185. node = lxml.etree.Element('service')
  186. node.text = service
  187. if not self.services[service]:
  188. node.set('enabled', 'False')
  189. services.append(node)
  190. element.append(services)
  191. for devclass in self.devices:
  192. devices = lxml.etree.Element('devices')
  193. devices.set('class', devclass)
  194. for device in self.devices[devclass]:
  195. node = lxml.etree.Element('device')
  196. node.text = device
  197. devices.append(node)
  198. element.append(devices)
  199. tags = lxml.etree.Element('tags')
  200. for tag in self.tags:
  201. node = lxml.etree.Element('tag', name=tag)
  202. node.text = self.tags[tag]
  203. tags.append(node)
  204. element.append(tags)
  205. return element
  206. def __repr__(self):
  207. proprepr = []
  208. for prop in self.get_props_list():
  209. try:
  210. proprepr.append('{}={!r}'.format(
  211. prop.__name__, getattr(self, prop.__name__)))
  212. except AttributeError:
  213. continue
  214. return '<{} object at {:#x} {}>'.format(
  215. self.__class__.__name__, id(self), ' '.join(proprepr))
  216. #
  217. # xml serialising methods
  218. #
  219. @staticmethod
  220. def xml_net_dev(ip, mac, backend):
  221. '''Return ``<interface>`` node for libvirt xml.
  222. This was previously _format_net_dev
  223. :param str ip: IP address of the frontend
  224. :param str mac: MAC (Ethernet) address of the frontend
  225. :param qubes.vm.QubesVM backend: Backend domain
  226. :rtype: lxml.etree._Element
  227. '''
  228. interface = lxml.etree.Element('interface', type='ethernet')
  229. interface.append(lxml.etree.Element('mac', address=mac))
  230. interface.append(lxml.etree.Element('ip', address=ip))
  231. interface.append(lxml.etree.Element('domain', name=backend.name))
  232. return interface
  233. @staticmethod
  234. def xml_pci_dev(address):
  235. '''Return ``<hostdev>`` node for libvirt xml.
  236. This was previously _format_pci_dev
  237. :param str ip: IP address of the frontend
  238. :param str mac: MAC (Ethernet) address of the frontend
  239. :param qubes.vm.QubesVM backend: Backend domain
  240. :rtype: lxml.etree._Element
  241. '''
  242. dev_match = re.match('([0-9a-f]+):([0-9a-f]+)\.([0-9a-f]+)', address)
  243. if not dev_match:
  244. raise QubesException("Invalid PCI device address: %s" % address)
  245. hostdev = lxml.etree.Element('hostdev', type='pci', managed='yes')
  246. source = lxml.etree.Element('source')
  247. source.append(lxml.etree.Element('address',
  248. bus='0x' + dev_match.group(1),
  249. slot='0x' + dev_match.group(2),
  250. function='0x' + dev_match.group(3)))
  251. hostdev.append(source)
  252. return hostdev
  253. #
  254. # old libvirt XML
  255. # TODO rewrite it to do proper XML synthesis via lxml.etree
  256. #
  257. def get_config_params(self):
  258. '''Return parameters for libvirt's XML domain config
  259. .. deprecated:: 3.0-alpha This will go away.
  260. '''
  261. args = {}
  262. args['name'] = self.name
  263. if hasattr(self, 'kernels_dir'):
  264. args['kerneldir'] = self.kernels_dir
  265. args['uuidnode'] = '<uuid>{!r}</uuid>'.format(self.uuid) \
  266. if hasattr(self, 'uuid') else ''
  267. args['vmdir'] = self.dir_path
  268. args['pcidevs'] = ''.join(lxml.etree.tostring(self.xml_pci_dev(dev))
  269. for dev in self.devices['pci'])
  270. args['maxmem'] = str(self.maxmem)
  271. args['vcpus'] = str(self.vcpus)
  272. args['mem'] = str(max(self.memory, self.maxmem))
  273. if 'meminfo-writer' in self.services and not self.services['meminfo-writer']:
  274. # If dynamic memory management disabled, set maxmem=mem
  275. args['maxmem'] = args['mem']
  276. if self.netvm is not None:
  277. args['ip'] = self.ip
  278. args['mac'] = self.mac
  279. args['gateway'] = self.netvm.gateway
  280. args['dns1'] = self.netvm.gateway
  281. args['dns2'] = self.secondary_dns
  282. args['netmask'] = self.netmask
  283. args['netdev'] = lxml.etree.tostring(self.xml_net_dev(self.ip, self.mac, self.netvm))
  284. args['disable_network1'] = '';
  285. args['disable_network2'] = '';
  286. else:
  287. args['ip'] = ''
  288. args['mac'] = ''
  289. args['gateway'] = ''
  290. args['dns1'] = ''
  291. args['dns2'] = ''
  292. args['netmask'] = ''
  293. args['netdev'] = ''
  294. args['disable_network1'] = '<!--';
  295. args['disable_network2'] = '-->';
  296. args.update(self.storage.get_config_params())
  297. if hasattr(self, 'kernelopts'):
  298. args['kernelopts'] = self.kernelopts
  299. if self.debug:
  300. self.log.info("Debug mode: adding 'earlyprintk=xen' to kernel opts")
  301. args['kernelopts'] += ' earlyprintk=xen'
  302. def create_config_file(self, file_path=None, prepare_dvm=False):
  303. '''Create libvirt's XML domain config file
  304. If :py:attr:`qubes.vm.qubesvm.QubesVM.uses_custom_config` is true, this
  305. does nothing.
  306. :param str file_path: Path to file to create (default: :py:attr:`qubes.vm.qubesvm.QubesVM.conf_file`)
  307. :param bool prepare_dvm: If we are in the process of preparing DisposableVM
  308. '''
  309. if file_path is None:
  310. file_path = self.conf_file
  311. if self.uses_custom_config:
  312. conf_appvm = open(file_path, "r")
  313. domain_config = conf_appvm.read()
  314. conf_appvm.close()
  315. return domain_config
  316. f_conf_template = open(self.config_file_template, 'r')
  317. conf_template = f_conf_template.read()
  318. f_conf_template.close()
  319. template_params = self.get_config_params()
  320. if prepare_dvm:
  321. template_params['name'] = '%NAME%'
  322. template_params['privatedev'] = ''
  323. template_params['netdev'] = re.sub(r"address='[0-9.]*'", "address='%IP%'", template_params['netdev'])
  324. domain_config = conf_template.format(**template_params)
  325. # FIXME: This is only for debugging purposes
  326. old_umask = os.umask(002)
  327. try:
  328. conf_appvm = open(file_path, "w")
  329. conf_appvm.write(domain_config)
  330. conf_appvm.close()
  331. except:
  332. # Ignore errors
  333. pass
  334. finally:
  335. os.umask(old_umask)
  336. return domain_config
  337. #
  338. # firewall
  339. # TODO rewrite it, have <firewall/> node under <domain/>
  340. # and possibly integrate with generic policy framework
  341. #
  342. def write_firewall_conf(self, conf):
  343. '''Write firewall config file.
  344. '''
  345. defaults = self.get_firewall_conf()
  346. expiring_rules_present = False
  347. for item in defaults.keys():
  348. if item not in conf:
  349. conf[item] = defaults[item]
  350. root = lxml.etree.Element(
  351. "QubesFirewallRules",
  352. policy = "allow" if conf["allow"] else "deny",
  353. dns = "allow" if conf["allowDns"] else "deny",
  354. icmp = "allow" if conf["allowIcmp"] else "deny",
  355. yumProxy = "allow" if conf["allowYumProxy"] else "deny"
  356. )
  357. for rule in conf["rules"]:
  358. # For backward compatibility
  359. if "proto" not in rule:
  360. if rule["portBegin"] is not None and rule["portBegin"] > 0:
  361. rule["proto"] = "tcp"
  362. else:
  363. rule["proto"] = "any"
  364. element = lxml.etree.Element(
  365. "rule",
  366. address=rule["address"],
  367. proto=str(rule["proto"]),
  368. )
  369. if rule["netmask"] is not None and rule["netmask"] != 32:
  370. element.set("netmask", str(rule["netmask"]))
  371. if rule.get("portBegin", None) is not None and \
  372. rule["portBegin"] > 0:
  373. element.set("port", str(rule["portBegin"]))
  374. if rule.get("portEnd", None) is not None and rule["portEnd"] > 0:
  375. element.set("toport", str(rule["portEnd"]))
  376. if "expire" in rule:
  377. element.set("expire", str(rule["expire"]))
  378. expiring_rules_present = True
  379. root.append(element)
  380. tree = lxml.etree.ElementTree(root)
  381. try:
  382. old_umask = os.umask(002)
  383. with open(self.firewall_conf, 'w') as f:
  384. tree.write(f, encoding="UTF-8", pretty_print=True)
  385. f.close()
  386. os.umask(old_umask)
  387. except EnvironmentError as err:
  388. print >> sys.stderr, "{0}: save error: {1}".format(
  389. os.path.basename(sys.argv[0]), err)
  390. return False
  391. # Automatically enable/disable 'yum-proxy-setup' service based on allowYumProxy
  392. if conf['allowYumProxy']:
  393. self.services['yum-proxy-setup'] = True
  394. else:
  395. if self.services.has_key('yum-proxy-setup'):
  396. self.services.pop('yum-proxy-setup')
  397. if expiring_rules_present:
  398. subprocess.call(["sudo", "systemctl", "start",
  399. "qubes-reload-firewall@%s.timer" % self.name])
  400. return True
  401. def has_firewall(self):
  402. return os.path.exists (self.firewall_conf)
  403. def get_firewall_defaults(self):
  404. return { "rules": list(), "allow": True, "allowDns": True, "allowIcmp": True, "allowYumProxy": False }
  405. def get_firewall_conf(self):
  406. conf = self.get_firewall_defaults()
  407. try:
  408. tree = lxml.etree.parse(self.firewall_conf)
  409. root = tree.getroot()
  410. conf["allow"] = (root.get("policy") == "allow")
  411. conf["allowDns"] = (root.get("dns") == "allow")
  412. conf["allowIcmp"] = (root.get("icmp") == "allow")
  413. conf["allowYumProxy"] = (root.get("yumProxy") == "allow")
  414. for element in root:
  415. rule = {}
  416. attr_list = ("address", "netmask", "proto", "port", "toport",
  417. "expire")
  418. for attribute in attr_list:
  419. rule[attribute] = element.get(attribute)
  420. if rule["netmask"] is not None:
  421. rule["netmask"] = int(rule["netmask"])
  422. else:
  423. rule["netmask"] = 32
  424. if rule["port"] is not None:
  425. rule["portBegin"] = int(rule["port"])
  426. else:
  427. # backward compatibility
  428. rule["portBegin"] = 0
  429. # For backward compatibility
  430. if rule["proto"] is None:
  431. if rule["portBegin"] > 0:
  432. rule["proto"] = "tcp"
  433. else:
  434. rule["proto"] = "any"
  435. if rule["toport"] is not None:
  436. rule["portEnd"] = int(rule["toport"])
  437. else:
  438. rule["portEnd"] = None
  439. if rule["expire"] is not None:
  440. rule["expire"] = int(rule["expire"])
  441. if rule["expire"] <= int(datetime.datetime.now().strftime(
  442. "%s")):
  443. continue
  444. else:
  445. del(rule["expire"])
  446. del(rule["port"])
  447. del(rule["toport"])
  448. conf["rules"].append(rule)
  449. except EnvironmentError as err:
  450. return conf
  451. except (xml.parsers.expat.ExpatError,
  452. ValueError, LookupError) as err:
  453. print("{0}: load error: {1}".format(
  454. os.path.basename(sys.argv[0]), err))
  455. return None
  456. return conf
  457. def load(class_, D):
  458. cls = BaseVM[class_]
  459. return cls(D)
  460. __all__ = qubes.plugins.load(__file__)