class TemplateVM(QubesVM): '''Template for AppVM''' dir_path_prefix = qubes.config.system_path['qubes_templates_dir'] @property def appvms(self): ''' Returns a generator containing all domains based on the current TemplateVM. ''' for vm in self.app.domains: if hasattr(vm, 'template') and vm.template is self: yield vm netvm = qubes.VMProperty('netvm', load_stage=4, allow_none=True, default=None, # pylint: disable=protected-access setter=qubes.vm.qubesvm.QubesVM.netvm._setter, doc='VM that provides network connection to this domain. When ' '`None`, machine is disconnected.') def __init__(self, *args, **kwargs): assert 'template' not in kwargs, "A TemplateVM can not have a template" self.volume_config = { 'root': { 'name': 'root', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['root_img_size'], }, 'private': { 'name': 'private', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['private_img_size'], 'revisions_to_keep': 0, }, 'volatile': { 'name': 'volatile', 'size': defaults['root_img_size'], 'snap_on_start': False, 'save_on_stop': False, 'rw': True, }, 'kernel': { 'name': 'kernel', 'snap_on_start': False, 'save_on_stop': False, 'rw': False } } super(TemplateVM, self).__init__(*args, **kwargs)
class NetVMMixin(qubes.events.Emitter): ''' Mixin containing network functionality ''' mac = qubes.property('mac', type=str, default='00:16:3E:5E:6C:00', setter=_setter_mac, doc='MAC address of the NIC emulated inside VM') ip = qubes.property('ip', type=ipaddress.IPv4Address, default=_default_ip, doc='IP address of this domain.') ip6 = qubes.property('ip6', type=ipaddress.IPv6Address, default=_default_ip6, doc='IPv6 address of this domain.') # CORE2: swallowed uses_default_netvm netvm = qubes.VMProperty( 'netvm', load_stage=4, allow_none=True, default=(lambda self: self.app.default_netvm), setter=_setter_netvm, doc='''VM that provides network connection to this domain. When `None`, machine is disconnected. When absent, domain uses default NetVM.''') provides_network = qubes.property( 'provides_network', default=False, type=bool, setter=qubes.property.bool, doc='''If this domain can act as network provider (formerly known as NetVM or ProxyVM)''') @property def firewall_conf(self): return 'firewall.xml' # # used in networked appvms or proxyvms (netvm is not None) # @qubes.stateless_property def visible_ip(self): '''IP address of this domain as seen by the domain.''' return self.features.check_with_template('net.fake-ip', None) or \ self.ip @qubes.stateless_property def visible_ip6(self): '''IPv6 address of this domain as seen by the domain.''' return self.ip6 @qubes.stateless_property def visible_gateway(self): '''Default gateway of this domain as seen by the domain.''' return self.features.check_with_template('net.fake-gateway', None) or \ (self.netvm.gateway if self.netvm else None) @qubes.stateless_property def visible_gateway6(self): '''Default (IPv6) gateway of this domain as seen by the domain.''' if self.features.check_with_netvm('ipv6', False): return self.netvm.gateway6 if self.netvm else None return None @qubes.stateless_property def visible_netmask(self): '''Netmask as seen by the domain.''' return self.features.check_with_template('net.fake-netmask', None) or \ (self.netvm.netmask if self.netvm else None) # # used in netvms (provides_network=True) # those properties and methods are most likely accessed as vm.netvm.<prop> # @staticmethod def get_ip_for_vm(vm): '''Get IP address for (appvm) domain connected to this (netvm) domain. ''' import qubes.vm.dispvm # pylint: disable=redefined-outer-name if isinstance(vm, qubes.vm.dispvm.DispVM): return ipaddress.IPv4Address('10.138.{}.{}'.format( (vm.dispid >> 8) & 0xff, vm.dispid & 0xff)) # VM technically can get address which ends in '.0'. This currently # does not happen, because qid < 253, but may happen in the future. return ipaddress.IPv4Address('10.137.{}.{}'.format( (vm.qid >> 8) & 0xff, vm.qid & 0xff)) @staticmethod def get_ip6_for_vm(vm): '''Get IPv6 address for (appvm) domain connected to this (netvm) domain. Default address is constructed with Qubes-specific site-local prefix, and IPv4 suffix (0xa89 is 10.137.). ''' import qubes.vm.dispvm # pylint: disable=redefined-outer-name if isinstance(vm, qubes.vm.dispvm.DispVM): return ipaddress.IPv6Address('{}::a8a:{:x}'.format( qubes.config.qubes_ipv6_prefix, vm.dispid)) return ipaddress.IPv6Address('{}::a89:{:x}'.format( qubes.config.qubes_ipv6_prefix, vm.qid)) @qubes.stateless_property def gateway(self): '''Gateway for other domains that use this domain as netvm.''' return self.visible_ip if self.provides_network else None @qubes.stateless_property def gateway6(self): '''Gateway (IPv6) for other domains that use this domain as netvm.''' if self.features.check_with_netvm('ipv6', False): return self.visible_ip6 if self.provides_network else \ None return None @property def netmask(self): '''Netmask for gateway address.''' return '255.255.255.255' if self.is_networked() else None @property def connected_vms(self): ''' Return a generator containing all domains connected to the current NetVM. ''' for vm in self.app.domains: if getattr(vm, 'netvm', None) is self: yield vm # # used in both # @property def dns(self): '''Secondary DNS server set up for this domain.''' if self.netvm is not None or self.provides_network: return ( '10.139.1.1', '10.139.1.2', ) return None def __init__(self, *args, **kwargs): self._firewall = None super(NetVMMixin, self).__init__(*args, **kwargs) @qubes.events.handler('domain-load') def on_domain_load_netvm_loop_check(self, event): # pylint: disable=unused-argument # make sure there are no netvm loops - which could cause qubesd # looping infinitely if self is self.netvm: self.log.error( 'vm \'%s\' network-connected to itself, breaking the ' 'connection', self.name) self.netvm = None elif self.netvm in self.app.domains.get_vms_connected_to(self): self.log.error( 'netvm loop detected on \'%s\', breaking the connection', self.name) self.netvm = None @qubes.events.handler('domain-start') def on_domain_started(self, event, **kwargs): '''Connect this domain to its downstream domains. Also reload firewall in its netvm. This is needed when starting netvm *after* its connected domains. ''' # pylint: disable=unused-argument if self.netvm: self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member for vm in self.connected_vms: if not vm.is_running(): continue vm.log.info('Attaching network') try: vm.detach_network() except (qubes.exc.QubesException, libvirt.libvirtError): vm.log.warning('Cannot detach old network', exc_info=1) try: vm.attach_network() except (qubes.exc.QubesException, libvirt.libvirtError): vm.log.warning('Cannot attach network', exc_info=1) @qubes.events.handler('domain-pre-shutdown') def shutdown_net(self, event, force=False): ''' Checks before NetVM shutdown if any connected domains are running. If `force` is `True` tries to detach network interfaces of connected vms ''' # pylint: disable=unused-argument connected_vms = [vm for vm in self.connected_vms if vm.is_running()] if connected_vms and not force: raise qubes.exc.QubesVMError( self, 'There are other VMs connected to this VM: {}'.format( ', '.join(vm.name for vm in connected_vms))) # SEE: 1426 # detach network interfaces of connected VMs before shutting down, # otherwise libvirt will not notice it and will try to detach them # again (which would fail, obviously). # This code can be removed when #1426 got implemented for vm in connected_vms: if vm.is_running(): try: vm.detach_network() except (qubes.exc.QubesException, libvirt.libvirtError): # ignore errors pass def attach_network(self): '''Attach network in this machine to it's netvm.''' if not self.is_running(): raise qubes.exc.QubesVMNotRunningError(self) if self.netvm is None: raise qubes.exc.QubesVMError( self, 'netvm should not be {}'.format(self.netvm)) if not self.netvm.is_running(): # pylint: disable=no-member # pylint: disable=no-member self.log.info('Starting NetVM ({0})'.format(self.netvm.name)) self.netvm.start() self.netvm.set_mapped_ip_info_for_vm(self) self.libvirt_domain.attachDevice( self.app.env.get_template('libvirt/devices/net.xml').render( vm=self)) def detach_network(self): '''Detach machine from it's netvm''' if not self.is_running(): raise qubes.exc.QubesVMNotRunningError(self) if self.netvm is None: raise qubes.exc.QubesVMError( self, 'netvm should not be {}'.format(self.netvm)) self.libvirt_domain.detachDevice( self.app.env.get_template('libvirt/devices/net.xml').render( vm=self)) def is_networked(self): '''Check whether this VM can reach network (firewall notwithstanding). :returns: :py:obj:`True` if is machine can reach network, \ :py:obj:`False` otherwise. :rtype: bool ''' if self.provides_network: return True return self.netvm is not None def reload_firewall_for_vm(self, vm): ''' Reload the firewall rules for the vm ''' if not self.is_running(): return for addr_family in (4, 6): ip = vm.ip6 if addr_family == 6 else vm.ip if ip is None: continue base_dir = '/qubes-firewall/{}/'.format(ip) # remove old entries if any (but don't touch base empty entry - it # would trigger reload right away self.untrusted_qdb.rm(base_dir) # write new rules for key, value in vm.firewall.qdb_entries( addr_family=addr_family).items(): self.untrusted_qdb.write(base_dir + key, value) # signal its done self.untrusted_qdb.write(base_dir[:-1], '') def set_mapped_ip_info_for_vm(self, vm): ''' Set configuration to possibly hide real IP from the VM. This needs to be done before executing 'script' (`/etc/xen/scripts/vif-route-qubes`) in network providing VM ''' # add info about remapped IPs (VM IP hidden from the VM itself) mapped_ip_base = '/mapped-ip/{}'.format(vm.ip) if vm.visible_ip: self.untrusted_qdb.write(mapped_ip_base + '/visible-ip', str(vm.visible_ip)) else: self.untrusted_qdb.rm(mapped_ip_base + '/visible-ip') if vm.visible_gateway: self.untrusted_qdb.write(mapped_ip_base + '/visible-gateway', str(vm.visible_gateway)) else: self.untrusted_qdb.rm(mapped_ip_base + '/visible-gateway') @qubes.events.handler('property-pre-del:netvm') def on_property_pre_del_netvm(self, event, name, oldvalue=None): ''' Sets the the NetVM to default NetVM ''' # pylint: disable=unused-argument # we are changing to default netvm newvalue = type(self).netvm.get_default(self) # check for netvm loop _setter_netvm(self, type(self).netvm, newvalue) if newvalue == oldvalue: return self.fire_event('property-pre-set:netvm', pre_event=True, name='netvm', newvalue=newvalue, oldvalue=oldvalue) @qubes.events.handler('property-del:netvm') def on_property_del_netvm(self, event, name, oldvalue=None): ''' Sets the the NetVM to default NetVM ''' # pylint: disable=unused-argument # we are changing to default netvm newvalue = self.netvm if newvalue == oldvalue: return self.fire_event('property-set:netvm', name='netvm', newvalue=newvalue, oldvalue=oldvalue) @qubes.events.handler('property-pre-set:netvm') def on_property_pre_set_netvm(self, event, name, newvalue, oldvalue=None): ''' Run sanity checks before setting a new NetVM ''' # pylint: disable=unused-argument if newvalue is not None: if not self.app.vmm.offline_mode \ and self.is_running() and not newvalue.is_running(): raise qubes.exc.QubesVMNotStartedError( newvalue, 'Cannot dynamically attach to stopped NetVM: {!r}'.format( newvalue)) # don't check oldvalue, because it's missing if it was default if self.netvm is not None: if self.is_running(): self.detach_network() @qubes.events.handler('property-set:netvm') def on_property_set_netvm(self, event, name, newvalue, oldvalue=None): ''' Replaces the current NetVM with a new one and fires net-domain-connect event ''' # pylint: disable=unused-argument if newvalue is None: return if self.is_running(): # refresh IP, DNS etc self.create_qdb_entries() self.attach_network() newvalue.fire_event('net-domain-connect', vm=self) @qubes.events.handler('net-domain-connect') def on_net_domain_connect(self, event, vm): ''' Reloads the firewall config for vm ''' # pylint: disable=unused-argument self.reload_firewall_for_vm(vm) @qubes.events.handler('domain-qdb-create') def on_domain_qdb_create(self, event): ''' Fills the QubesDB with firewall entries. ''' # pylint: disable=unused-argument for vm in self.connected_vms: if vm.is_running(): # keep in sync with on_firewall_changed self.set_mapped_ip_info_for_vm(vm) self.reload_firewall_for_vm(vm) @qubes.events.handler('firewall-changed', 'domain-spawn') def on_firewall_changed(self, event, **kwargs): ''' Reloads the firewall if vm is running and has a NetVM assigned ''' # pylint: disable=unused-argument if self.is_running() and self.netvm: self.netvm.set_mapped_ip_info_for_vm(self) self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member # CORE2: swallowed get_firewall_conf, write_firewall_conf, # get_firewall_defaults @property def firewall(self): if self._firewall is None: self._firewall = qubes.firewall.Firewall(self) return self._firewall def has_firewall(self): ''' Return `True` if there are some vm specific firewall rules set ''' return os.path.exists(os.path.join(self.dir_path, self.firewall_conf))
class Qubes(qubes.PropertyHolder): '''Main Qubes application :param str store: path to ``qubes.xml`` The store is loaded in stages: 1. In the first stage there are loaded some basic features from store (currently labels). 2. In the second stage stubs for all VMs are loaded. They are filled with their basic properties, like ``qid`` and ``name``. 3. In the third stage all global properties are loaded. They often reference VMs, like default netvm, so they should be filled after loading VMs. 4. In the fourth stage all remaining VM properties are loaded. They also need all VMs loaded, because they represent dependencies between VMs like aforementioned netvm. 5. In the fifth stage there are some fixups to ensure sane system operation. This class emits following events: .. event:: domain-add (subject, event, vm) When domain is added. :param subject: Event emitter :param event: Event name (``'domain-add'``) :param vm: Domain object .. event:: domain-pre-delete (subject, event, vm) When domain is deleted. VM still has reference to ``app`` object, and is contained within VMCollection. You may prevent removal by raising an exception. :param subject: Event emitter :param event: Event name (``'domain-pre-delete'``) :param vm: Domain object .. event:: domain-delete (subject, event, vm) When domain is deleted. VM still has reference to ``app`` object, but is not contained within VMCollection. :param subject: Event emitter :param event: Event name (``'domain-delete'``) :param vm: Domain object Methods and attributes: ''' default_netvm = qubes.VMProperty('default_netvm', load_stage=3, default=None, allow_none=True, setter=_setter_default_netvm, doc='''Default NetVM for AppVMs. Initial state is `None`, which means that AppVMs are not connected to the Internet.''') default_template = qubes.VMProperty('default_template', load_stage=3, vmclass=qubes.vm.templatevm.TemplateVM, doc='Default template for new AppVMs') updatevm = qubes.VMProperty('updatevm', load_stage=3, default=None, allow_none=True, doc='''Which VM to use as `yum` proxy for updating AdminVM and TemplateVMs''') clockvm = qubes.VMProperty('clockvm', load_stage=3, default=None, allow_none=True, doc='Which VM to use as NTP proxy for updating AdminVM') default_kernel = qubes.property('default_kernel', load_stage=3, doc='Which kernel to use when not overriden in VM') default_dispvm = qubes.VMProperty('default_dispvm', load_stage=3, default=None, doc='Default DispVM base for service calls', allow_none=True) default_pool = qubes.property('default_pool', load_stage=3, default=_default_pool, setter=_setter_pool, doc='Default storage pool') default_pool_private = qubes.property('default_pool_private', load_stage=3, default=lambda app: app.default_pool, setter=_setter_pool, doc='Default storage pool for private volumes') default_pool_root = qubes.property('default_pool_root', load_stage=3, default=lambda app: app.default_pool, setter=_setter_pool, doc='Default storage pool for root volumes') default_pool_volatile = qubes.property('default_pool_volatile', load_stage=3, default=lambda app: app.default_pool, setter=_setter_pool, doc='Default storage pool for volatile volumes') default_pool_kernel = qubes.property('default_pool_kernel', load_stage=3, default=lambda app: app.default_pool, setter=_setter_pool, doc='Default storage pool for kernel volumes') default_qrexec_timeout = qubes.property('default_qrexec_timeout', load_stage=3, default=60, type=int, doc='''Default time in seconds after which qrexec connection attempt is deemed failed''') stats_interval = qubes.property('stats_interval', default=3, type=int, doc='Interval in seconds for VM stats reporting (memory, CPU usage)') # TODO #1637 #892 check_updates_vm = qubes.property('check_updates_vm', type=bool, setter=qubes.property.bool, default=True, doc='check for updates inside qubes') def __init__(self, store=None, load=True, offline_mode=None, lock=False, **kwargs): #: logger instance for logging global messages self.log = logging.getLogger('app') self.log.debug('init() -> %#x', id(self)) self.log.debug('stack:') for frame in traceback.extract_stack(): self.log.debug('%s', frame) self._extensions = qubes.ext.get_extensions() #: collection of all VMs managed by this Qubes instance self.domains = VMCollection(self) #: collection of all available labels for VMs self.labels = {} #: collection of all pools self.pools = {} #: Connection to VMM self.vmm = VMMConnection(offline_mode=offline_mode) #: Information about host system self.host = QubesHost(self) if store is not None: self._store = store else: self._store = os.environ.get('QUBES_XML_PATH', os.path.join( qubes.config.qubes_base_dir, qubes.config.system_path['qubes_store_filename'])) super(Qubes, self).__init__(xml=None, **kwargs) self.__load_timestamp = None self.__locked_fh = None self._domain_event_callback_id = None #: jinja2 environment for libvirt XML templates self.env = jinja2.Environment( loader=jinja2.FileSystemLoader([ '/etc/qubes/templates', '/usr/share/qubes/templates', ]), undefined=jinja2.StrictUndefined) if load: self.load(lock=lock) self.events_enabled = True @property def store(self): return self._store def _migrate_global_properties(self): '''Migrate renamed/dropped properties''' if self.xml is None: return # drop default_fw_netvm node_default_fw_netvm = self.xml.find( './properties/property[@name=\'default_fw_netvm\']') if node_default_fw_netvm is not None: node_default_netvm = self.xml.find( './properties/property[@name=\'default_netvm\']') try: default_fw_netvm = self.domains[node_default_fw_netvm.text] if node_default_netvm is None: default_netvm = None else: default_netvm = self.domains[node_default_netvm.text] if default_netvm != default_fw_netvm: for vm in self.domains: if not hasattr(vm, 'netvm'): continue if not getattr(vm, 'provides_network', False): continue node_netvm = vm.xml.find( './properties/property[@name=\'netvm\']') if node_netvm is not None: # non-default netvm continue # this will unfortunately break "being default" # property state, but the alternative (changing # value behind user's back) is worse properties = vm.xml.find('./properties') element = lxml.etree.Element('property', name='netvm') element.text = default_fw_netvm.name # manipulate xml directly, before loading netvm # property, to avoid hitting netvm loop detection properties.append(element) except KeyError: # if default_fw_netvm was set to invalid value, simply # drop it pass node_default_fw_netvm.getparent().remove(node_default_fw_netvm) def load(self, lock=False): '''Open qubes.xml :throws EnvironmentError: failure on parsing store :throws xml.parsers.expat.ExpatError: failure on parsing store :raises lxml.etree.XMLSyntaxError: on syntax error in qubes.xml ''' fh = self._acquire_lock() self.xml = lxml.etree.parse(fh) # stage 1: load labels and pools for node in self.xml.xpath('./labels/label'): label = qubes.Label.fromxml(node) self.labels[label.index] = label for node in self.xml.xpath('./pools/pool'): name = node.get('name') assert name, "Pool name '%s' is invalid " % name try: self.pools[name] = self._get_pool(**node.attrib) except qubes.exc.QubesException as e: self.log.error(str(e)) # stage 2: load VMs for node in self.xml.xpath('./domains/domain'): # pylint: disable=no-member cls = self.get_vm_class(node.get('class')) vm = cls(self, node) vm.load_properties(load_stage=2) vm.init_log() self.domains.add(vm, _enable_events=False) if 0 not in self.domains: self.domains.add( qubes.vm.adminvm.AdminVM(self, None), _enable_events=False) self._migrate_global_properties() # stage 3: load global properties self.load_properties(load_stage=3) # stage 4: fill all remaining VM properties for vm in self.domains: vm.load_properties(load_stage=4) vm.load_extras() # stage 5: misc fixups self.property_require('default_netvm', allow_none=True) self.property_require('default_template') self.property_require('clockvm', allow_none=True) self.property_require('updatevm', allow_none=True) for vm in self.domains: vm.events_enabled = True vm.fire_event('domain-load') # get a file timestamp (before closing it - still holding the lock!), # to detect whether anyone else have modified it in the meantime self.__load_timestamp = os.path.getmtime(self._store) if not lock: self._release_lock() def __xml__(self): element = lxml.etree.Element('qubes') element.append(self.xml_labels()) pools_xml = lxml.etree.Element('pools') for pool in self.pools.values(): xml = pool.__xml__() if xml is not None: pools_xml.append(xml) element.append(pools_xml) element.append(self.xml_properties()) domains = lxml.etree.Element('domains') for vm in self.domains: domains.append(vm.__xml__()) element.append(domains) return element def __str__(self): return type(self).__name__ def save(self, lock=True): '''Save all data to qubes.xml There are several problems with saving :file:`qubes.xml` which must be mitigated: - Running out of disk space. No space left should not result in empty file. This is done by writing to temporary file and then renaming. - Attempts to write two or more files concurrently. This is done by sophisticated locking. :param bool lock: keep file locked after saving :throws EnvironmentError: failure on saving ''' if not self.__locked_fh: self._acquire_lock(for_save=True) fh_new = tempfile.NamedTemporaryFile( prefix=self._store, delete=False) lxml.etree.ElementTree(self.__xml__()).write( fh_new, encoding='utf-8', pretty_print=True) fh_new.flush() try: os.chown(fh_new.name, -1, grp.getgrnam('qubes').gr_gid) os.chmod(fh_new.name, 0o660) except KeyError: # group 'qubes' not found # don't change mode if no 'qubes' group in the system pass os.rename(fh_new.name, self._store) # update stored mtime, in case of multiple save() calls without # loading qubes.xml again self.__load_timestamp = os.path.getmtime(self._store) # this releases lock for all other processes, # but they should instantly block on the new descriptor self.__locked_fh.close() self.__locked_fh = fh_new if not lock: self._release_lock() def close(self): '''Deconstruct the object and break circular references After calling this the object is unusable, not even for saving.''' self.log.debug('close() <- %#x', id(self)) for frame in traceback.extract_stack(): self.log.debug('%s', frame) super().close() if self._domain_event_callback_id is not None: self.vmm.libvirt_conn.domainEventDeregisterAny( self._domain_event_callback_id) self._domain_event_callback_id = None # Only our Lord, The God Almighty, knows what references # are kept in extensions. del self._extensions for vm in self.domains: vm.close() self.domains.close() del self.domains self.vmm.close() del self.vmm del self.host if self.__locked_fh: self._release_lock() def _acquire_lock(self, for_save=False): assert self.__locked_fh is None, 'double lock' while True: try: fd = os.open(self._store, os.O_RDWR | (os.O_CREAT * int(for_save))) except FileNotFoundError: if not for_save: raise qubes.exc.QubesException( 'Qubes XML store {!r} is missing; ' 'use qubes-create tool'.format(self._store)) raise # While we were waiting for lock, someone could have unlink()ed # (or rename()d) our file out of the filesystem. We have to # ensure we got lock on something linked to filesystem. # If not, try again. if os.fstat(fd) != os.stat(self._store): os.close(fd) continue if self.__load_timestamp and \ os.path.getmtime(self._store) != self.__load_timestamp: os.close(fd) raise qubes.exc.QubesException( 'Someone else modified qubes.xml in the meantime') break if os.name == 'posix': fcntl.lockf(fd, fcntl.LOCK_EX) elif os.name == 'nt': # pylint: disable=protected-access overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx( win32file._get_osfhandle(fd), win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped) self.__locked_fh = os.fdopen(fd, 'r+b') return self.__locked_fh def _release_lock(self): assert self.__locked_fh is not None, 'double release' # intentionally do not call explicit unlock to not unlock the file # before all buffers are flushed self.__locked_fh.close() self.__locked_fh = None def load_initial_values(self): self.labels = { 1: qubes.Label(1, '0xcc0000', 'red'), 2: qubes.Label(2, '0xf57900', 'orange'), 3: qubes.Label(3, '0xedd400', 'yellow'), 4: qubes.Label(4, '0x73d216', 'green'), 5: qubes.Label(5, '0x555753', 'gray'), 6: qubes.Label(6, '0x3465a4', 'blue'), 7: qubes.Label(7, '0x75507b', 'purple'), 8: qubes.Label(8, '0x000000', 'black'), } assert max(self.labels.keys()) == qubes.config.max_default_label pool_configs = copy.deepcopy(qubes.config.defaults['pool_configs']) root_volume_group, root_thin_pool = \ qubes.storage.DirectoryThinPool.thin_pool('/') if root_thin_pool: lvm_config = { 'name': 'lvm', 'driver': 'lvm_thin', 'volume_group': root_volume_group, 'thin_pool': root_thin_pool } pool_configs[lvm_config['name']] = lvm_config for name, config in pool_configs.items(): if 'driver' not in config and 'dir_path' in config: config['driver'] = 'file' try: os.makedirs(config['dir_path'], exist_ok=True) if qubes.storage.reflink.is_supported(config['dir_path']): config['driver'] = 'file-reflink' config['setup_check'] = 'no' # don't check twice except PermissionError: # looks like a testing environment pass # stay with 'file' self.pools[name] = self._get_pool(**config) self.default_pool_kernel = 'linux-kernel' self.domains.add( qubes.vm.adminvm.AdminVM(self, None, label='black')) @classmethod def create_empty_store(cls, *args, **kwargs): self = cls(*args, load=False, **kwargs) if os.path.exists(self.store): raise qubes.exc.QubesException( '{} already exists, aborting'.format(self.store)) self.load_initial_values() # TODO py3 get lock= as keyword-only arg self.save(kwargs.get('lock')) return self def xml_labels(self): '''Serialise labels :rtype: lxml.etree._Element ''' labels = lxml.etree.Element('labels') for label in sorted(self.labels.values(), key=lambda labl: labl.index): labels.append(label.__xml__()) return labels @staticmethod def get_vm_class(clsname): '''Find the class for a domain. Classes are registered as setuptools' entry points in ``qubes.vm`` group. Any package may supply their own classes. :param str clsname: name of the class :return type: class ''' try: return qubes.utils.get_entry_point_one( qubes.vm.VM_ENTRY_POINT, clsname) except KeyError: raise qubes.exc.QubesException( 'no such VM class: {!r}'.format(clsname)) # don't catch TypeError def add_new_vm(self, cls, qid=None, **kwargs): '''Add new Virtual Machine to collection ''' if qid is None: qid = self.domains.get_new_unused_qid() if isinstance(cls, str): cls = self.get_vm_class(cls) # handle default template; specifically allow template=None (do not # override it with default template) if 'template' not in kwargs and hasattr(cls, 'template'): if cls == self.get_vm_class('DispVM'): kwargs['template'] = self.default_dispvm else: kwargs['template'] = self.default_template elif 'template' in kwargs and isinstance(kwargs['template'], str): kwargs['template'] = self.domains[kwargs['template']] return self.domains.add(cls(self, None, qid=qid, **kwargs)) def get_label(self, label): '''Get label as identified by index or name :throws KeyError: when label is not found ''' # first search for index, verbatim try: return self.labels[label] except KeyError: pass # then search for name for i in self.labels.values(): if i.name == label: return i # last call, if label is a number represented as str, search in indices try: return self.labels[int(label)] except (KeyError, ValueError): pass raise KeyError(label) def setup_pools(self): """ Run implementation specific setup for each storage pool. """ for pool in self.pools.values(): pool.setup() def add_pool(self, name, **kwargs): """ Add a storage pool to config.""" if name in self.pools.keys(): raise qubes.exc.QubesException('pool named %s already exists \n' % name) kwargs['name'] = name pool = self._get_pool(**kwargs) pool.setup() self.pools[name] = pool return pool def remove_pool(self, name): """ Remove a storage pool from config file. """ try: pool = self.pools[name] del self.pools[name] pool.destroy() except KeyError: return def get_pool(self, pool): ''' Returns a :py:class:`qubes.storage.Pool` instance ''' if isinstance(pool, qubes.storage.Pool): return pool try: return self.pools[pool] except KeyError: raise qubes.exc.QubesException('Unknown storage pool ' + pool) @staticmethod def _get_pool(**kwargs): try: name = kwargs['name'] assert name, 'Name needs to be an non empty string' except KeyError: raise qubes.exc.QubesException('No pool name for pool') try: driver = kwargs['driver'] except KeyError: raise qubes.exc.QubesException('No driver specified for pool ' + name) try: klass = qubes.utils.get_entry_point_one( qubes.storage.STORAGE_ENTRY_POINT, driver) del kwargs['driver'] return klass(**kwargs) except KeyError: raise qubes.exc.QubesException('No driver %s for pool %s' % (driver, name)) def register_event_handlers(self): '''Register libvirt event handlers, which will translate libvirt events into qubes.events. This function should be called only in 'qubesd' process and only when mainloop has been already set. ''' self._domain_event_callback_id = ( self.vmm.libvirt_conn.domainEventRegisterAny( None, # any domain libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE, self._domain_event_callback, None)) def _domain_event_callback(self, _conn, domain, event, _detail, _opaque): '''Generic libvirt event handler (virConnectDomainEventCallback), translate libvirt event into qubes.events. ''' if not self.events_enabled: return try: vm = self.domains[domain.name()] except KeyError: # ignore events for unknown domains return if event == libvirt.VIR_DOMAIN_EVENT_STOPPED: vm.on_libvirt_domain_stopped() elif event == libvirt.VIR_DOMAIN_EVENT_SUSPENDED: try: vm.fire_event('domain-paused') except Exception: # pylint: disable=broad-except self.log.exception( 'Uncaught exception from domain-paused handler ' 'for domain %s', vm.name) elif event == libvirt.VIR_DOMAIN_EVENT_RESUMED: try: vm.fire_event('domain-unpaused') except Exception: # pylint: disable=broad-except self.log.exception( 'Uncaught exception from domain-unpaused handler ' 'for domain %s', vm.name) @qubes.events.handler('domain-pre-delete') def on_domain_pre_deleted(self, event, vm): # pylint: disable=unused-argument for obj in itertools.chain(self.domains, (self,)): for prop in obj.property_list(): try: if isinstance(prop, qubes.vm.VMProperty) and \ getattr(obj, prop.__name__) == vm: self.log.error( 'Cannot remove %s, used by %s.%s', vm, obj, prop.__name__) raise qubes.exc.QubesVMInUseError(vm, 'Domain is in ' 'use: {!r}; see /var/log/qubes/qubes.log in dom0 for ' 'details'.format(vm.name)) except AttributeError: pass @qubes.events.handler('domain-delete') def on_domain_deleted(self, event, vm): # pylint: disable=unused-argument for propname in ( 'default_netvm', 'default_fw_netvm', 'clockvm', 'updatevm', 'default_template', ): try: if getattr(self, propname) == vm: delattr(self, propname) except AttributeError: pass @qubes.events.handler('property-pre-set:clockvm') def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,no-self-use if newvalue is None: return if 'service.clocksync' not in newvalue.features: newvalue.features['service.clocksync'] = True @qubes.events.handler('property-set:clockvm') def on_property_set_clockvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,no-self-use if oldvalue and oldvalue.features.get('service.clocksync', False): del oldvalue.features['service.clocksync'] @qubes.events.handler('property-pre-set:default_netvm') def on_property_pre_set_default_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,invalid-name if newvalue is not None and oldvalue is not None \ and oldvalue.is_running() and not newvalue.is_running() \ and self.domains.get_vms_connected_to(oldvalue): raise qubes.exc.QubesVMNotRunningError(newvalue, 'Cannot change {!r} to domain that ' 'is not running ({!r}).'.format(name, newvalue.name)) @qubes.events.handler('property-set:default_fw_netvm') def on_property_set_default_fw_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,invalid-name for vm in self.domains: if hasattr(vm, 'provides_network') and vm.provides_network and \ hasattr(vm, 'netvm') and vm.property_is_default('netvm'): # fire property-del:netvm as it is responsible for resetting # netvm to it's default value vm.fire_event('property-pre-del:netvm', pre_event=True, name='netvm', oldvalue=oldvalue) vm.fire_event('property-del:netvm', name='netvm', oldvalue=oldvalue) @qubes.events.handler('property-set:default_netvm') def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument for vm in self.domains: if hasattr(vm, 'provides_network') and not vm.provides_network and \ hasattr(vm, 'netvm') and vm.property_is_default('netvm'): # fire property-del:netvm as it is responsible for resetting # netvm to it's default value vm.fire_event('property-pre-del:netvm', pre_event=True, name='netvm', oldvalue=oldvalue) vm.fire_event('property-del:netvm', name='netvm', oldvalue=oldvalue)
class Qubes(qubes.PropertyHolder): '''Main Qubes application :param str store: path to ``qubes.xml`` The store is loaded in stages: 1. In the first stage there are loaded some basic features from store (currently labels). 2. In the second stage stubs for all VMs are loaded. They are filled with their basic properties, like ``qid`` and ``name``. 3. In the third stage all global properties are loaded. They often reference VMs, like default netvm, so they should be filled after loading VMs. 4. In the fourth stage all remaining VM properties are loaded. They also need all VMs loaded, because they represent dependencies between VMs like aforementioned netvm. 5. In the fifth stage there are some fixups to ensure sane system operation. This class emits following events: .. event:: domain-add (subject, event, vm) When domain is added. :param subject: Event emitter :param event: Event name (``'domain-add'``) :param vm: Domain object .. event:: domain-pre-delete (subject, event, vm) When domain is deleted. VM still has reference to ``app`` object, and is contained within VMCollection. You may prevent removal by raising an exception. :param subject: Event emitter :param event: Event name (``'domain-pre-delete'``) :param vm: Domain object .. event:: domain-delete (subject, event, vm) When domain is deleted. VM still has reference to ``app`` object, but is not contained within VMCollection. :param subject: Event emitter :param event: Event name (``'domain-delete'``) :param vm: Domain object Methods and attributes: ''' default_netvm = qubes.VMProperty( 'default_netvm', load_stage=3, default=None, allow_none=True, doc='''Default NetVM for AppVMs. Initial state is `None`, which means that AppVMs are not connected to the Internet.''') default_fw_netvm = qubes.VMProperty( 'default_fw_netvm', load_stage=3, default=None, allow_none=True, doc='''Default NetVM for ProxyVMs. Initial state is `None`, which means that ProxyVMs (including FirewallVM) are not connected to the Internet.''') default_template = qubes.VMProperty('default_template', load_stage=3, vmclass=qubes.vm.templatevm.TemplateVM, doc='Default template for new AppVMs') updatevm = qubes.VMProperty( 'updatevm', load_stage=3, allow_none=True, doc='''Which VM to use as `yum` proxy for updating AdminVM and TemplateVMs''') clockvm = qubes.VMProperty( 'clockvm', load_stage=3, allow_none=True, doc='Which VM to use as NTP proxy for updating AdminVM') default_kernel = qubes.property( 'default_kernel', load_stage=3, doc='Which kernel to use when not overriden in VM') default_dispvm = qubes.VMProperty( 'default_dispvm', load_stage=3, doc='Default DispVM base for service calls') # TODO #1637 #892 check_updates_vm = qubes.property('check_updates_vm', type=bool, setter=qubes.property.bool, default=True, doc='check for updates inside qubes') def __init__(self, store=None, load=True, offline_mode=None, lock=False, **kwargs): #: logger instance for logging global messages self.log = logging.getLogger('app') self._extensions = qubes.ext.get_extensions() #: collection of all VMs managed by this Qubes instance self.domains = VMCollection(self) #: collection of all available labels for VMs self.labels = {} #: collection of all pools self.pools = {} #: Connection to VMM self.vmm = VMMConnection(offline_mode=offline_mode) #: Information about host system self.host = QubesHost(self) if store is not None: self._store = store else: self._store = os.environ.get( 'QUBES_XML_PATH', os.path.join(qubes.config.system_path['qubes_base_dir'], qubes.config.system_path['qubes_store_filename'])) super(Qubes, self).__init__(xml=None, **kwargs) self.__load_timestamp = None self.__locked_fh = None #: jinja2 environment for libvirt XML templates self.env = jinja2.Environment(loader=jinja2.FileSystemLoader([ '/etc/qubes/templates', '/usr/share/qubes/templates', ]), undefined=jinja2.StrictUndefined) if load: self.load(lock=lock) self.events_enabled = True @property def store(self): return self._store def load(self, lock=False): '''Open qubes.xml :throws EnvironmentError: failure on parsing store :throws xml.parsers.expat.ExpatError: failure on parsing store :raises lxml.etree.XMLSyntaxError: on syntax error in qubes.xml ''' fh = self._acquire_lock() self.xml = lxml.etree.parse(fh) # stage 1: load labels and pools for node in self.xml.xpath('./labels/label'): label = qubes.Label.fromxml(node) self.labels[label.index] = label for node in self.xml.xpath('./pools/pool'): name = node.get('name') assert name, "Pool name '%s' is invalid " % name try: self.pools[name] = self._get_pool(**node.attrib) except qubes.exc.QubesException as e: self.log.error(str(e)) # stage 2: load VMs for node in self.xml.xpath('./domains/domain'): # pylint: disable=no-member cls = self.get_vm_class(node.get('class')) vm = cls(self, node) vm.load_properties(load_stage=2) vm.init_log() self.domains.add(vm, _enable_events=False) if 0 not in self.domains: self.domains.add(qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0'), _enable_events=False) # stage 3: load global properties self.load_properties(load_stage=3) # stage 4: fill all remaining VM properties for vm in self.domains: vm.load_properties(load_stage=4) vm.load_extras() # stage 5: misc fixups self.property_require('default_fw_netvm', allow_none=True) self.property_require('default_netvm', allow_none=True) self.property_require('default_template') self.property_require('clockvm', allow_none=True) self.property_require('updatevm', allow_none=True) # Disable ntpd in ClockVM - to not conflict with ntpdate (both are # using 123/udp port) if hasattr(self, 'clockvm') and self.clockvm is not None: if self.clockvm.features.get('service/ntpd', False): self.log.warning( 'VM set as clockvm (%r) has enabled \'ntpd\' service! ' 'Expect failure when syncing time in dom0.', self.clockvm) else: self.clockvm.features['service/ntpd'] = '' for vm in self.domains: vm.events_enabled = True vm.fire_event('domain-load') # get a file timestamp (before closing it - still holding the lock!), # to detect whether anyone else have modified it in the meantime self.__load_timestamp = os.path.getmtime(self._store) if not lock: self._release_lock() def __xml__(self): element = lxml.etree.Element('qubes') element.append(self.xml_labels()) pools_xml = lxml.etree.Element('pools') for pool in self.pools.values(): xml = pool.__xml__() if xml is not None: pools_xml.append(xml) element.append(pools_xml) element.append(self.xml_properties()) domains = lxml.etree.Element('domains') for vm in self.domains: domains.append(vm.__xml__()) element.append(domains) return element def save(self, lock=True): '''Save all data to qubes.xml There are several problems with saving :file:`qubes.xml` which must be mitigated: - Running out of disk space. No space left should not result in empty file. This is done by writing to temporary file and then renaming. - Attempts to write two or more files concurrently. This is done by sophisticated locking. :param bool lock: keep file locked after saving :throws EnvironmentError: failure on saving ''' if not self.__locked_fh: self._acquire_lock(for_save=True) fh_new = tempfile.NamedTemporaryFile(prefix=self._store, delete=False) lxml.etree.ElementTree(self.__xml__()).write(fh_new, encoding='utf-8', pretty_print=True) fh_new.flush() try: os.chown(fh_new.name, -1, grp.getgrnam('qubes').gr_gid) os.chmod(fh_new.name, 0o660) except KeyError: # group 'qubes' not found # don't change mode if no 'qubes' group in the system pass os.rename(fh_new.name, self._store) # update stored mtime, in case of multiple save() calls without # loading qubes.xml again self.__load_timestamp = os.path.getmtime(self._store) # this releases lock for all other processes, # but they should instantly block on the new descriptor self.__locked_fh.close() self.__locked_fh = fh_new if not lock: self._release_lock() def _acquire_lock(self, for_save=False): assert self.__locked_fh is None, 'double lock' while True: try: fd = os.open(self._store, os.O_RDWR | (os.O_CREAT * int(for_save))) except OSError as e: if not for_save and e.errno == errno.ENOENT: raise qubes.exc.QubesException( 'Qubes XML store {!r} is missing; ' 'use qubes-create tool'.format(self._store)) raise # While we were waiting for lock, someone could have unlink()ed # (or rename()d) our file out of the filesystem. We have to # ensure we got lock on something linked to filesystem. # If not, try again. if os.fstat(fd) != os.stat(self._store): os.close(fd) continue if self.__load_timestamp and \ os.path.getmtime(self._store) != self.__load_timestamp: os.close(fd) raise qubes.exc.QubesException( 'Someone else modified qubes.xml in the meantime') break if os.name == 'posix': fcntl.lockf(fd, fcntl.LOCK_EX) elif os.name == 'nt': # pylint: disable=protected-access overlapped = pywintypes.OVERLAPPED() win32file.LockFileEx(win32file._get_osfhandle(fd), win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped) self.__locked_fh = os.fdopen(fd, 'r+b') return self.__locked_fh def _release_lock(self): assert self.__locked_fh is not None, 'double release' # intentionally do not call explicit unlock to not unlock the file # before all buffers are flushed self.__locked_fh.close() self.__locked_fh = None def load_initial_values(self): self.labels = { 1: qubes.Label(1, '0xcc0000', 'red'), 2: qubes.Label(2, '0xf57900', 'orange'), 3: qubes.Label(3, '0xedd400', 'yellow'), 4: qubes.Label(4, '0x73d216', 'green'), 5: qubes.Label(5, '0x555753', 'gray'), 6: qubes.Label(6, '0x3465a4', 'blue'), 7: qubes.Label(7, '0x75507b', 'purple'), 8: qubes.Label(8, '0x000000', 'black'), } assert max(self.labels.keys()) == qubes.config.max_default_label # check if the default LVM Thin pool qubes_dom0/pool00 exists if os.path.exists('/dev/mapper/qubes_dom0-pool00-tpool'): self.add_pool(volume_group='qubes_dom0', thin_pool='pool00', name='default', driver='lvm_thin') else: self.pools['default'] = self._get_pool( dir_path=qubes.config.qubes_base_dir, name='default', driver='file') for name, config in qubes.config.defaults['pool_configs'].items(): self.pools[name] = self._get_pool(**config) self.domains.add(qubes.vm.adminvm.AdminVM(self, None, label='black')) @classmethod def create_empty_store(cls, *args, **kwargs): self = cls(*args, load=False, **kwargs) if os.path.exists(self.store): raise qubes.exc.QubesException( '{} already exists, aborting'.format(self.store)) self.load_initial_values() # TODO py3 get lock= as keyword-only arg self.save(kwargs.get('lock')) return self def xml_labels(self): '''Serialise labels :rtype: lxml.etree._Element ''' labels = lxml.etree.Element('labels') for label in sorted(self.labels.values(), key=lambda labl: labl.index): labels.append(label.__xml__()) return labels @staticmethod def get_vm_class(clsname): '''Find the class for a domain. Classes are registered as setuptools' entry points in ``qubes.vm`` group. Any package may supply their own classes. :param str clsname: name of the class :return type: class ''' try: return qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT, clsname) except KeyError: raise qubes.exc.QubesException( 'no such VM class: {!r}'.format(clsname)) # don't catch TypeError def add_new_vm(self, cls, qid=None, **kwargs): '''Add new Virtual Machine to collection ''' if qid is None: qid = self.domains.get_new_unused_qid() if isinstance(cls, str): cls = self.get_vm_class(cls) # handle default template; specifically allow template=None (do not # override it with default template) if 'template' not in kwargs and hasattr(cls, 'template'): kwargs['template'] = self.default_template elif 'template' in kwargs and isinstance(kwargs['template'], str): kwargs['template'] = self.domains[kwargs['template']] return self.domains.add(cls(self, None, qid=qid, **kwargs)) def get_label(self, label): '''Get label as identified by index or name :throws KeyError: when label is not found ''' # first search for index, verbatim try: return self.labels[label] except KeyError: pass # then search for name for i in self.labels.values(): if i.name == label: return i # last call, if label is a number represented as str, search in indices try: return self.labels[int(label)] except (KeyError, ValueError): pass raise KeyError(label) def add_pool(self, name, **kwargs): """ Add a storage pool to config.""" if name in self.pools.keys(): raise qubes.exc.QubesException('pool named %s already exists \n' % name) kwargs['name'] = name pool = self._get_pool(**kwargs) pool.setup() self.pools[name] = pool return pool def remove_pool(self, name): """ Remove a storage pool from config file. """ try: pool = self.pools[name] del self.pools[name] pool.destroy() except KeyError: return def get_pool(self, pool): ''' Returns a :py:class:`qubes.storage.Pool` instance ''' if isinstance(pool, qubes.storage.Pool): return pool try: return self.pools[pool] except KeyError: raise qubes.exc.QubesException('Unknown storage pool ' + pool) @staticmethod def _get_pool(**kwargs): try: name = kwargs['name'] assert name, 'Name needs to be an non empty string' except KeyError: raise qubes.exc.QubesException('No pool name for pool') try: driver = kwargs['driver'] except KeyError: raise qubes.exc.QubesException('No driver specified for pool ' + name) try: klass = qubes.utils.get_entry_point_one( qubes.storage.STORAGE_ENTRY_POINT, driver) del kwargs['driver'] return klass(**kwargs) except KeyError: raise qubes.exc.QubesException('No driver %s for pool %s' % (driver, name)) @qubes.events.handler('domain-pre-delete') def on_domain_pre_deleted(self, event, vm): # pylint: disable=unused-argument if isinstance(vm, qubes.vm.templatevm.TemplateVM): appvms = self.domains.get_vms_based_on(vm) if appvms: raise qubes.exc.QubesException( 'Cannot remove template that has dependent AppVMs. ' 'Affected are: {}'.format(', '.join( appvm.name for appvm in sorted(appvms)))) @qubes.events.handler('domain-delete') def on_domain_deleted(self, event, vm): # pylint: disable=unused-argument for propname in ( 'default_netvm', 'default_fw_netvm', 'clockvm', 'updatevm', 'default_template', ): try: if getattr(self, propname) == vm: delattr(self, propname) except AttributeError: pass @qubes.events.handler('property-pre-set:clockvm') def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,no-self-use if newvalue is None: return if newvalue.features.get('service/ntpd', False): raise qubes.exc.QubesVMError( newvalue, 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format( newvalue.name, name)) else: newvalue.features['service/ntpd'] = '' @qubes.events.handler('property-pre-set:default_netvm', 'property-pre-set:default_fw_netvm') def on_property_pre_set_default_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,invalid-name if newvalue is not None and oldvalue is not None \ and oldvalue.is_running() and not newvalue.is_running() \ and self.domains.get_vms_connected_to(oldvalue): raise qubes.exc.QubesVMNotRunningError( newvalue, 'Cannot change {!r} to domain that ' 'is not running ({!r}).'.format(name, newvalue.name)) @qubes.events.handler('property-set:default_fw_netvm') def on_property_set_default_fw_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,invalid-name for vm in self.domains: if not vm.provides_network and vm.property_is_default('netvm'): # fire property-del:netvm as it is responsible for resetting # netvm to it's default value vm.fire_event('property-del:netvm', name='netvm', newvalue=newvalue, oldvalue=oldvalue) @qubes.events.handler('property-set:default_netvm') def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument for vm in self.domains: if hasattr(vm, 'netvm') and vm.property_is_default('netvm'): # fire property-del:netvm as it is responsible for resetting # netvm to it's default value vm.fire_event('property-del:netvm', name='netvm', oldvalue=oldvalue)
class AdminVM(qubes.vm.BaseVM): '''Dom0''' dir_path = None name = qubes.property('name', default='dom0', setter=qubes.property.forbidden) qid = qubes.property('qid', default=0, type=int, setter=qubes.property.forbidden) uuid = qubes.property('uuid', default='00000000-0000-0000-0000-000000000000', setter=qubes.property.forbidden) default_dispvm = qubes.VMProperty( 'default_dispvm', load_stage=4, allow_none=True, default=(lambda self: self.app.default_dispvm), doc='Default VM to be used as Disposable VM for service calls.') include_in_backups = qubes.property( 'include_in_backups', default=True, type=bool, doc='If this domain is to be included in default backup.') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._qdb_connection = None self._libvirt_domain = None if not self.app.vmm.offline_mode: self.start_qdb_watch() def __str__(self): return self.name def __lt__(self, other): # order dom0 before anything return self.name != other.name @property def attached_volumes(self): return [] @property def xid(self): '''Always ``0``. .. seealso: :py:attr:`qubes.vm.qubesvm.QubesVM.xid` ''' return 0 @property def libvirt_domain(self): '''Libvirt object for dom0. .. seealso: :py:attr:`qubes.vm.qubesvm.QubesVM.libvirt_domain` ''' if self._libvirt_domain is None: self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByID(0) return self._libvirt_domain @staticmethod def is_running(): '''Always :py:obj:`True`. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.is_running` ''' return True @staticmethod def is_halted(): '''Always :py:obj:`False`. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.is_halted` ''' return False @staticmethod def get_power_state(): '''Always ``'Running'``. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_power_state` ''' return 'Running' @staticmethod def get_mem(): '''Get current memory usage of Dom0. Unit is KiB. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem` ''' # return psutil.virtual_memory().total/1024 with open('/proc/meminfo') as file: for line in file: if line.startswith('MemTotal:'): return int(line.split(':')[1].strip().split()[0]) raise NotImplementedError() def get_mem_static_max(self): '''Get maximum memory available to Dom0. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem_static_max` ''' if self.app.vmm.offline_mode: # default value passed on xen cmdline return 4096 try: return self.app.vmm.libvirt_conn.getInfo()[1] except libvirt.libvirtError as e: self.log.warning('Failed to get memory limit for dom0: %s', e) return 4096 def verify_files(self): '''Always :py:obj:`True` .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.verify_files` ''' # pylint: disable=no-self-use return True def start(self, start_guid=True, notify_function=None, mem_required=None): '''Always raises an exception. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.start` ''' # pylint: disable=unused-argument,arguments-differ raise qubes.exc.QubesVMError(self, 'Cannot start Dom0 fake domain!') def suspend(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.suspend` ''' raise qubes.exc.QubesVMError(self, 'Cannot suspend Dom0 fake domain!') def shutdown(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.shutdown` ''' raise qubes.exc.QubesVMError(self, 'Cannot shutdown Dom0 fake domain!') def kill(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.kill` ''' raise qubes.exc.QubesVMError(self, 'Cannot kill Dom0 fake domain!') @property def icon_path(self): pass @property def untrusted_qdb(self): '''QubesDB handle for this domain.''' if self._qdb_connection is None: import qubesdb # pylint: disable=import-error self._qdb_connection = qubesdb.QubesDB(self.name) return self._qdb_connection
class AdminVM(qubes.vm.BaseVM): '''Dom0''' dir_path = None name = qubes.property('name', default='dom0', setter=qubes.property.forbidden) qid = qubes.property('qid', default=0, type=int, setter=qubes.property.forbidden) uuid = qubes.property('uuid', default='00000000-0000-0000-0000-000000000000', setter=qubes.property.forbidden) default_dispvm = qubes.VMProperty( 'default_dispvm', load_stage=4, allow_none=True, default=(lambda self: self.app.default_dispvm), doc='Default VM to be used as Disposable VM for service calls.') include_in_backups = qubes.property( 'include_in_backups', default=True, type=bool, doc='If this domain is to be included in default backup.') updateable = qubes.property( 'updateable', default=True, type=bool, setter=qubes.property.forbidden, doc='True if this machine may be updated on its own.') # for changes in keyboard_layout, see also the same property in QubesVM keyboard_layout = qubes.property('keyboard_layout', type=str, setter=_setter_kbd_layout, default='us++', doc='Keyboard layout for this VM') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._qdb_connection = None self._libvirt_domain = None if not self.app.vmm.offline_mode: self.start_qdb_watch() def __str__(self): return self.name def __lt__(self, other): # order dom0 before anything return self.name != other.name @property def attached_volumes(self): return [] @property def xid(self): '''Always ``0``. .. seealso: :py:attr:`qubes.vm.qubesvm.QubesVM.xid` ''' return 0 @qubes.stateless_property def icon(self): # pylint: disable=no-self-use """freedesktop icon name, suitable for use in :py:meth:`PyQt4.QtGui.QIcon.fromTheme`""" return 'adminvm-black' @property def libvirt_domain(self): '''Libvirt object for dom0. .. seealso: :py:attr:`qubes.vm.qubesvm.QubesVM.libvirt_domain` ''' if self._libvirt_domain is None: self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByID(0) return self._libvirt_domain @staticmethod def is_running(): '''Always :py:obj:`True`. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.is_running` ''' return True @staticmethod def is_halted(): '''Always :py:obj:`False`. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.is_halted` ''' return False @staticmethod def get_power_state(): '''Always ``'Running'``. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_power_state` ''' return 'Running' @staticmethod def get_mem(): '''Get current memory usage of Dom0. Unit is KiB. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem` ''' # return psutil.virtual_memory().total/1024 with open('/proc/meminfo') as file: for line in file: if line.startswith('MemTotal:'): return int(line.split(':')[1].strip().split()[0]) raise NotImplementedError() def get_mem_static_max(self): '''Get maximum memory available to Dom0. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem_static_max` ''' if self.app.vmm.offline_mode: # default value passed on xen cmdline return 4096 try: return self.app.vmm.libvirt_conn.getInfo()[1] except libvirt.libvirtError as e: self.log.warning('Failed to get memory limit for dom0: %s', e) return 4096 def get_cputime(self): '''Get total CPU time burned by Dom0 since start. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_cputime` ''' try: return self.libvirt_domain.info()[4] except libvirt.libvirtError as e: self.log.warning('Failed to get CPU time for dom0: %s', e) return 0 def verify_files(self): '''Always :py:obj:`True` .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.verify_files` ''' # pylint: disable=no-self-use return True def start(self, start_guid=True, notify_function=None, mem_required=None): '''Always raises an exception. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.start` ''' # pylint: disable=unused-argument,arguments-differ raise qubes.exc.QubesVMNotHaltedError( self, 'Cannot start Dom0 fake domain!') def suspend(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.suspend` ''' raise qubes.exc.QubesVMError(self, 'Cannot suspend Dom0 fake domain!') def shutdown(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.shutdown` ''' raise qubes.exc.QubesVMError(self, 'Cannot shutdown Dom0 fake domain!') def kill(self): '''Does nothing. .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.kill` ''' raise qubes.exc.QubesVMError(self, 'Cannot kill Dom0 fake domain!') @property def untrusted_qdb(self): '''QubesDB handle for this domain.''' if self._qdb_connection is None: import qubesdb # pylint: disable=import-error self._qdb_connection = qubesdb.QubesDB(self.name) return self._qdb_connection async def run_service(self, service, source=None, user=None, filter_esc=False, autostart=False, gui=False, **kwargs): '''Run service on this VM :param str service: service name :param qubes.vm.qubesvm.QubesVM source: source domain as presented to this VM :param str user: username to run service as :param bool filter_esc: filter escape sequences to protect terminal \ emulator :param bool autostart: if :py:obj:`True`, machine will be started if \ it is not running :param bool gui: when autostarting, also start gui daemon :rtype: asyncio.subprocess.Process .. note:: User ``root`` is redefined to ``SYSTEM`` in the Windows agent code ''' # pylint: disable=unused-argument source = 'dom0' if source is None else self.app.domains[source].name if filter_esc: raise NotImplementedError( 'filter_esc=True not supported on calls to dom0') if user is None: user = '******' await self.fire_event_async('domain-cmd-pre-run', pre_event=True, start_guid=gui) if user != 'root': cmd = ['runuser', '-u', user, '--'] else: cmd = [] cmd.extend([ qubes.config.system_path['qrexec_rpc_multiplexer'], service, source, 'name', self.name, ]) return await asyncio.create_subprocess_exec(*cmd, **kwargs) async def run_service_for_stdio(self, *args, input=None, **kwargs): '''Run a service, pass an optional input and return (stdout, stderr). Raises an exception if return code != 0. *args* and *kwargs* are passed verbatim to :py:meth:`run_service`. .. warning:: There are some combinations if stdio-related *kwargs*, which are not filtered for problems originating between the keyboard and the chair. ''' # pylint: disable=redefined-builtin kwargs.setdefault('stdin', subprocess.PIPE) kwargs.setdefault('stdout', subprocess.PIPE) kwargs.setdefault('stderr', subprocess.PIPE) p = await self.run_service(*args, **kwargs) # this one is actually a tuple, but there is no need to unpack it stdouterr = await p.communicate(input=input) if p.returncode: raise subprocess.CalledProcessError(p.returncode, args[0], *stdouterr) return stdouterr
class TemplateVM(QubesVM): '''Template for AppVM''' dir_path_prefix = qubes.config.system_path['qubes_templates_dir'] @property def appvms(self): ''' Returns a generator containing all domains based on the current TemplateVM. ''' for vm in self.app.domains: if hasattr(vm, 'template') and vm.template is self: yield vm netvm = qubes.VMProperty( 'netvm', load_stage=4, allow_none=True, default=None, # pylint: disable=protected-access setter=qubes.vm.qubesvm.QubesVM.netvm._setter, doc='VM that provides network connection to this domain. When ' '`None`, machine is disconnected.') def __init__(self, *args, **kwargs): assert 'template' not in kwargs, "A TemplateVM can not have a template" self.volume_config = { 'root': { 'name': 'root', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['root_img_size'], }, 'private': { 'name': 'private', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['private_img_size'], 'revisions_to_keep': 0, }, 'volatile': { 'name': 'volatile', 'size': defaults['root_img_size'], 'snap_on_start': False, 'save_on_stop': False, 'rw': True, }, 'kernel': { 'name': 'kernel', 'snap_on_start': False, 'save_on_stop': False, 'rw': False } } super(TemplateVM, self).__init__(*args, **kwargs) @qubes.events.handler('property-set:default_user', 'property-set:kernel', 'property-set:kernelopts', 'property-set:vcpus', 'property-set:memory', 'property-set:maxmem', 'property-set:qrexec_timeout', 'property-set:shutdown_timeout', 'property-set:management_dispvm') def on_property_set_child(self, _event, name, newvalue, oldvalue=None): """Send event about default value change to child VMs (which use default inherited from the template). This handler is supposed to be set for properties using `_default_with_template()` function for the default value. """ if newvalue == oldvalue: return for vm in self.appvms: if not vm.property_is_default(name): continue vm.fire_event('property-reset:' + name, name=name)
class TemplateVM(QubesVM): '''Template for AppVM''' dir_path_prefix = qubes.config.system_path['qubes_templates_dir'] @property def rootcow_img(self): '''COW image''' warnings.warn( "rootcow_img is deprecated, use " "volumes['root'].path_origin", DeprecationWarning) return self.volumes['root'].path_cow @property def appvms(self): ''' Returns a generator containing all domains based on the current TemplateVM. ''' for vm in self.app.domains: if hasattr(vm, 'template') and vm.template is self: yield vm netvm = qubes.VMProperty( 'netvm', load_stage=4, allow_none=True, default=None, # pylint: disable=protected-access setter=qubes.vm.qubesvm.QubesVM.netvm._setter, doc='VM that provides network connection to this domain. When ' '`None`, machine is disconnected.') def __init__(self, *args, **kwargs): assert 'template' not in kwargs, "A TemplateVM can not have a template" self.volume_config = { 'root': { 'name': 'root', 'pool': 'default', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['root_img_size'], 'internal': True }, 'private': { 'name': 'private', 'pool': 'default', 'snap_on_start': False, 'save_on_stop': True, 'rw': True, 'source': None, 'size': defaults['private_img_size'], 'revisions_to_keep': 0, 'internal': True }, 'volatile': { 'name': 'volatile', 'pool': 'default', 'size': defaults['root_img_size'], 'internal': True, 'rw': True, }, 'kernel': { 'name': 'kernel', 'pool': 'linux-kernel', 'internal': True, 'rw': False } } super(TemplateVM, self).__init__(*args, **kwargs) def commit_changes(self): '''Commit changes to template''' self.log.debug('commit_changes()') if not self.app.vmm.offline_mode: assert not self.is_running(), \ 'Attempt to commit changes on running Template VM!' self.storage.commit()