Ejemplo n.º 1
0
class StandaloneVM(qubes.vm.standalonevm.StandaloneVM):
    """core2 compatibility StandaloneVM class, with variable dir_path
    """  # pylint: disable=too-many-ancestors
    dir_path = qubes.property(
        'dir_path',
        # pylint: disable=undefined-variable
        default=(lambda self: super(StandaloneVM, self).dir_path),
        saver=qubes.property.dontsave,
        doc="VM storage directory")

    def is_running(self):
        return False
Ejemplo n.º 2
0
class AppVM(qubes.vm.appvm.AppVM):  # pylint: disable=too-many-ancestors
    """core2 compatibility AppVM class, with variable dir_path"""
    dir_path = qubes.property(
        'dir_path',
        # pylint: disable=undefined-variable
        default=(lambda self: super(AppVM, self).dir_path),
        saver=qubes.property.dontsave,
        doc="VM storage directory",
    )

    def is_running(self):
        return False
Ejemplo n.º 3
0
class TestVM(qubes.vm.BaseVM):
    qid = qubes.property('qid', type=int)
    name = qubes.property('name')
    uuid = uuid.uuid5(uuid.NAMESPACE_DNS, 'testvm')

    def __lt__(self, other):
        try:
            return self.name < other.name
        except AttributeError:
            return NotImplemented

    class MockLibvirt(object):
        def undefine(self):
            pass

    libvirt_domain = MockLibvirt()

    def is_halted(self):
        return True

    def get_power_state(self):
        return "Halted"
Ejemplo n.º 4
0
 class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
     guivm = qubes.property('guivm',
                            default=(lambda self: 'dom0'))
Ejemplo n.º 5
0
class Rule(qubes.PropertyHolder):
    def __init__(self, xml=None, **kwargs):
        '''Single firewall rule

        :param xml: XML element describing rule, or None
        :param kwargs: rule elements
        '''
        super(Rule, self).__init__(xml, **kwargs)
        self.load_properties()
        self.events_enabled = True
        # validate dependencies
        if self.dstports:
            self.on_set_dstports('property-set:dstports', 'dstports',
                                 self.dstports, None)
        if self.icmptype:
            self.on_set_icmptype('property-set:icmptype', 'icmptype',
                                 self.icmptype, None)
        self.property_require('action', False, True)

    action = qubes.property('action', type=Action, order=0, doc='rule action')

    proto = qubes.property('proto',
                           type=Proto,
                           default=None,
                           order=1,
                           doc='protocol to match')

    dsthost = qubes.property('dsthost',
                             type=DstHost,
                             default=None,
                             order=1,
                             doc='destination host/network')

    dstports = qubes.property(
        'dstports',
        type=DstPorts,
        default=None,
        order=2,
        doc='Destination port(s) (for \'tcp\' and \'udp\' protocol only)')

    icmptype = qubes.property(
        'icmptype',
        type=IcmpType,
        default=None,
        order=2,
        doc='ICMP packet type (for \'icmp\' protocol only)')

    specialtarget = qubes.property(
        'specialtarget',
        type=SpecialTarget,
        default=None,
        order=1,
        doc='Special target, for now only \'dns\' supported')

    expire = qubes.property(
        'expire',
        type=Expire,
        default=None,
        doc='Timestamp (UNIX epoch) on which this rule expire')

    comment = qubes.property('comment',
                             type=Comment,
                             default=None,
                             doc='User comment')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-pre-set:dstports')
    def on_set_dstports(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if self.proto not in ('tcp', 'udp'):
            raise ValueError(
                'dstports valid only for \'tcp\' and \'udp\' protocols')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-pre-set:icmptype')
    def on_set_icmptype(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if self.proto not in ('icmp', ):
            raise ValueError('icmptype valid only for \'icmp\' protocol')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-set:proto')
    def on_set_proto(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if newvalue not in ('tcp', 'udp'):
            self.dstports = qubes.property.DEFAULT
        if newvalue not in ('icmp', ):
            self.icmptype = qubes.property.DEFAULT

    @qubes.events.handler('property-reset:proto')
    def on_reset_proto(self, event, name, oldvalue):
        # pylint: disable=unused-argument
        self.dstports = qubes.property.DEFAULT
        self.icmptype = qubes.property.DEFAULT

    @property
    def rule(self):
        if self.expire and self.expire.expired:
            return None
        values = []
        for prop in self.property_list():
            value = getattr(self, prop.__name__)
            if value is None:
                continue
            if value.rule is None:
                continue
            values.append(value.rule)
        return ' '.join(values)

    @property
    def api_rule(self):
        values = []
        if self.expire and self.expire.expired:
            return None
        # put comment at the end
        for prop in sorted(self.property_list(),
                           key=(lambda p: p.__name__ == 'comment')):
            value = getattr(self, prop.__name__)
            if value is None:
                continue
            if value.api_rule is None:
                continue
            values.append(value.api_rule)
        return ' '.join(values)

    @classmethod
    def from_xml_v1(cls, node, action):
        netmask = node.get('netmask')
        if netmask is None:
            netmask = 32
        else:
            netmask = int(netmask)
        address = node.get('address')
        if address:
            dsthost = DstHost(address, netmask)
        else:
            dsthost = None

        proto = node.get('proto')

        port = node.get('port')
        toport = node.get('toport')
        if port and toport:
            dstports = port + '-' + toport
        elif port:
            dstports = port
        else:
            dstports = None

        # backward compatibility: protocol defaults to TCP if port is specified
        if dstports and not proto:
            proto = 'tcp'

        if proto == 'any':
            proto = None

        expire = node.get('expire')

        kwargs = {
            'action': action,
        }
        if dsthost:
            kwargs['dsthost'] = dsthost
        if dstports:
            kwargs['dstports'] = dstports
        if proto:
            kwargs['proto'] = proto
        if expire:
            kwargs['expire'] = expire

        return cls(**kwargs)

    @classmethod
    def from_api_string(cls, untrusted_rule):
        '''Parse a single line of firewall rule'''
        # comment is allowed to have spaces
        untrusted_options, _, untrusted_comment = untrusted_rule.partition(
            'comment=')
        # appropriate handlers in __init__ of individual options will perform
        #  option-specific validation
        kwargs = {}
        if untrusted_comment:
            kwargs['comment'] = Comment(untrusted_value=untrusted_comment)

        for untrusted_option in untrusted_options.strip().split(' '):
            untrusted_key, untrusted_value = untrusted_option.split('=', 1)
            if untrusted_key in kwargs:
                raise ValueError(
                    'Option \'{}\' already set'.format(untrusted_key))
            if untrusted_key in [str(prop) for prop in cls.property_list()]:
                kwargs[untrusted_key] = cls.property_get_def(
                    untrusted_key).type(untrusted_value=untrusted_value)
            elif untrusted_key in ('dst4', 'dst6', 'dstname'):
                if 'dsthost' in kwargs:
                    raise ValueError(
                        'Option \'{}\' already set'.format('dsthost'))
                kwargs['dsthost'] = DstHost(untrusted_value=untrusted_value)
            else:
                raise ValueError('Unknown firewall option')

        return cls(**kwargs)

    def __eq__(self, other):
        if isinstance(other, Rule):
            return self.api_rule == other.api_rule
        return self.api_rule == str(other)

    def __hash__(self):
        return hash(self.api_rule)
Ejemplo n.º 6
0
class BaseVM(qubes.PropertyHolder):
    '''Base class for all VMs

    :param app: Qubes application context
    :type app: :py:class:`qubes.Qubes`
    :param xml: xml node from which to deserialise
    :type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`

    This class is responsible for serializing and deserialising machines and
    provides basic framework. It contains no management logic. For that, see
    :py:class:`qubes.vm.qubesvm.QubesVM`.
    '''
    # pylint: disable=no-member

    uuid = qubes.property('uuid',
                          type=uuid.UUID,
                          write_once=True,
                          clone=False,
                          doc='UUID from libvirt.')

    name = qubes.property('name',
                          type=str,
                          write_once=True,
                          clone=False,
                          doc='User-specified name of the domain.')

    qid = qubes.property(
        'qid',
        type=int,
        write_once=True,
        setter=_setter_qid,
        clone=False,
        doc='''Internal, persistent identificator of particular domain. Note
            this is different from Xen domid.''')

    label = qubes.property(
        'label',
        setter=setter_label,
        doc='''Colourful label assigned to VM. This is where the colour of the
            padlock is set.''')

    def __init__(self,
                 app,
                 xml,
                 features=None,
                 devices=None,
                 tags=None,
                 **kwargs):
        # pylint: disable=redefined-outer-name

        self._qdb_watch_paths = set()
        self._qdb_connection_watch = None

        # self.app must be set before super().__init__, because some property
        # setters need working .app attribute
        #: mother :py:class:`qubes.Qubes` object
        self.app = app

        super(BaseVM, self).__init__(xml, **kwargs)

        #: dictionary of features of this qube
        self.features = Features(self, features)

        #: :py:class:`DeviceManager` object keeping devices that are attached to
        #: this domain
        self.devices = devices or qubes.devices.DeviceManager(self)

        #: user-specified tags
        self.tags = Tags(self, tags or ())

        #: logger instance for logging messages related to this VM
        self.log = None

        #: storage volumes
        self.volumes = {}

        #: storage manager
        self.storage = None

        if hasattr(self, 'name'):
            self.init_log()

    def close(self):
        super().close()

        if self._qdb_connection_watch is not None:
            asyncio.get_event_loop().remove_reader(
                self._qdb_connection_watch.watch_fd())
            self._qdb_connection_watch.close()
            del self._qdb_connection_watch

        del self.app
        del self.features
        del self.storage
        # TODO storage may have circ references, but it doesn't leak fds
        del self.devices
        del self.tags

    def load_extras(self):
        if self.xml is None:
            return

        # features
        for node in self.xml.xpath('./features/feature'):
            self.features[node.get('name')] = node.text

        # devices (pci, usb, ...)
        for parent in self.xml.xpath('./devices'):
            devclass = parent.get('class')
            for node in parent.xpath('./device'):
                options = {}
                for option in node.xpath('./option'):
                    options[option.get('name')] = option.text

                device_assignment = qubes.devices.DeviceAssignment(
                    self.app.domains[node.get('backend-domain')],
                    node.get('id'),
                    options,
                    persistent=True)
                self.devices[devclass].load_persistent(device_assignment)

        # tags
        for node in self.xml.xpath('./tags/tag'):
            self.tags.add(node.get('name'))

        # SEE:1815 firewall, policy.

    def init_log(self):
        '''Initialise logger for this domain.'''
        self.log = qubes.log.get_vm_logger(self.name)

    def __xml__(self):
        element = lxml.etree.Element('domain')
        element.set('id', 'domain-' + str(self.qid))
        element.set('class', self.__class__.__name__)

        element.append(self.xml_properties())

        features = lxml.etree.Element('features')
        for feature in self.features:
            node = lxml.etree.Element('feature', name=feature)
            node.text = self.features[feature]
            features.append(node)
        element.append(features)

        for devclass in self.devices:
            devices = lxml.etree.Element('devices')
            devices.set('class', devclass)
            for device in self.devices[devclass].assignments(persistent=True):
                node = lxml.etree.Element('device')
                node.set('backend-domain', device.backend_domain.name)
                node.set('id', device.ident)
                for key, val in device.options.items():
                    option_node = lxml.etree.Element('option')
                    option_node.set('name', key)
                    option_node.text = val
                    node.append(option_node)
                devices.append(node)
            element.append(devices)

        tags = lxml.etree.Element('tags')
        for tag in self.tags:
            node = lxml.etree.Element('tag', name=tag)
            tags.append(node)
        element.append(tags)

        return element

    def __repr__(self):
        proprepr = []
        for prop in self.property_list():
            if prop.__name__ in ('name', 'qid'):
                continue
            try:
                proprepr.append('{}={!s}'.format(prop.__name__,
                                                 getattr(self, prop.__name__)))
            except AttributeError:
                continue

        return '<{} at {:#x} name={!r} qid={!r} {}>'.format(
            type(self).__name__, id(self), self.name, self.qid,
            ' '.join(proprepr))

    #
    # xml serialising methods
    #

    def create_config_file(self):
        '''Create libvirt's XML domain config file

        '''
        domain_config = self.app.env.select_template([
            'libvirt/xen/by-name/{}.xml'.format(self.name),
            'libvirt/xen-user.xml',
            'libvirt/xen-dist.xml',
            'libvirt/xen.xml',
        ]).render(vm=self)
        return domain_config

    def watch_qdb_path(self, path):
        '''Add a QubesDB path to be watched.

        Each change to the path will cause `domain-qdb-change:path` event to be
        fired.
        You can call this method for example in response to
        `domain-init` and `domain-load` events.
        '''

        if path not in self._qdb_watch_paths:
            self._qdb_watch_paths.add(path)
            if self._qdb_connection_watch:
                self._qdb_connection_watch.watch(path)

    def _qdb_watch_reader(self, loop):
        '''Callback when self._qdb_connection_watch.watch_fd() FD is
        readable.

        Read reported event (watched path change) and fire appropriate event.
        '''
        import qubesdb  # pylint: disable=import-error
        try:
            path = self._qdb_connection_watch.read_watch()
            for watched_path in self._qdb_watch_paths:
                if watched_path == path or (watched_path.endswith('/')
                                            and path.startswith(watched_path)):
                    self.fire_event('domain-qdb-change:' + watched_path,
                                    path=path)
        except qubesdb.DisconnectedError:
            loop.remove_reader(self._qdb_connection_watch.watch_fd())
            self._qdb_connection_watch.close()
            self._qdb_connection_watch = None

    def start_qdb_watch(self, loop=None):
        '''Start watching QubesDB

        Calling this method in appropriate time is responsibility of child
        class.
        '''
        # cleanup old watch connection first, if any
        if self._qdb_connection_watch is not None:
            asyncio.get_event_loop().remove_reader(
                self._qdb_connection_watch.watch_fd())
            self._qdb_connection_watch.close()

        import qubesdb  # pylint: disable=import-error
        self._qdb_connection_watch = qubesdb.QubesDB(self.name)
        if loop is None:
            loop = asyncio.get_event_loop()
        loop.add_reader(self._qdb_connection_watch.watch_fd(),
                        self._qdb_watch_reader, loop)
        for path in self._qdb_watch_paths:
            self._qdb_connection_watch.watch(path)

    @qubes.stateless_property
    def klass(self):
        '''Domain class name'''
        return type(self).__name__
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
 class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
     testprop1 = qubes.property('testprop1', default='defaultvalue')
Ejemplo n.º 9
0
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))
Ejemplo n.º 10
0
class AdminVM(qubes.vm.BaseVM):
    '''Dom0'''

    dir_path = None

    name = qubes.property('name',
                          default='dom0',
                          setter=qubes.property.forbidden)

    label = qubes.property(
        'label',
        setter=qubes.vm.setter_label,
        saver=(lambda self, prop, value: 'label-{}'.format(value.index)),
        doc='''Colourful label assigned to VM. This is where the colour of the
            padlock is set.''')

    qid = qubes.property('qid', default=0, setter=qubes.property.forbidden)

    uuid = qubes.property('uuid',
                          default='00000000-0000-0000-0000-000000000000',
                          setter=qubes.property.forbidden)

    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):
        '''Always :py:obj:`None`.

        .. seealso:
           :py:attr:`qubes.vm.qubesvm.QubesVM.libvirt_domain`
        '''
        return None

    @staticmethod
    def is_running():
        '''Always :py:obj:`True`.

        .. seealso:
           :py:meth:`qubes.vm.qubesvm.QubesVM.is_running`
        '''
        return True

    @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
        else:
            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!')

    @property
    def icon_path(self):
        return None
Ejemplo n.º 11
0
class Rule(qubes.PropertyHolder):
    def __init__(self, xml=None, **kwargs):
        '''Single firewall rule

        :param xml: XML element describing rule, or None
        :param kwargs: rule elements
        '''
        super(Rule, self).__init__(xml, **kwargs)
        self.load_properties()
        self.events_enabled = True
        # validate dependencies
        if self.dstports:
            self.on_set_dstports('property-set:dstports', 'dstports',
                                 self.dstports, None)
        if self.icmptype:
            self.on_set_icmptype('property-set:icmptype', 'icmptype',
                                 self.icmptype, None)
        self.property_require('action', False, True)

    action = qubes.property('action', type=Action, order=0, doc='rule action')

    proto = qubes.property('proto',
                           type=Proto,
                           default=None,
                           order=1,
                           doc='protocol to match')

    dsthost = qubes.property('dsthost',
                             type=DstHost,
                             default=None,
                             order=1,
                             doc='destination host/network')

    dstports = qubes.property(
        'dstports',
        type=DstPorts,
        default=None,
        order=2,
        doc='Destination port(s) (for \'tcp\' and \'udp\' protocol only)')

    icmptype = qubes.property(
        'icmptype',
        type=IcmpType,
        default=None,
        order=2,
        doc='ICMP packet type (for \'icmp\' protocol only)')

    specialtarget = qubes.property(
        'specialtarget',
        type=SpecialTarget,
        default=None,
        order=1,
        doc='Special target, for now only \'dns\' supported')

    expire = qubes.property(
        'expire',
        type=Expire,
        default=None,
        doc='Timestamp (UNIX epoch) on which this rule expire')

    comment = qubes.property('comment',
                             type=Comment,
                             default=None,
                             doc='User comment')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-pre-set:dstports')
    def on_set_dstports(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if self.proto not in ('tcp', 'udp'):
            raise ValueError(
                'dstports valid only for \'tcp\' and \'udp\' protocols')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-pre-set:icmptype')
    def on_set_icmptype(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if self.proto not in ('icmp', ):
            raise ValueError('icmptype valid only for \'icmp\' protocol')

    # noinspection PyUnusedLocal
    @qubes.events.handler('property-set:proto')
    def on_set_proto(self, event, name, newvalue, oldvalue=None):
        # pylint: disable=unused-argument
        if newvalue not in ('tcp', 'udp'):
            self.dstports = qubes.property.DEFAULT
        if newvalue not in ('icmp', ):
            self.icmptype = qubes.property.DEFAULT

    @qubes.events.handler('property-del:proto')
    def on_del_proto(self, event, name, oldvalue):
        # pylint: disable=unused-argument
        self.dstports = qubes.property.DEFAULT
        self.icmptype = qubes.property.DEFAULT

    @property
    def rule(self):
        if self.expire and self.expire.expired:
            return None
        values = []
        for prop in self.property_list():
            value = getattr(self, prop.__name__)
            if value is None:
                continue
            if value.rule is None:
                continue
            values.append(value.rule)
        return ' '.join(values)

    @classmethod
    def from_xml_v1(cls, node, action):
        netmask = node.get('netmask')
        if netmask is None:
            netmask = 32
        else:
            netmask = int(netmask)
        address = node.get('address')
        if address:
            dsthost = DstHost(address, netmask)
        else:
            dsthost = None

        proto = node.get('proto')

        port = node.get('port')
        toport = node.get('toport')
        if port and toport:
            dstports = port + '-' + toport
        elif port:
            dstports = port
        else:
            dstports = None

        # backward compatibility: protocol defaults to TCP if port is specified
        if dstports and not proto:
            proto = 'tcp'

        if proto == 'any':
            proto = None

        expire = node.get('expire')

        kwargs = {
            'action': action,
        }
        if dsthost:
            kwargs['dsthost'] = dsthost
        if dstports:
            kwargs['dstports'] = dstports
        if proto:
            kwargs['proto'] = proto
        if expire:
            kwargs['expire'] = expire

        return cls(**kwargs)

    def __eq__(self, other):
        return self.rule == other.rule
Ejemplo n.º 12
0
 def test_002_eq(self):
     self.assertEqual(qubes.property('testprop2'),
                      qubes.property('testprop2'))
Ejemplo n.º 13
0
 class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
     testprop1 = qubes.property('testprop1')
Ejemplo n.º 14
0
class TestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
    testprop1 = qubes.property('testprop1', order=0)
    testprop2 = qubes.property('testprop2', order=1, save_via_ref=True)
    testprop3 = qubes.property('testprop3', order=2, default='testdefault')
    testprop4 = qubes.property('testprop4', order=3)
Ejemplo n.º 15
0
 class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
     testprop1 = qubes.property('testprop1', write_once=True)
Ejemplo n.º 16
0
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)
Ejemplo n.º 17
0
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)
Ejemplo n.º 18
0
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
Ejemplo n.º 19
0
class TestVM(qubes.vm.BaseVM):
    qid = qubes.property('qid', type=int)
    name = qubes.property('name')
    testprop = qubes.property('testprop')
    testlabel = qubes.property('testlabel')
    defaultprop = qubes.property('defaultprop', default='defaultvalue')
Ejemplo n.º 20
0
 def test_002_eq(self):
     self.assertEqual(qubes.property('testprop2'),
         qubes.property('testprop2'))