core2.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 = (
  163. True if element.get("uses_default_netvm") == "True" else False)
  164. if not uses_default_netvm:
  165. netvm_qid = element.get("netvm_qid")
  166. if netvm_qid is None or netvm_qid == "none":
  167. vm.properties['netvm'] = None
  168. else:
  169. vm.properties['netvm'] = self.qid_map[int(netvm_qid)]
  170. # And DispVM netvm, translated to default_dispvm
  171. if element.get("uses_default_dispvm_netvm") is None:
  172. uses_default_dispvm_netvm = True
  173. else:
  174. uses_default_dispvm_netvm = (
  175. True if element.get("uses_default_dispvm_netvm") == "True"
  176. else False)
  177. if not uses_default_dispvm_netvm:
  178. dispvm_netvm_qid = element.get("dispvm_netvm_qid")
  179. if dispvm_netvm_qid is None or dispvm_netvm_qid == "none":
  180. dispvm_netvm = None
  181. else:
  182. dispvm_netvm = self.qid_map[int(dispvm_netvm_qid)]
  183. else:
  184. dispvm_netvm = vm.properties.get('netvm', self.globals[
  185. 'default_netvm'])
  186. if dispvm_netvm != self.globals['default_netvm']:
  187. if dispvm_netvm:
  188. dispvm_tpl_name = 'disp-{}'.format(dispvm_netvm)
  189. else:
  190. dispvm_tpl_name = 'disp-no-netvm'
  191. vm.properties['default_dispvm'] = dispvm_tpl_name
  192. if dispvm_tpl_name not in self.domains:
  193. vm = Core2VM()
  194. vm.name = dispvm_tpl_name
  195. vm.label = 'red'
  196. vm.properties['netvm'] = dispvm_netvm
  197. vm.properties['template_for_dispvms'] = True
  198. vm.backup_content = True
  199. vm.backup_path = None
  200. self.domains[vm.name] = vm
  201. # TODO: add support for #2075
  202. # TODO: set qrexec policy based on dispvm_netvm value
  203. def import_core2_vm(self, element):
  204. '''Parse a single VM from given XML node
  205. This method load only VM properties not depending on other VMs
  206. (other than template). VM connections are set later.
  207. :param element: XML node
  208. '''
  209. vm_class_name = element.tag
  210. vm = Core2VM()
  211. vm.name = element.get('name')
  212. vm.label = element.get('label', 'red')
  213. self.domains[vm.name] = vm
  214. kwargs = {}
  215. if vm_class_name in ["QubesTemplateVm", "QubesTemplateHVm"]:
  216. vm.klass = "TemplateVM"
  217. elif element.get('template_qid').lower() == "none":
  218. kwargs['dir_path'] = element.get('dir_path')
  219. vm.klass = "StandaloneVM"
  220. else:
  221. kwargs['dir_path'] = element.get('dir_path')
  222. vm.template = \
  223. self.qid_map[int(element.get('template_qid'))]
  224. vm.klass = "AppVM"
  225. # simple attributes
  226. for attr, default in {
  227. #'installed_by_rpm': 'False',
  228. 'include_in_backups': 'True',
  229. 'qrexec_timeout': '60',
  230. 'vcpus': '2',
  231. 'memory': '400',
  232. 'maxmem': '4000',
  233. 'default_user': 'user',
  234. 'debug': 'False',
  235. 'mac': None,
  236. 'autostart': 'False'}.items():
  237. value = element.get(attr)
  238. if value and value != default:
  239. vm.properties[attr] = value
  240. # attributes with default value
  241. for attr in ["kernel", "kernelopts"]:
  242. value = element.get(attr)
  243. if value and value.lower() == "none":
  244. value = None
  245. value_is_default = element.get(
  246. "uses_default_{}".format(attr))
  247. if value_is_default and value_is_default.lower() != \
  248. "true":
  249. vm.properties[attr] = value
  250. if "HVm" in vm_class_name:
  251. vm.properties['virt_mode'] = 'hvm'
  252. vm.properties['kernel'] = ''
  253. # Qubes 3.2 used MiniOS stubdomain (with qemu-traditional); keep
  254. # it this way, otherwise some OSes (Windows) will crash because
  255. # of substantial hardware change
  256. vm.features['linux-stubdom'] = False
  257. if vm_class_name in ('QubesNetVm', 'QubesProxyVm'):
  258. vm.properties['provides_network'] = True
  259. if vm_class_name == 'QubesNetVm':
  260. vm.properties['netvm'] = None
  261. if vm_class_name == 'QubesTemplateVm' or \
  262. (vm_class_name == 'QubesAppVm' and vm.template is None):
  263. # PV VMs in Qubes 3.x assumed gui-agent and qrexec-agent installed
  264. vm.features['qrexec'] = True
  265. vm.features['gui'] = True
  266. if element.get('internal', False) == 'True':
  267. vm.features['internal'] = True
  268. services = element.get('services')
  269. if services:
  270. services = ast.literal_eval(services)
  271. else:
  272. services = {}
  273. for service, value in services.items():
  274. feature = service
  275. for repl_service, repl_feature in \
  276. service_to_feature.items():
  277. if repl_service == service:
  278. feature = repl_feature
  279. vm.features[feature] = value
  280. vm.backup_content = element.get('backup_content', False) == 'True'
  281. vm.backup_path = element.get('backup_path', None)
  282. vm.size = element.get('backup_size', 0)
  283. pci_strictreset = element.get('pci_strictreset', True)
  284. pcidevs = element.get('pcidevs')
  285. if pcidevs:
  286. pcidevs = ast.literal_eval(pcidevs)
  287. for pcidev in pcidevs:
  288. if not pci_strictreset:
  289. vm.devices['pci'][('dom0', pcidev.replace(':', '_'))] = {
  290. 'no-strict-reset': True}
  291. else:
  292. vm.devices['pci'][('dom0', pcidev.replace(':', '_'))] = {}
  293. def load(self):
  294. with open(self.store) as fh:
  295. try:
  296. # pylint: disable=no-member
  297. tree = lxml.etree.parse(fh)
  298. except (EnvironmentError, # pylint: disable=broad-except
  299. xml.parsers.expat.ExpatError) as err:
  300. self.log.error(err)
  301. return False
  302. self.globals['default_kernel'] = tree.getroot().get("default_kernel")
  303. vm_classes = ["AdminVM", "TemplateVm", "TemplateHVm",
  304. "AppVm", "HVm", "NetVm", "ProxyVm"]
  305. # First build qid->name map
  306. for vm_class_name in vm_classes:
  307. vms_of_class = tree.findall("Qubes" + vm_class_name)
  308. for element in vms_of_class:
  309. qid = element.get('qid', None)
  310. name = element.get('name', None)
  311. if qid and name:
  312. self.qid_map[int(qid)] = name
  313. # Qubes R2 din't have dom0 in qubes.xml
  314. if 0 not in self.qid_map:
  315. vm = Core2VM()
  316. vm.name = 'dom0'
  317. vm.klass = 'AdminVM'
  318. vm.label = 'black'
  319. self.domains['dom0'] = vm
  320. self.qid_map[0] = 'dom0'
  321. # Then load all VMs - since we have qid_map, no need to preserve
  322. # specific load older.
  323. for vm_class_name in vm_classes:
  324. vms_of_class = tree.findall("Qubes" + vm_class_name)
  325. for element in vms_of_class:
  326. self.import_core2_vm(element)
  327. # ... and load other VMs
  328. for vm_class_name in ["AppVm", "HVm", "NetVm", "ProxyVm"]:
  329. vms_of_class = tree.findall("Qubes" + vm_class_name)
  330. # first non-template based, then template based
  331. sorted_vms_of_class = sorted(vms_of_class,
  332. key=lambda x: str(x.get('template_qid')).lower() != "none")
  333. for element in sorted_vms_of_class:
  334. self.import_core2_vm(element)
  335. # and load other defaults (default netvm, updatevm etc)
  336. self.load_globals(tree.getroot())
  337. # After importing all VMs, set netvm references, in the same order
  338. for vm_class_name in vm_classes:
  339. for element in tree.findall("Qubes" + vm_class_name):
  340. try:
  341. self.set_netvm_dependency(element)
  342. except (ValueError, LookupError) as err:
  343. self.log.error("VM %s: failed to set netvm dependency: %s",
  344. element.get('name'), err)