__init__.py 19 KB

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