class TestVM(vanir.vm.BaseVM): qid = vanir.property('qid', type=int) name = vanir.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"
class AdminVM(vanir.vm.BaseVM): '''Dom0''' dir_path = None name = vanir.property('name', default='dom0', setter=vanir.property.forbidden) qid = vanir.property('qid', default=0, type=int, setter=vanir.property.forbidden) uuid = vanir.property('uuid', default='00000000-0000-0000-0000-000000000000', setter=vanir.property.forbidden) default_dispvm = vanir.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 = vanir.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:`vanir.vm.vanirvm.VanirVM.xid` ''' return 0 @property def libvirt_domain(self): '''Libvirt object for dom0. .. seealso: :py:attr:`vanir.vm.vanirvm.VanirVM.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:`vanir.vm.vanirvm.VanirVM.is_running` ''' return True @staticmethod def is_halted(): '''Always :py:obj:`False`. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.is_halted` ''' return False @staticmethod def get_power_state(): '''Always ``'Running'``. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.get_power_state` ''' return 'Running' @staticmethod def get_mem(): '''Get current memory usage of Dom0. Unit is KiB. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.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:`vanir.vm.vanirvm.VanirVM.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:`vanir.vm.vanirvm.VanirVM.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:`vanir.vm.vanirvm.VanirVM.start` ''' # pylint: disable=unused-argument,arguments-differ raise vanir.exc.VanirVMError(self, 'Cannot start Dom0 fake domain!') def suspend(self): '''Does nothing. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.suspend` ''' raise vanir.exc.VanirVMError(self, 'Cannot suspend Dom0 fake domain!') def shutdown(self): '''Does nothing. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.shutdown` ''' raise vanir.exc.VanirVMError(self, 'Cannot shutdown Dom0 fake domain!') def kill(self): '''Does nothing. .. seealso: :py:meth:`vanir.vm.vanirvm.VanirVM.kill` ''' raise vanir.exc.VanirVMError(self, 'Cannot kill Dom0 fake domain!') @property def icon_path(self): pass @property def untrusted_qdb(self): '''Vanirdb handle for this domain.''' if self._qdb_connection is None: import vanirdb # pylint: disable=import-error self._qdb_connection = vanirdb.Vanirdb(self.name) return self._qdb_connection # def __init__(self, **kwargs): # super(VanirAdminVm, self).__init__(qid=0, name="dom0", # dir_path=None, # private_img = None, # template = None, # maxmem = 0, # vcpus = 0, # label = defaults["template_label"], # **kwargs)
class MyTestHolder(vanir.tests.TestEmitter, vanir.PropertyHolder): testprop1 = vanir.property('testprop1', default=(lambda self: 'defaultvalue'))
def test_002_eq(self): self.assertEqual(vanir.property('testprop2'), vanir.property('testprop2'))
class MyTestHolder(vanir.tests.TestEmitter, vanir.PropertyHolder): testprop1 = vanir.property('testprop1')
class MyTestHolder(vanir.tests.TestEmitter, vanir.PropertyHolder): testprop1 = vanir.property('testprop1', write_once=True)
class TestHolder(vanir.tests.TestEmitter, vanir.PropertyHolder): testprop1 = vanir.property('testprop1', order=0) testprop2 = vanir.property('testprop2', order=1, save_via_ref=True) testprop3 = vanir.property('testprop3', order=2, default='testdefault') testprop4 = vanir.property('testprop4', order=3)
class BaseVM(vanir.PropertyHolder): '''Base class for all VMs :param app: vanir application context :type app: :py:class:`vanir.vanir` :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:`vanir.vm.vanirvm.VanirVM`. ''' # pylint: disable=no-member uuid = vanir.property('uuid', type=uuid.UUID, write_once=True, clone=False, doc='UUID from libvirt.') name = vanir.property('name', type=str, write_once=True, clone=False, doc='User-specified name of the domain.') qid = vanir.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 = vanir.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:`vanir.vanir` object self.app = app super(BaseVM, self).__init__(xml, **kwargs) #: dictionary of features of this qube self.features = vanir.features.Features(self, features) #: :py:class:`DeviceManager` object keeping devices that are attached to #: this domain self.devices = devices or vanir.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 = vanir.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 = vanir.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 VanirDB 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 VanirDB # 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 VanirDB.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 VanirDB 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 VanirDB # pylint: disable=import-error self._qdb_connection_watch = VanirDB.VanirDB(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) @vanir.stateless_property def klass(self): '''Domain class name''' return type(self).__name__
class Rule(vanir.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 = vanir.property('action', type=Action, order=0, doc='rule action') proto = vanir.property('proto', type=Proto, default=None, order=1, doc='protocol to match') dsthost = vanir.property('dsthost', type=DstHost, default=None, order=1, doc='destination host/network') dstports = vanir.property( 'dstports', type=DstPorts, default=None, order=2, doc='Destination port(s) (for \'tcp\' and \'udp\' protocol only)') icmptype = vanir.property( 'icmptype', type=IcmpType, default=None, order=2, doc='ICMP packet type (for \'icmp\' protocol only)') specialtarget = vanir.property( 'specialtarget', type=SpecialTarget, default=None, order=1, doc='Special target, for now only \'dns\' supported') expire = vanir.property( 'expire', type=Expire, default=None, doc='Timestamp (UNIX epoch) on which this rule expire') comment = vanir.property('comment', type=Comment, default=None, doc='User comment') # noinspection PyUnusedLocal @vanir.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 @vanir.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 @vanir.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 = vanir.property.DEFAULT if newvalue not in ('icmp', ): self.icmptype = vanir.property.DEFAULT @vanir.events.handler('property-del:proto') def on_del_proto(self, event, name, oldvalue): # pylint: disable=unused-argument self.dstports = vanir.property.DEFAULT self.icmptype = vanir.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)
class NetVMMixin(vanir.events.Emitter): ''' Mixin containing network functionality ''' mac = vanir.property('mac', type=str, default='00:16:3E:5E:6C:00', setter=_setter_mac, doc='MAC address of the NIC emulated inside VM') ip = vanir.property('ip', type=ipaddress.IPv4Address, default=_default_ip, doc='IP address of this domain.') ip6 = vanir.property('ip6', type=ipaddress.IPv6Address, default=_default_ip6, doc='IPv6 address of this domain.') # CORE2: swallowed uses_default_netvm netvm = vanir.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 = vanir.property( 'provides_network', default=False, type=bool, setter=_setter_provides_network, 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) # @vanir.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 @vanir.stateless_property def visible_ip6(self): '''IPv6 address of this domain as seen by the domain.''' return self.ip6 @vanir.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) @vanir.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 @vanir.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 vanir.vm.dispvm # pylint: disable=redefined-outer-name if isinstance(vm, vanir.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 vanir-specific site-local prefix, and IPv4 suffix (0xa89 is 10.137.). ''' import vanir.vm.dispvm # pylint: disable=redefined-outer-name if isinstance(vm, vanir.vm.dispvm.DispVM): return ipaddress.IPv6Address('{}::a8a:{:x}'.format( vanir.config.vanir_ipv6_prefix, vm.dispid)) return ipaddress.IPv6Address('{}::a89:{:x}'.format( vanir.config.vanir_ipv6_prefix, vm.qid)) @vanir.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 @vanir.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.0', '10.139.1.1', '10.139.1.2', '10.139.1.3', '10.139.1.4', '10.139.1.5', ) return None def __init__(self, *args, **kwargs): self._firewall = None super(NetVMMixin, self).__init__(*args, **kwargs) @vanir.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 vanirsd # 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 @vanir.events.handler('domain-shutdown') def on_domain_shutdown(self, event, **kwargs): '''Cleanup network interfaces of connected, running VMs. This will allow re-reconnecting them cleanly later. ''' # pylint: disable=unused-argument for vm in self.connected_vms: if not vm.is_running(): continue try: vm.detach_network() except (vanir.exc.VanirException, libvirt.libvirtError): # ignore errors pass @vanir.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.attach_network() except (vanir.exc.VanirException, libvirt.libvirtError): vm.log.warning('Cannot attach network', exc_info=1) @vanir.events.handler('domain-pre-shutdown') def on_domain_pre_shutdown(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 vanir.exc.VanirVMError( self, 'There are other VMs connected to this VM: {}'.format( ', '.join(vm.name for vm in connected_vms))) def attach_network(self): '''Attach network in this machine to it's netvm.''' if not self.is_running(): raise vanir.exc.VanirVMNotRunningError(self) if self.netvm is None: raise vanir.exc.VanirVMError( 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 vanir.exc.VanirVMNotRunningError(self) if self.netvm is None: raise vanir.exc.VanirVMError( 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 = '/vanir-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-vanir`) 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') @vanir.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) @vanir.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) @vanir.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 vanir.exc.VanirVMNotStartedError( 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() @vanir.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) @vanir.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) @vanir.events.handler('domain-qdb-create') def on_domain_qdb_create(self, event): ''' Fills the VanirDB 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) @vanir.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 = vanir.firewall.Firewall(self) return self._firewall def has_firewall(self): ''' Return `True` if there are some vm specific firewall rules set ''' return os.path.exists(os.path.join(self.dir_path, self.firewall_conf))
class vanir(vanir.PropertyHolder): '''Main vanir application :param str store: path to ``vanir.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 .. event:: pool-add (subject, event, pool) When storage pool is added. Handler for this event can be asynchronous (a coroutine). :param subject: Event emitter :param event: Event name (``'pool-add'``) :param pool: Pool object .. event:: pool-pre-delete (subject, event, pool) When pool is deleted. Pool is still contained within app.pools dictionary. You may prevent removal by raising an exception. Handler for this event can be asynchronous (a coroutine). :param subject: Event emitter :param event: Event name (``'pool-pre-delete'``) :param pool: Pool object .. event:: pool-delete (subject, event, pool) When storage pool is deleted. The pool is already removed at this point. Handler for this event can be asynchronous (a coroutine). :param subject: Event emitter :param event: Event name (``'pool-delete'``) :param pool: Pool object Methods and attributes: ''' default_netvm = vanir.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 = vanir.VMProperty('default_template', load_stage=3, vmclass=vanir.vm.templatevm.TemplateVM, doc='Default template for new AppVMs', allow_none=True) updatevm = vanir.VMProperty('updatevm', load_stage=3, default=None, allow_none=True, doc='''Which VM to use as `yum` proxy for updating AdminVM and TemplateVMs''') clockvm = vanir.VMProperty('clockvm', load_stage=3, default=None, allow_none=True, doc='Which VM to use as NTP proxy for updating AdminVM') default_kernel = vanir.property('default_kernel', load_stage=3, doc='Which kernel to use when not overriden in VM') default_dispvm = vanir.VMProperty('default_dispvm', load_stage=3, default=None, doc='Default DispVM base for service calls', allow_none=True) management_dispvm = vanir.VMProperty('management_dispvm', load_stage=3, default=None, doc='Default DispVM base for managing VMs', allow_none=True) default_pool = vanir.property('default_pool', load_stage=3, default=_default_pool, setter=_setter_pool, doc='Default storage pool') default_pool_private = vanir.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 = vanir.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 = vanir.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 = vanir.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 = vanir.property('default_qrexec_timeout', load_stage=3, default=60, type=int, doc='''Default time in seconds after which qrexec connection attempt is deemed failed''') default_shutdown_timeout = vanir.property('default_shutdown_timeout', load_stage=3, default=60, type=int, doc='''Default time in seconds for VM shutdown to complete''') stats_interval = vanir.property('stats_interval', default=3, type=int, doc='Interval in seconds for VM stats reporting (memory, CPU usage)') # TODO #1637 #892 check_updates_vm = vanir.property('check_updates_vm', type=bool, setter=vanir.property.bool, default=True, doc='check for updates inside vanir') 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 = vanir.ext.get_extensions() #: collection of all VMs managed by this vanir 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 = VanirHost(self) if store is not None: self._store = store else: self._store = os.environ.get('VANIR_XML_PATH', os.path.join( vanir.config.vanir_base_dir, vanir.config.system_path['vanir_store_filename'])) super(vanir, 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/vanir/templates', '/usr/share/vanir/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 vanir.xml :throws EnvironmentError: failure on parsing store :throws xml.parsers.expat.ExpatError: failure on parsing store :raises lxml.etree.XMLSyntaxError: on syntax error in vanir.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 = vanir.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 vanir.exc.VanirException 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( vanir.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('vanir') 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 vanir.xml There are several problems with saving :file:`vanir.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('vanir').gr_gid) os.chmod(fh_new.name, 0o660) except KeyError: # group 'vanir' not found # don't change mode if no 'vanir' group in the system pass os.rename(fh_new.name, self._store) # update stored mtime, in case of multiple save() calls without # loading vanir.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 vanir.exc.VanirException( 'vanir XML store {!r} is missing; ' 'use vanir-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 vanir.exc.VanirException( 'Someone else modified vanir.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: vanir.Label(1, '0xcc0000', 'red'), 2: vanir.Label(2, '0xf57900', 'orange'), 3: vanir.Label(3, '0xedd400', 'yellow'), 4: vanir.Label(4, '0x73d216', 'green'), 5: vanir.Label(5, '0x555753', 'gray'), 6: vanir.Label(6, '0x3465a4', 'blue'), 7: vanir.Label(7, '0x75507b', 'purple'), 8: vanir.Label(8, '0x000000', 'black'), } assert max(self.labels.keys()) == vanir.config.max_default_label pool_configs = copy.deepcopy(vanir.config.defaults['pool_configs']) root_volume_group, root_thin_pool = \ vanir.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 vanir.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( vanir.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 vanir.exc.VanirException( '{} 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 ``vanir.vm`` group. Any package may supply their own classes. :param str clsname: name of the class :return type: class ''' try: return vanir.utils.get_entry_point_one( vanir.vm.VM_ENTRY_POINT, clsname) except KeyError: raise vanir.exc.VanirException( '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 if kwargs['template'] is None: raise vanir.exc.VanirValueError( 'Template for the Vanir not specified, nor default ' 'template set.') 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() @asyncio.coroutine def add_pool(self, name, **kwargs): """ Add a storage pool to config.""" if name in self.pools.keys(): raise vanir.exc.VanirException('pool named %s already exists \n' % name) kwargs['name'] = name pool = self._get_pool(**kwargs) ret = pool.setup() if asyncio.iscoroutine(ret): yield from ret self.pools[name] = pool yield from self.fire_event_async('pool-add', pool=pool) return pool @asyncio.coroutine def remove_pool(self, name): """ Remove a storage pool from config file. """ try: pool = self.pools[name] volumes = [(vm, volume) for vm in self.domains for volume in vm.volumes.values() if volume.pool is pool] if volumes: raise vanir.exc.VanirPoolInUseError(pool) prop_suffixes = ['', '_kernel', '_private', '_root', '_volatile'] for suffix in prop_suffixes: if getattr(self, 'default_pool' + suffix, None) is pool: raise vanir.exc.VanirPoolInUseError(pool, 'Storage pool is in use: set as {}'.format( 'default_pool' + suffix)) yield from self.fire_event_async('pool-pre-delete', pre_event=True, pool=pool) del self.pools[name] ret = pool.destroy() if asyncio.iscoroutine(ret): yield from ret yield from self.fire_event_async('pool-delete', pool=pool) except KeyError: return def get_pool(self, pool): ''' Returns a :py:class:`vanir.storage.Pool` instance ''' if isinstance(pool, vanir.storage.Pool): return pool try: return self.pools[pool] except KeyError: raise vanir.exc.VanirException('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 vanir.exc.VanirException('No pool name for pool') try: driver = kwargs['driver'] except KeyError: raise vanir.exc.VanirException('No driver specified for pool ' + name) try: klass = vanir.utils.get_entry_point_one( vanir.storage.STORAGE_ENTRY_POINT, driver) del kwargs['driver'] return klass(**kwargs) except KeyError: raise vanir.exc.VanirException('No driver %s for pool %s' % (driver, name)) def register_event_handlers(self): '''Register libvirt event handlers, which will translate libvirt events into vanir.events. This function should be called only in 'vanirsd' 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 vanir.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) @vanir.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,)): if obj is vm: # allow removed VM to reference itself continue for prop in obj.property_list(): try: if isinstance(prop, vanir.vm.VMProperty) and \ getattr(obj, prop.__name__) == vm: self.log.error( 'Cannot remove %s, used by %s.%s', vm, obj, prop.__name__) raise vanir.exc.VanirVMInUseError(vm, 'Domain is in ' 'use: {!r}; see /var/log/vanir/vanir.log in dom0 for ' 'details'.format(vm.name)) except AttributeError: pass @vanir.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 @vanir.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 @vanir.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'] @vanir.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 vanir.exc.VanirVMNotRunningError(newvalue, 'Cannot change {!r} to domain that ' 'is not running ({!r}).'.format(name, newvalue.name)) @vanir.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) @vanir.events.handler('property-set:default_netvm') def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument for vm in self.domains: if hasattr(vm, 'provides_network') and not vm.provides_network and \ hasattr(vm, 'netvm') and vm.property_is_default('netvm'): # fire property-del:netvm as it is responsible for resetting # netvm to it's default value vm.fire_event('property-pre-del:netvm', pre_event=True, name='netvm', oldvalue=oldvalue) vm.fire_event('property-del:netvm', name='netvm', oldvalue=oldvalue)
class TestVM(vanir.vm.BaseVM): qid = vanir.property('qid', type=int) name = vanir.property('name') testprop = vanir.property('testprop') testlabel = vanir.property('testlabel') defaultprop = vanir.property('defaultprop', default='defaultvalue')