app.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  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. import errno
  26. import grp
  27. import logging
  28. import os
  29. import sys
  30. import tempfile
  31. import time
  32. import jinja2
  33. import libvirt
  34. import lxml.etree
  35. try:
  36. import xen.lowlevel.xs
  37. import xen.lowlevel.xc
  38. except ImportError:
  39. pass
  40. if os.name == 'posix':
  41. import fcntl
  42. elif os.name == 'nt':
  43. # pylint: disable=import-error
  44. import win32con
  45. import win32file
  46. import pywintypes
  47. else:
  48. raise RuntimeError("Qubes works only on POSIX or WinNT systems")
  49. import qubes
  50. import qubes.ext
  51. import qubes.utils
  52. import qubes.vm.adminvm
  53. import qubes.vm.qubesvm
  54. import qubes.vm.templatevm
  55. class VMMConnection(object):
  56. '''Connection to Virtual Machine Manager (libvirt)'''
  57. def __init__(self):
  58. self._libvirt_conn = None
  59. self._xs = None
  60. self._xc = None
  61. self._offline_mode = False
  62. @property
  63. def offline_mode(self):
  64. '''Check or enable offline mode (do not actually connect to vmm)'''
  65. return self._offline_mode
  66. @offline_mode.setter
  67. def offline_mode(self, value):
  68. if value and self._libvirt_conn is not None:
  69. raise qubes.exc.QubesException(
  70. 'Cannot change offline mode while already connected')
  71. self._offline_mode = value
  72. def _libvirt_error_handler(self, ctx, error):
  73. pass
  74. def init_vmm_connection(self):
  75. '''Initialise connection
  76. This method is automatically called when getting'''
  77. if self._libvirt_conn is not None:
  78. # Already initialized
  79. return
  80. if self._offline_mode:
  81. # Do not initialize in offline mode
  82. raise qubes.exc.QubesException(
  83. 'VMM operations disabled in offline mode')
  84. if 'xen.lowlevel.xs' in sys.modules:
  85. self._xs = xen.lowlevel.xs.xs()
  86. if 'xen.lowlevel.cs' in sys.modules:
  87. self._xc = xen.lowlevel.xc.xc()
  88. self._libvirt_conn = libvirt.open(qubes.config.defaults['libvirt_uri'])
  89. if self._libvirt_conn is None:
  90. raise qubes.exc.QubesException('Failed connect to libvirt driver')
  91. libvirt.registerErrorHandler(self._libvirt_error_handler, None)
  92. @property
  93. def libvirt_conn(self):
  94. '''Connection to libvirt'''
  95. self.init_vmm_connection()
  96. return self._libvirt_conn
  97. @property
  98. def xs(self):
  99. '''Connection to Xen Store
  100. This property in available only when running on Xen.
  101. '''
  102. # XXX what about the case when we run under KVM,
  103. # but xen modules are importable?
  104. if 'xen.lowlevel.xs' not in sys.modules:
  105. raise AttributeError(
  106. 'xs object is available under Xen hypervisor only')
  107. self.init_vmm_connection()
  108. return self._xs
  109. @property
  110. def xc(self):
  111. '''Connection to Xen
  112. This property in available only when running on Xen.
  113. '''
  114. # XXX what about the case when we run under KVM,
  115. # but xen modules are importable?
  116. if 'xen.lowlevel.xc' not in sys.modules:
  117. raise AttributeError(
  118. 'xc object is available under Xen hypervisor only')
  119. self.init_vmm_connection()
  120. return self._xs
  121. def __del__(self):
  122. if self._libvirt_conn:
  123. self._libvirt_conn.close()
  124. class QubesHost(object):
  125. '''Basic information about host machine
  126. :param qubes.Qubes app: Qubes application context (must have \
  127. :py:attr:`Qubes.vmm` attribute defined)
  128. '''
  129. def __init__(self, app):
  130. self.app = app
  131. self._no_cpus = None
  132. self._total_mem = None
  133. self._physinfo = None
  134. def _fetch(self):
  135. if self._no_cpus is not None:
  136. return
  137. # pylint: disable=unused-variable
  138. (model, memory, cpus, mhz, nodes, socket, cores, threads) = \
  139. self.app.vmm.libvirt_conn.getInfo()
  140. self._total_mem = long(memory) * 1024
  141. self._no_cpus = cpus
  142. self.app.log.debug('QubesHost: no_cpus={} memory_total={}'.format(
  143. self.no_cpus, self.memory_total))
  144. try:
  145. self.app.log.debug('QubesHost: xen_free_memory={}'.format(
  146. self.get_free_xen_memory()))
  147. except NotImplementedError:
  148. pass
  149. @property
  150. def memory_total(self):
  151. '''Total memory, in kbytes'''
  152. self._fetch()
  153. return self._total_mem
  154. @property
  155. def no_cpus(self):
  156. '''Number of CPUs'''
  157. self._fetch()
  158. return self._no_cpus
  159. def get_free_xen_memory(self):
  160. '''Get free memory from Xen's physinfo.
  161. :raises NotImplementedError: when not under Xen
  162. '''
  163. try:
  164. self._physinfo = self.app.xc.physinfo()
  165. except AttributeError:
  166. raise NotImplementedError('This function requires Xen hypervisor')
  167. return long(self._physinfo['free_memory'])
  168. def measure_cpu_usage(self, previous_time=None, previous=None,
  169. wait_time=1):
  170. '''Measure cpu usage for all domains at once.
  171. This function requires Xen hypervisor.
  172. .. versionchanged:: 3.0
  173. argument order to match return tuple
  174. :raises NotImplementedError: when not under Xen
  175. '''
  176. if previous is None:
  177. previous_time = time.time()
  178. previous = {}
  179. try:
  180. info = self.app.vmm.xc.domain_getinfo(0, qubes.config.max_qid)
  181. except AttributeError:
  182. raise NotImplementedError(
  183. 'This function requires Xen hypervisor')
  184. for vm in info:
  185. previous[vm['domid']] = {}
  186. previous[vm['domid']]['cpu_time'] = (
  187. vm['cpu_time'] / vm['online_vcpus'])
  188. previous[vm['domid']]['cpu_usage'] = 0
  189. time.sleep(wait_time)
  190. current_time = time.time()
  191. current = {}
  192. try:
  193. info = self.app.vmm.xc.domain_getinfo(0, qubes.config.max_qid)
  194. except AttributeError:
  195. raise NotImplementedError(
  196. 'This function requires Xen hypervisor')
  197. for vm in info:
  198. current[vm['domid']] = {}
  199. current[vm['domid']]['cpu_time'] = (
  200. vm['cpu_time'] / max(vm['online_vcpus'], 1))
  201. if vm['domid'] in previous.keys():
  202. current[vm['domid']]['cpu_usage'] = (
  203. float(current[vm['domid']]['cpu_time'] -
  204. previous[vm['domid']]['cpu_time']) /
  205. long(1000 ** 3) / (current_time - previous_time) * 100)
  206. if current[vm['domid']]['cpu_usage'] < 0:
  207. # VM has been rebooted
  208. current[vm['domid']]['cpu_usage'] = 0
  209. else:
  210. current[vm['domid']]['cpu_usage'] = 0
  211. return (current_time, current)
  212. class VMCollection(object):
  213. '''A collection of Qubes VMs
  214. VMCollection supports ``in`` operator. You may test for ``qid``, ``name``
  215. and whole VM object's presence.
  216. Iterating over VMCollection will yield machine objects.
  217. '''
  218. def __init__(self, app):
  219. self.app = app
  220. self._dict = dict()
  221. def __repr__(self):
  222. return '<{} {!r}>'.format(
  223. self.__class__.__name__, list(sorted(self.keys())))
  224. def items(self):
  225. '''Iterate over ``(qid, vm)`` pairs'''
  226. for qid in self.qids():
  227. yield (qid, self[qid])
  228. def qids(self):
  229. '''Iterate over all qids
  230. qids are sorted by numerical order.
  231. '''
  232. return iter(sorted(self._dict.keys()))
  233. keys = qids
  234. def names(self):
  235. '''Iterate over all names
  236. names are sorted by lexical order.
  237. '''
  238. return iter(sorted(vm.name for vm in self._dict.values()))
  239. def vms(self):
  240. '''Iterate over all machines
  241. vms are sorted by qid.
  242. '''
  243. return iter(sorted(self._dict.values()))
  244. __iter__ = vms
  245. values = vms
  246. def add(self, value):
  247. '''Add VM to collection
  248. :param qubes.vm.BaseVM value: VM to add
  249. :raises TypeError: when value is of wrong type
  250. :raises ValueError: when there is already VM which has equal ``qid``
  251. '''
  252. # this violates duck typing, but is needed
  253. # for VMProperty to function correctly
  254. if not isinstance(value, qubes.vm.BaseVM):
  255. raise TypeError('{} holds only BaseVM instances'.format(
  256. self.__class__.__name__))
  257. if value.qid in self:
  258. raise ValueError('This collection already holds VM that has '
  259. 'qid={!r} ({!r})'.format(value.qid, self[value.qid]))
  260. if value.name in self:
  261. raise ValueError('This collection already holds VM that has '
  262. 'name={!r} ({!r})'.format(value.name, self[value.name]))
  263. self._dict[value.qid] = value
  264. value.events_enabled = True
  265. self.app.fire_event('domain-add', value)
  266. return value
  267. def __getitem__(self, key):
  268. if isinstance(key, int):
  269. return self._dict[key]
  270. if isinstance(key, basestring):
  271. for vm in self:
  272. if vm.name == key:
  273. return vm
  274. raise KeyError(key)
  275. if isinstance(key, qubes.vm.BaseVM):
  276. if key in self:
  277. return key
  278. raise KeyError(key)
  279. raise KeyError(key)
  280. def __delitem__(self, key):
  281. vm = self[key]
  282. self.app.fire_event_pre('domain-pre-delete', vm)
  283. del self._dict[vm.qid]
  284. self.app.fire_event('domain-delete', vm)
  285. def __contains__(self, key):
  286. return any((key == vm or key == vm.qid or key == vm.name)
  287. for vm in self)
  288. def __len__(self):
  289. return len(self._dict)
  290. def get_vms_based_on(self, template):
  291. template = self[template]
  292. return set(vm for vm in self
  293. if hasattr(vm, 'template') and vm.template == template)
  294. def get_vms_connected_to(self, netvm):
  295. new_vms = set([self[netvm]])
  296. dependent_vms = set()
  297. # Dependency resolving only makes sense on NetVM (or derivative)
  298. # if not self[netvm_qid].is_netvm():
  299. # return set([])
  300. while len(new_vms) > 0:
  301. cur_vm = new_vms.pop()
  302. for vm in cur_vm.connected_vms.values():
  303. if vm in dependent_vms:
  304. continue
  305. dependent_vms.add(vm.qid)
  306. # if vm.is_netvm():
  307. new_vms.add(vm.qid)
  308. return dependent_vms
  309. # XXX with Qubes Admin Api this will probably lead to race condition
  310. # whole process of creating and adding should be synchronised
  311. def get_new_unused_qid(self):
  312. used_ids = set(self.qids())
  313. for i in range(1, qubes.config.max_qid):
  314. if i not in used_ids:
  315. return i
  316. raise LookupError("Cannot find unused qid!")
  317. def get_new_unused_netid(self):
  318. used_ids = set([vm.netid for vm in self]) # if vm.is_netvm()])
  319. for i in range(1, qubes.config.max_netid):
  320. if i not in used_ids:
  321. return i
  322. raise LookupError("Cannot find unused netid!")
  323. class Qubes(qubes.PropertyHolder):
  324. '''Main Qubes application
  325. :param str store: path to ``qubes.xml``
  326. The store is loaded in stages:
  327. 1. In the first stage there are loaded some basic features from store
  328. (currently labels).
  329. 2. In the second stage stubs for all VMs are loaded. They are filled
  330. with their basic properties, like ``qid`` and ``name``.
  331. 3. In the third stage all global properties are loaded. They often
  332. reference VMs, like default netvm, so they should be filled after
  333. loading VMs.
  334. 4. In the fourth stage all remaining VM properties are loaded. They
  335. also need all VMs loaded, because they represent dependencies
  336. between VMs like aforementioned netvm.
  337. 5. In the fifth stage there are some fixups to ensure sane system
  338. operation.
  339. This class emits following events:
  340. .. event:: domain-add (subject, event, vm)
  341. When domain is added.
  342. :param subject: Event emitter
  343. :param event: Event name (``'domain-add'``)
  344. :param vm: Domain object
  345. .. event:: domain-delete (subject, event, vm)
  346. When domain is deleted. VM still has reference to ``app`` object,
  347. but is not contained within VMCollection.
  348. :param subject: Event emitter
  349. :param event: Event name (``'domain-delete'``)
  350. :param vm: Domain object
  351. Methods and attributes:
  352. '''
  353. default_netvm = qubes.VMProperty('default_netvm', load_stage=3,
  354. default=None, allow_none=True,
  355. doc='''Default NetVM for AppVMs. Initial state is `None`, which means
  356. that AppVMs are not connected to the Internet.''')
  357. default_fw_netvm = qubes.VMProperty('default_fw_netvm', load_stage=3,
  358. default=None, allow_none=True,
  359. doc='''Default NetVM for ProxyVMs. Initial state is `None`, which means
  360. that ProxyVMs (including FirewallVM) are not connected to the
  361. Internet.''')
  362. default_template = qubes.VMProperty('default_template', load_stage=3,
  363. vmclass=qubes.vm.templatevm.TemplateVM,
  364. doc='Default template for new AppVMs')
  365. updatevm = qubes.VMProperty('updatevm', load_stage=3,
  366. allow_none=True,
  367. doc='''Which VM to use as `yum` proxy for updating AdminVM and
  368. TemplateVMs''')
  369. clockvm = qubes.VMProperty('clockvm', load_stage=3,
  370. allow_none=True,
  371. doc='Which VM to use as NTP proxy for updating AdminVM')
  372. default_kernel = qubes.property('default_kernel', load_stage=3,
  373. doc='Which kernel to use when not overriden in VM')
  374. # TODO #1637 #892
  375. check_updates_vm = qubes.property('check_updates_vm',
  376. type=bool, setter=qubes.property.bool,
  377. default=True,
  378. doc='check for updates inside qubes')
  379. def __init__(self, store=None, load=True, **kwargs):
  380. #: logger instance for logging global messages
  381. self.log = logging.getLogger('app')
  382. self._extensions = qubes.ext.get_extensions()
  383. #: collection of all VMs managed by this Qubes instance
  384. self.domains = VMCollection(self)
  385. #: collection of all available labels for VMs
  386. self.labels = {}
  387. #: collection of all pools
  388. self.pools = {}
  389. #: Connection to VMM
  390. self.vmm = VMMConnection()
  391. #: Information about host system
  392. self.host = QubesHost(self)
  393. if store is not None:
  394. self._store = store
  395. else:
  396. self._store = os.environ.get('QUBES_XML_PATH',
  397. os.path.join(
  398. qubes.config.system_path['qubes_base_dir'],
  399. qubes.config.system_path['qubes_store_filename']))
  400. super(Qubes, self).__init__(xml=None, **kwargs)
  401. self.__load_timestamp = None
  402. #: jinja2 environment for libvirt XML templates
  403. self.env = jinja2.Environment(
  404. loader=jinja2.FileSystemLoader('/usr/share/qubes/templates'),
  405. undefined=jinja2.StrictUndefined)
  406. if load:
  407. self.load()
  408. self.events_enabled = True
  409. @property
  410. def store(self):
  411. return self._store
  412. def load(self):
  413. '''Open qubes.xml
  414. :throws EnvironmentError: failure on parsing store
  415. :throws xml.parsers.expat.ExpatError: failure on parsing store
  416. :raises lxml.etree.XMLSyntaxError: on syntax error in qubes.xml
  417. '''
  418. try:
  419. fd = os.open(self._store, os.O_RDWR) # no O_CREAT
  420. except OSError as e:
  421. if e.errno != errno.ENOENT:
  422. raise
  423. raise qubes.exc.QubesException(
  424. 'Qubes XML store {!r} is missing; use qubes-create tool'.format(
  425. self._store))
  426. fh = os.fdopen(fd, 'rb')
  427. if os.name == 'posix':
  428. fcntl.lockf(fh, fcntl.LOCK_EX)
  429. elif os.name == 'nt':
  430. # pylint: disable=protected-access
  431. win32file.LockFileEx(
  432. win32file._get_osfhandle(fh.fileno()),
  433. win32con.LOCKFILE_EXCLUSIVE_LOCK,
  434. 0, -0x10000,
  435. pywintypes.OVERLAPPED())
  436. self.xml = lxml.etree.parse(fh)
  437. # stage 1: load labels and pools
  438. for node in self.xml.xpath('./labels/label'):
  439. label = qubes.Label.fromxml(node)
  440. self.labels[label.index] = label
  441. for node in self.xml.xpath('./pools/pool'):
  442. name = node.get('name')
  443. assert name, "Pool name '%s' is invalid " % name
  444. try:
  445. self.pools[name] = self._get_pool(**node.attrib)
  446. except qubes.exc.QubesException as e:
  447. self.log.error(e.message)
  448. # stage 2: load VMs
  449. for node in self.xml.xpath('./domains/domain'):
  450. # pylint: disable=no-member
  451. cls = self.get_vm_class(node.get('class'))
  452. vm = cls(self, node)
  453. vm.load_properties(load_stage=2)
  454. vm.init_log()
  455. self.domains.add(vm)
  456. if 0 not in self.domains:
  457. self.domains.add(qubes.vm.adminvm.AdminVM(
  458. self, None, qid=0, name='dom0'))
  459. # stage 3: load global properties
  460. self.load_properties(load_stage=3)
  461. # stage 4: fill all remaining VM properties
  462. for vm in self.domains:
  463. vm.load_properties(load_stage=4)
  464. # stage 5: misc fixups
  465. self.property_require('default_fw_netvm', allow_none=True)
  466. self.property_require('default_netvm', allow_none=True)
  467. self.property_require('default_template')
  468. self.property_require('clockvm', allow_none=True)
  469. self.property_require('updatevm', allow_none=True)
  470. # Disable ntpd in ClockVM - to not conflict with ntpdate (both are
  471. # using 123/udp port)
  472. if hasattr(self, 'clockvm') and self.clockvm is not None:
  473. if self.clockvm.features.get('services/ntpd', False):
  474. self.log.warning("VM set as clockvm ({!r}) has enabled 'ntpd' "
  475. "service! Expect failure when syncing time in dom0.".format(
  476. self.clockvm))
  477. else:
  478. self.clockvm.features['services/ntpd'] = ''
  479. for vm in self.domains:
  480. vm.events_enabled = True
  481. vm.fire_event('domain-load')
  482. # get a file timestamp (before closing it - still holding the lock!),
  483. # to detect whether anyone else have modified it in the meantime
  484. self.__load_timestamp = os.path.getmtime(self._store)
  485. # intentionally do not call explicit unlock
  486. fh.close()
  487. del fh
  488. def __xml__(self):
  489. element = lxml.etree.Element('qubes')
  490. element.append(self.xml_labels())
  491. pools_xml = lxml.etree.Element('pools')
  492. for pool in self.pools.values():
  493. pools_xml.append(pool.__xml__())
  494. element.append(pools_xml)
  495. element.append(self.xml_properties())
  496. domains = lxml.etree.Element('domains')
  497. for vm in self.domains:
  498. domains.append(vm.__xml__())
  499. element.append(domains)
  500. return element
  501. def save(self):
  502. '''Save all data to qubes.xml
  503. There are several problems with saving :file:`qubes.xml` which must be
  504. mitigated:
  505. - Running out of disk space. No space left should not result in empty
  506. file. This is done by writing to temporary file and then renaming.
  507. - Attempts to write two or more files concurrently. This is done by
  508. sophisticated locking.
  509. :throws EnvironmentError: failure on saving
  510. '''
  511. while True:
  512. fd_old = os.open(self._store, os.O_RDWR | os.O_CREAT)
  513. if os.name == 'posix':
  514. fcntl.lockf(fd_old, fcntl.LOCK_EX)
  515. elif os.name == 'nt':
  516. # pylint: disable=protected-access
  517. overlapped = pywintypes.OVERLAPPED()
  518. win32file.LockFileEx(
  519. win32file._get_osfhandle(fd_old),
  520. win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped)
  521. # While we were waiting for lock, someone could have unlink()ed (or
  522. # rename()d) our file out of the filesystem. We have to ensure we
  523. # got lock on something linked to filesystem. If not, try again.
  524. if os.fstat(fd_old) == os.stat(self._store):
  525. break
  526. else:
  527. os.close(fd_old)
  528. if self.__load_timestamp:
  529. current_file_timestamp = os.path.getmtime(self._store)
  530. if current_file_timestamp != self.__load_timestamp:
  531. os.close(fd_old)
  532. raise qubes.exc.QubesException(
  533. "Someone else modified qubes.xml in the meantime")
  534. fh_new = tempfile.NamedTemporaryFile(prefix=self._store, delete=False)
  535. lxml.etree.ElementTree(self.__xml__()).write(
  536. fh_new, encoding='utf-8', pretty_print=True)
  537. fh_new.flush()
  538. os.chmod(fh_new.name, 0660)
  539. os.chown(fh_new.name, -1, grp.getgrnam('qubes').gr_gid)
  540. os.rename(fh_new.name, self._store)
  541. # intentionally do not call explicit unlock to not unlock the file
  542. # before all buffers are flushed
  543. fh_new.close()
  544. # update stored mtime, in case of multiple save() calls without
  545. # loading qubes.xml again
  546. self.__load_timestamp = os.path.getmtime(self._store)
  547. os.close(fd_old)
  548. @classmethod
  549. def create_empty_store(cls, *args, **kwargs):
  550. self = cls(*args, load=False, **kwargs)
  551. self.labels = {
  552. 1: qubes.Label(1, '0xcc0000', 'red'),
  553. 2: qubes.Label(2, '0xf57900', 'orange'),
  554. 3: qubes.Label(3, '0xedd400', 'yellow'),
  555. 4: qubes.Label(4, '0x73d216', 'green'),
  556. 5: qubes.Label(5, '0x555753', 'gray'),
  557. 6: qubes.Label(6, '0x3465a4', 'blue'),
  558. 7: qubes.Label(7, '0x75507b', 'purple'),
  559. 8: qubes.Label(8, '0x000000', 'black'),
  560. }
  561. for name, config in qubes.config.defaults['pool_configs'].items():
  562. self.pools[name] = self._get_pool(**config)
  563. self.domains.add(
  564. qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0'))
  565. self.save()
  566. return self
  567. def xml_labels(self):
  568. '''Serialise labels
  569. :rtype: lxml.etree._Element
  570. '''
  571. labels = lxml.etree.Element('labels')
  572. for label in sorted(self.labels.values(), key=lambda labl: labl.index):
  573. labels.append(label.__xml__())
  574. return labels
  575. def get_vm_class(self, clsname):
  576. '''Find the class for a domain.
  577. Classess are registered as setuptools' entry points in ``qubes.vm``
  578. group. Any package may supply their own classess.
  579. :param str clsname: name of the class
  580. :return type: class
  581. '''
  582. try:
  583. return qubes.utils.get_entry_point_one('qubes.vm', clsname)
  584. except KeyError:
  585. raise qubes.exc.QubesException(
  586. 'no such VM class: {!r}'.format(clsname))
  587. # don't catch TypeError
  588. def add_new_vm(self, cls, qid=None, **kwargs):
  589. '''Add new Virtual Machine to colletion
  590. '''
  591. if qid is None:
  592. qid = self.domains.get_new_unused_qid()
  593. # handle default template; specifically allow template=None (do not
  594. # override it with default template)
  595. if 'template' not in kwargs and hasattr(cls, 'template'):
  596. kwargs['template'] = self.default_template
  597. return self.domains.add(cls(self, None, qid=qid, **kwargs))
  598. def get_label(self, label):
  599. '''Get label as identified by index or name
  600. :throws KeyError: when label is not found
  601. '''
  602. # first search for index, verbatim
  603. try:
  604. return self.labels[label]
  605. except KeyError:
  606. pass
  607. # then search for name
  608. for i in self.labels.values():
  609. if i.name == label:
  610. return i
  611. # last call, if label is a number represented as str, search in indices
  612. try:
  613. return self.labels[int(label)]
  614. except (KeyError, ValueError):
  615. pass
  616. raise KeyError(label)
  617. def add_pool(self, **kwargs):
  618. """ Add a storage pool to config."""
  619. name = kwargs['name']
  620. assert name not in self.pools.keys(), \
  621. "Pool named %s already exists" % name
  622. pool = self._get_pool(**kwargs)
  623. pool.setup()
  624. self.pools[name] = pool
  625. def remove_pool(self, name):
  626. """ Remove a storage pool from config file. """
  627. try:
  628. pool = self.pools[name]
  629. del self.pools[name]
  630. pool.destroy()
  631. except KeyError:
  632. return
  633. def get_pool(self, name):
  634. ''' Returns a :py:class:`qubes.storage.Pool` instance '''
  635. try:
  636. return self.pools[name]
  637. except KeyError:
  638. raise qubes.exc.QubesException('Unknown storage pool ' + name)
  639. def _get_pool(self, **kwargs):
  640. try:
  641. name = kwargs['name']
  642. assert name, 'Name needs to be an non empty string'
  643. except KeyError:
  644. raise qubes.exc.QubesException('No pool name for pool')
  645. try:
  646. driver = kwargs['driver']
  647. except KeyError:
  648. raise qubes.exc.QubesException('No driver specified for pool ' +
  649. name)
  650. try:
  651. klass = qubes.utils.get_entry_point_one(
  652. qubes.storage.STORAGE_ENTRY_POINT, driver)
  653. del kwargs['driver']
  654. return klass(**kwargs)
  655. except KeyError:
  656. raise qubes.exc.QubesException('Driver %s for pool %s' %
  657. (driver, name))
  658. @qubes.events.handler('domain-pre-delete')
  659. def on_domain_pre_deleted(self, event, vm):
  660. # pylint: disable=unused-argument
  661. if isinstance(vm, qubes.vm.templatevm.TemplateVM):
  662. appvms = self.domains.get_vms_based_on(vm)
  663. if appvms:
  664. raise qubes.exc.QubesException(
  665. 'Cannot remove template that has dependent AppVMs. '
  666. 'Affected are: {}'.format(', '.join(
  667. vm.name for name in sorted(appvms))))
  668. @qubes.events.handler('domain-delete')
  669. def on_domain_deleted(self, event, vm):
  670. # pylint: disable=unused-argument
  671. for propname in (
  672. 'default_netvm',
  673. 'default_fw_netvm',
  674. 'clockvm',
  675. 'updatevm',
  676. 'default_template',
  677. ):
  678. try:
  679. if getattr(self, propname) == vm:
  680. delattr(self, propname)
  681. except AttributeError:
  682. pass
  683. @qubes.events.handler('property-pre-set:clockvm')
  684. def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None):
  685. # pylint: disable=unused-argument,no-self-use
  686. if newvalue is None:
  687. return
  688. if newvalue.features.get('services/ntpd', False):
  689. raise qubes.exc.QubesVMError(newvalue,
  690. 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format(
  691. newvalue.name, name))
  692. else:
  693. newvalue.features['services/ntpd'] = ''
  694. @qubes.events.handler(
  695. 'property-pre-set:default_netvm',
  696. 'property-pre-set:default_fw_netvm')
  697. def on_property_pre_set_default_netvm(self, event, name, newvalue,
  698. oldvalue=None):
  699. # pylint: disable=unused-argument,invalid-name
  700. if newvalue is not None and oldvalue is not None \
  701. and oldvalue.is_running() and not newvalue.is_running() \
  702. and self.domains.get_vms_connected_to(oldvalue):
  703. raise qubes.exc.QubesVMNotRunningError(newvalue,
  704. 'Cannot change {!r} to domain that '
  705. 'is not running ({!r}).'.format(name, newvalue.name))
  706. @qubes.events.handler('property-set:default_fw_netvm')
  707. def on_property_set_default_fw_netvm(self, event, name, newvalue,
  708. oldvalue=None):
  709. # pylint: disable=unused-argument,invalid-name
  710. for vm in self.domains:
  711. if not vm.provides_network and vm.property_is_default('netvm'):
  712. # fire property-del:netvm as it is responsible for resetting
  713. # netvm to it's default value
  714. vm.fire_event('property-del:netvm', 'netvm', newvalue, oldvalue)
  715. @qubes.events.handler('property-set:default_netvm')
  716. def on_property_set_default_netvm(self, event, name, newvalue,
  717. oldvalue=None):
  718. # pylint: disable=unused-argument
  719. for vm in self.domains:
  720. if vm.provides_network and vm.property_is_default('netvm'):
  721. # fire property-del:netvm as it is responsible for resetting
  722. # netvm to it's default value
  723. vm.fire_event('property-del:netvm', 'netvm', newvalue, oldvalue)