core2.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''Parser for core2 qubes.xml'''
  21. import ast
  22. import xml.parsers
  23. import logging
  24. import lxml.etree
  25. from qubesadmin.firewall import Rule, Action, Proto, DstHost, SpecialTarget
  26. import qubesadmin.backup
  27. service_to_feature = {
  28. 'ntpd': 'service.ntpd',
  29. 'qubes-update-check': 'check-updates',
  30. 'meminfo-writer': 'service.meminfo-writer',
  31. }
  32. class Core2VM(qubesadmin.backup.BackupVM):
  33. '''VM object'''
  34. # pylint: disable=too-few-public-methods
  35. def __init__(self):
  36. super(Core2VM, self).__init__()
  37. self.backup_content = False
  38. @property
  39. def included_in_backup(self):
  40. return self.backup_content
  41. @staticmethod
  42. def rule_from_xml_v1(node, action):
  43. '''Parse single rule in old XML format (pre Qubes 4.0)
  44. :param node: XML node for the rule
  45. :param action: action to apply (in old format it wasn't part of the
  46. rule itself)
  47. '''
  48. netmask = node.get('netmask')
  49. if netmask is None:
  50. netmask = 32
  51. else:
  52. netmask = int(netmask)
  53. address = node.get('address')
  54. if address:
  55. dsthost = DstHost(address, netmask)
  56. else:
  57. dsthost = None
  58. proto = node.get('proto')
  59. port = node.get('port')
  60. toport = node.get('toport')
  61. if port and toport:
  62. dstports = port + '-' + toport
  63. elif port:
  64. dstports = port
  65. else:
  66. dstports = None
  67. # backward compatibility: protocol defaults to TCP if port is specified
  68. if dstports and not proto:
  69. proto = 'tcp'
  70. if proto == 'any':
  71. proto = None
  72. expire = node.get('expire')
  73. kwargs = {
  74. 'action': action,
  75. }
  76. if dsthost:
  77. kwargs['dsthost'] = dsthost
  78. if dstports:
  79. kwargs['dstports'] = dstports
  80. if proto:
  81. kwargs['proto'] = proto
  82. if expire:
  83. kwargs['expire'] = expire
  84. return Rule(None, **kwargs)
  85. def handle_firewall_xml(self, vm, stream):
  86. '''Load old (Qubes < 4.0) firewall XML format'''
  87. try:
  88. tree = lxml.etree.parse(stream) # pylint: disable=no-member
  89. xml_root = tree.getroot()
  90. policy_v1 = xml_root.get('policy')
  91. assert policy_v1 in ('allow', 'deny')
  92. default_policy_is_accept = (policy_v1 == 'allow')
  93. rules = []
  94. def _translate_action(key):
  95. '''Translate action name'''
  96. if xml_root.get(key, policy_v1) == 'allow':
  97. return Action.accept
  98. return Action.drop
  99. rules.append(Rule(None,
  100. action=_translate_action('dns'),
  101. specialtarget=SpecialTarget('dns')))
  102. rules.append(Rule(None,
  103. action=_translate_action('icmp'),
  104. proto=Proto.icmp))
  105. if default_policy_is_accept:
  106. rule_action = Action.drop
  107. else:
  108. rule_action = Action.accept
  109. for element in xml_root:
  110. rule = self.rule_from_xml_v1(element, rule_action)
  111. rules.append(rule)
  112. if default_policy_is_accept:
  113. rules.append(Rule(None, action='accept'))
  114. else:
  115. rules.append(Rule(None, action='drop'))
  116. vm.firewall.rules = rules
  117. except: # pylint: disable=bare-except
  118. vm.log.exception('Failed to set firewall')
  119. class Core2Qubes(qubesadmin.backup.BackupApp):
  120. '''Parsed qubes.xml'''
  121. def __init__(self, store=None):
  122. if store is None:
  123. raise ValueError("store path required")
  124. self.qid_map = {}
  125. self.log = logging.getLogger('qubesadmin.backup.core2')
  126. super(Core2Qubes, self).__init__(store)
  127. def load_globals(self, element):
  128. '''Load global settings
  129. :param element: XML element containing global settings (root node)
  130. '''
  131. default_netvm = element.get("default_netvm")
  132. if default_netvm is not None:
  133. self.globals['default_netvm'] = self.qid_map[int(default_netvm)] \
  134. if default_netvm != "None" else None
  135. # default_fw_netvm = element.get("default_fw_netvm")
  136. # if default_fw_netvm is not None:
  137. # self.globals['default_fw_netvm'] = \
  138. # self.qid_map[int(default_fw_netvm)] \
  139. # if default_fw_netvm != "None" else None
  140. updatevm = element.get("updatevm")
  141. if updatevm is not None:
  142. self.globals['updatevm'] = self.qid_map[int(updatevm)] \
  143. if updatevm != "None" else None
  144. clockvm = element.get("clockvm")
  145. if clockvm is not None:
  146. self.globals['clockvm'] = self.qid_map[int(clockvm)] \
  147. if clockvm != "None" else None
  148. default_template = element.get("default_template")
  149. self.globals['default_template'] = self.qid_map[int(default_template)] \
  150. if default_template.lower() != "none" else None
  151. def set_netvm_dependency(self, element):
  152. '''Set dependencies between VMs'''
  153. kwargs = {}
  154. attr_list = ("name", "uses_default_netvm", "netvm_qid")
  155. for attribute in attr_list:
  156. kwargs[attribute] = element.get(attribute)
  157. vm = self.domains[kwargs["name"]]
  158. # netvm property
  159. if element.get("uses_default_netvm") is None:
  160. uses_default_netvm = True
  161. else:
  162. uses_default_netvm = (element.get("uses_default_netvm") == "True")
  163. if not uses_default_netvm:
  164. netvm_qid = element.get("netvm_qid")
  165. if netvm_qid is None or netvm_qid == "none":
  166. vm.properties['netvm'] = None
  167. else:
  168. vm.properties['netvm'] = self.qid_map[int(netvm_qid)]
  169. # And DispVM netvm, translated to default_dispvm
  170. if element.get("uses_default_dispvm_netvm") is None:
  171. uses_default_dispvm_netvm = True
  172. else:
  173. uses_default_dispvm_netvm = (
  174. element.get("uses_default_dispvm_netvm") == "True")
  175. if not uses_default_dispvm_netvm:
  176. dispvm_netvm_qid = element.get("dispvm_netvm_qid")
  177. if dispvm_netvm_qid is None or dispvm_netvm_qid == "none":
  178. dispvm_netvm = None
  179. else:
  180. dispvm_netvm = self.qid_map[int(dispvm_netvm_qid)]
  181. else:
  182. dispvm_netvm = vm.properties.get('netvm', self.globals[
  183. 'default_netvm'])
  184. if dispvm_netvm != self.globals['default_netvm']:
  185. if dispvm_netvm:
  186. dispvm_tpl_name = 'disp-{}'.format(dispvm_netvm)
  187. else:
  188. dispvm_tpl_name = 'disp-no-netvm'
  189. vm.properties['default_dispvm'] = dispvm_tpl_name
  190. if dispvm_tpl_name not in self.domains:
  191. vm = Core2VM()
  192. vm.name = dispvm_tpl_name
  193. vm.label = 'red'
  194. vm.properties['netvm'] = dispvm_netvm
  195. vm.properties['template_for_dispvms'] = True
  196. vm.backup_content = True
  197. vm.backup_path = None
  198. self.domains[vm.name] = vm
  199. # TODO: add support for #2075
  200. # TODO: set qrexec policy based on dispvm_netvm value
  201. def import_core2_vm(self, element):
  202. '''Parse a single VM from given XML node
  203. This method load only VM properties not depending on other VMs
  204. (other than template). VM connections are set later.
  205. :param element: XML node
  206. '''
  207. vm_class_name = element.tag
  208. vm = Core2VM()
  209. vm.name = element.get('name')
  210. vm.label = element.get('label', 'red')
  211. self.domains[vm.name] = vm
  212. kwargs = {}
  213. if vm_class_name in ["QubesTemplateVm", "QubesTemplateHVm"]:
  214. vm.klass = "TemplateVM"
  215. elif element.get('qid') == '0':
  216. kwargs['dir_path'] = element.get('dir_path')
  217. vm.klass = "AdminVM"
  218. elif element.get('template_qid').lower() == "none":
  219. kwargs['dir_path'] = element.get('dir_path')
  220. vm.klass = "StandaloneVM"
  221. else:
  222. kwargs['dir_path'] = element.get('dir_path')
  223. vm.template = \
  224. self.qid_map[int(element.get('template_qid'))]
  225. vm.klass = "AppVM"
  226. vm.backup_content = element.get('backup_content', False) == 'True'
  227. vm.backup_path = element.get('backup_path', None)
  228. vm.size = element.get('backup_size', 0)
  229. if vm.klass == 'AdminVM':
  230. # don't set any other dom0 property
  231. return
  232. # simple attributes
  233. for attr, default in {
  234. #'installed_by_rpm': 'False',
  235. 'include_in_backups': 'True',
  236. 'qrexec_timeout': '60',
  237. 'vcpus': '2',
  238. 'memory': '400',
  239. 'maxmem': '4000',
  240. 'default_user': 'user',
  241. 'debug': 'False',
  242. 'mac': None,
  243. 'autostart': 'False'}.items():
  244. value = element.get(attr)
  245. if value and value != default:
  246. vm.properties[attr] = value
  247. # attributes with default value
  248. for attr in ["kernel", "kernelopts"]:
  249. value = element.get(attr)
  250. if value and value.lower() == "none":
  251. value = None
  252. value_is_default = element.get(
  253. "uses_default_{}".format(attr))
  254. if value_is_default and value_is_default.lower() != \
  255. "true":
  256. vm.properties[attr] = value
  257. if "HVm" in vm_class_name:
  258. vm.properties['virt_mode'] = 'hvm'
  259. vm.properties['kernel'] = ''
  260. # Qubes 3.2 used MiniOS stubdomain (with qemu-traditional); keep
  261. # it this way, otherwise some OSes (Windows) will crash because
  262. # of substantial hardware change
  263. vm.features['linux-stubdom'] = False
  264. if vm_class_name in ('QubesNetVm', 'QubesProxyVm'):
  265. vm.properties['provides_network'] = True
  266. if vm_class_name == 'QubesNetVm':
  267. vm.properties['netvm'] = None
  268. if vm_class_name == 'QubesTemplateVm' or \
  269. (vm_class_name == 'QubesAppVm' and vm.template is None):
  270. # PV VMs in Qubes 3.x assumed gui-agent and qrexec-agent installed
  271. vm.features['qrexec'] = True
  272. vm.features['gui'] = True
  273. if element.get('internal', False) == 'True':
  274. vm.features['internal'] = True
  275. services = element.get('services')
  276. if services:
  277. services = ast.literal_eval(services)
  278. else:
  279. services = {}
  280. for service, value in services.items():
  281. feature = service
  282. for repl_service, repl_feature in \
  283. service_to_feature.items():
  284. if repl_service == service:
  285. feature = repl_feature
  286. vm.features[feature] = value
  287. pci_strictreset = element.get('pci_strictreset', True)
  288. pcidevs = element.get('pcidevs')
  289. if pcidevs:
  290. pcidevs = ast.literal_eval(pcidevs)
  291. for pcidev in pcidevs:
  292. if not pci_strictreset:
  293. vm.devices['pci'][('dom0', pcidev.replace(':', '_'))] = {
  294. 'no-strict-reset': True}
  295. else:
  296. vm.devices['pci'][('dom0', pcidev.replace(':', '_'))] = {}
  297. def load(self):
  298. with open(self.store) as fh:
  299. try:
  300. # pylint: disable=no-member
  301. tree = lxml.etree.parse(fh)
  302. except (EnvironmentError, # pylint: disable=broad-except
  303. xml.parsers.expat.ExpatError) as err:
  304. self.log.error(err)
  305. return False
  306. self.globals['default_kernel'] = tree.getroot().get("default_kernel")
  307. vm_classes = ["AdminVm", "TemplateVm", "TemplateHVm",
  308. "AppVm", "HVm", "NetVm", "ProxyVm"]
  309. # First build qid->name map
  310. for vm_class_name in vm_classes:
  311. vms_of_class = tree.findall("Qubes" + vm_class_name)
  312. for element in vms_of_class:
  313. qid = element.get('qid', None)
  314. name = element.get('name', None)
  315. if qid and name:
  316. self.qid_map[int(qid)] = name
  317. # Qubes R2 din't have dom0 in qubes.xml
  318. if 0 not in self.qid_map:
  319. vm = Core2VM()
  320. vm.name = 'dom0'
  321. vm.klass = 'AdminVM'
  322. vm.label = 'black'
  323. self.domains['dom0'] = vm
  324. self.qid_map[0] = 'dom0'
  325. # Then load all VMs - since we have qid_map, no need to preserve
  326. # specific load older.
  327. for vm_class_name in vm_classes:
  328. vms_of_class = tree.findall("Qubes" + vm_class_name)
  329. for element in vms_of_class:
  330. self.import_core2_vm(element)
  331. # ... and load other VMs
  332. for vm_class_name in ["AppVm", "HVm", "NetVm", "ProxyVm"]:
  333. vms_of_class = tree.findall("Qubes" + vm_class_name)
  334. # first non-template based, then template based
  335. sorted_vms_of_class = sorted(vms_of_class,
  336. key=lambda x: str(x.get('template_qid')).lower() != "none")
  337. for element in sorted_vms_of_class:
  338. self.import_core2_vm(element)
  339. # and load other defaults (default netvm, updatevm etc)
  340. self.load_globals(tree.getroot())
  341. # After importing all VMs, set netvm references, in the same order
  342. for vm_class_name in vm_classes:
  343. for element in tree.findall("Qubes" + vm_class_name):
  344. try:
  345. self.set_netvm_dependency(element)
  346. except (ValueError, LookupError) as err:
  347. self.log.error("VM %s: failed to set netvm dependency: %s",
  348. element.get('name'), err)