class MyModel(models.Driver): field = base.ParamField(default=10) number = base.ParamField(default=None, choices=(None, 'one', 'two')) multi = base.ParamMultiField( sub1=base.ParamField(default='abc'), sub2=base.ParamField(default=15, choices=(11, 13, 15)), )
class DiskDevice(base.ParamedModel): class Meta(object): db_table = 'devops_diskdevice' app_label = 'devops' node = models.ForeignKey('Node', null=False) volume = models.ForeignKey('Volume', null=True) # TODO(astudenov): temporarily added for ipmi driver # and driverless testcase. These fields should be removed # after refactoring of volume section in themplate device = base.ParamField() type = base.ParamField() bus = base.ParamField() target_dev = base.ParamField()
class IronicDriver(driver.Driver): """Ironic driver Note: This class is imported as Driver at .__init__.py """ os_auth_token = base.ParamField() ironic_url = base.ParamField(default="http://localhost:6385/") agent_kernel_url = base.ParamField() agent_ramdisk_url = base.ParamField() @property def conn(self): """Connection to ironic api""" logger.debug("Ironic client is connecting to {0}".format( self.ironic_url)) kwargs = { 'os_auth_token': self.os_auth_token, 'ironic_url': self.ironic_url } return client.get_client(1, **kwargs)
class IronicVolume(volume.Volume): """Note: This class is imported as Volume at .__init__.py """ capacity = base.ParamField(default=None) # in gigabytes format = base.ParamField(default='qcow2', choices=('qcow2', 'raw')) source_image = base.ParamField(default=None) source_image_checksum = base.ParamField(default=None) cloudinit_meta_data = base.ParamField(default=None) cloudinit_user_data = base.ParamField(default=None)
class DummyDriver(driver.Driver): """Example of driver implementation Note: This class is imported as Driver at .__init__.py This class should contain parameters specified in template group['driver']['params'] yaml example: groups: - name: rack-01 driver: name: devops.driver.dummy.dummy_driver params: dummy_parameter: 15 choice_parameter: two nested: parameter: abc """ # example parameters dummy_parameter = base.ParamField(default=10) choice_parameter = base.ParamField(default='one', choices=('one', 'two', 'three')) nested = base.ParamMultiField(parameter=base.ParamField()) def get_allocated_networks(self): """This methods return list of already allocated networks. implement it if your driver knows which network pools are already taken by the driver """ # example format return ['192.168.0.0/24', '192.168.1.0/24']
class DummyVolume(volume.Volume): """Example implementation of volume Note: This class is imported as Volume at .__init__.py Volume is image or disk which should be mounted to a specific Node """ # example parameter size = base.ParamField(default=1024) def define(self): """Define Define method is called one time after environment successfully saved from template to database. It should contain something to prepare an instance of Volume before usage in Node class """ # driver instance is available from property self.driver # so you can use any parameters defined in driver print(self.driver) print(self.driver.dummy_parameter) print('Do something before define') super(DummyVolume, self).define() print('Do something after define') def erase(self): """Erase Erase method is called one time when you want remove existing volume """ print('Do something before erase') super(DummyVolume, self).erase() print('Do something after erase')
class DummyL2NetworkDevice(network.L2NetworkDevice): """Example implementation of l2 network device. Note: This class is imported as L2NetworkDevice at .__init__.py L2NetworkDevice represents node/device which acts like switch or router yaml example: l2_network_devices: admin: address_pool: fuelweb_admin-pool01 dhcp: false other: abcd public: address_pool: public-pool01 dhcp: false other: efgh """ # example parameter dhcp = base.ParamField(default=False) other = base.ParamField(default='') def define(self): """Define Define method is called one time after environment successfully saved from template to database. It should contain something to prepare an instance of L2NetworkDevice before start """ print('Do something before define') # driver instance is available from property self.driver # so you can use any parameters defined in driver print(self.driver) print(self.driver.dummy_parameter) print(self.driver.nested.parameter) # parameters of L2NetworkDevice print(self.dhcp) print(self.other) # name of L2NetworkDevice print(self.name) # associated adress pool print(self.address_pool) # and network print(self.address_pool.ip_network) super(DummyL2NetworkDevice, self).define() print('Do something after define') def start(self): """Start Start method is called every time you want to boot up previsously saved and defined l2 network device """ print('implementation of start') def destroy(self): """Destroy Destroy method is called every time you want to power off previsously started l2 network device """ print('implementation of destroy') def erase(self): """Erase Erase method is called one time when you want remove existing l2 network device """ super(DummyL2NetworkDevice, self).erase() print('Do something after erase')
class DummyNode(node.Node): """Example implementation of node Note: This class is imported as Node at .__init__.py Node is a server which will be used for deloying of openstack depending on node role yaml example: nodes: - name: slave role: fuel_slave params: cpu: 2 memory: 3072 boot: - hd - cdrom volumes: - name: system size: 75 format: qcow2 interfaces: - label: eth0 l2_network_device: admin interface_model: e1000 network_config: eth0: networks: - fuelweb_admin """ cpu = base.ParamField(default=1) memory = base.ParamField(default=1024) boot = base.ParamField(default=['network', 'cdrom', 'hd']) def define(self): """Define Define method is called one time after environment successfully saved from template to database. It should contain something to prepare an instance of Node before start """ # driver instance is available from property self.driver print(self.driver) # node parameters print(self.cpu) print(self.memory) print(self.boot) # list of disk devices print(self.disk_devices) # list of network intefraces print(self.interfaces) print('Do something before define') super(DummyNode, self).define() print('Do something after define') def start(self): """Start method is called every time you want to boot up node""" print('implementation of start') def destroy(self): """Destroy Destroy method is called every time you want to power off previsously started node """ print('implementation of destroy') def erase(self): """Erase Erase method is called one time when you want remove existing node """ print('Do something before erase') super(DummyNode, self).erase() print('Do something after erase')
class Interface(base.ParamedModel): """Describes a network interface configuration Specify the abstract label of the interface (you can use labels that match the real interface names or use any suitable names). 'l2_network_device' describes the switch name (virtual or hardware) to which the interface is connected. 'features' is a json list with any strings you want use to mark interfaces with specific features and use these marks in 3rd-party libraries to perform highlevel configuration of the right interfaces for your product. Template example (interfaces): --------------------------------- - name: some_node_name params: ... interfaces: - label: iface0 l2_network_device: admin - label: iface1 l2_network_device: data mac_address: !os_env IFACE_MAC_ADDRESS, 00:11:22:33:44:55 features: ['dpdk', 'dpdk_pci: 0000:05:00.1'] """ class Meta(object): db_table = 'devops_interface' app_label = 'devops' node = models.ForeignKey('Node') l2_network_device = models.ForeignKey('L2NetworkDevice', null=True) label = models.CharField(max_length=255, null=True) mac_address = models.CharField(max_length=255, unique=True, null=False) type = models.CharField(max_length=255, null=False) model = base.choices('virtio', 'e1000', 'pcnet', 'rtl8139', 'ne2k_pci') features = base.ParamField(default=[]) @property def driver(self): return self.node.driver # LEGACY, for fuel-qa compatibility if MULTIPLE_NETWORKS enabled @property def network(self): return self.l2_network_device @property def target_dev(self): return self.label @property def addresses(self): return self.address_set.all() @property def network_config(self): return self.node.networkconfig_set.get(label=self.label) def define(self): self.save() def remove(self): self.delete() def add_address(self): """Assign an IP address to the interface Try to get an IP from reserved IP with name '<group>_<node>' , or generate next IP if reserved IP wasn't found. Next IP is generated from the DHCP ip_range, or from the network range [+2:-2] of all available addresses in the address pool. """ reserved_ip_name = helpers.underscored(self.node.group.name, self.node.name) reserved_ip = self.l2_network_device.address_pool.get_ip( reserved_ip_name) ip = reserved_ip or self.l2_network_device.address_pool.next_ip() Address.objects.create( ip_address=str(ip), interface=self, ) @property def is_blocked(self): """Show state of interface""" return False def block(self): """Block traffic on interface""" pass def unblock(self): """Unblock traffic on interface""" pass @classmethod def interface_create(cls, l2_network_device, node, label, if_type='network', mac_address=None, model='virtio', features=None): """Create interface :rtype : Interface """ interface = cls.objects.create( l2_network_device=l2_network_device, node=node, label=label, type=if_type, mac_address=mac_address or helpers.generate_mac(), model=model, features=features or []) if (interface.l2_network_device and interface.l2_network_device.address_pool is not None): interface.add_address() return interface
class AddressPool(base.ParamedModel, base.BaseModel): """Address pool address_pools: <address_pool_name>: net: <IPNetwork[:prefix]> params: # Optional params for the address pool vlan_start: <int> vlan_end: <int> ip_reserved: <'gateway'>:<int|IPAddress> # Reserved for gateway. <'l2_network_device'>:<int|IPAddress> # Reserved for local IP # for libvirt networks. <'groupname_nodename'>:<int|IPAddress> # Reserved for specific node # IP address for iface in # this address pool. ... # user-defined IPs (for fuel-qa) ip_ranges: <group_name>: [<int|IPAddress>, <int|IPAddress>] ... # user-defined ranges (for fuel-qa, 'floating' for example) Template example (address_pools): --------------------------------- address_pools: fuelweb_admin-pool01: net: 172.0.0.0/16:24 params: ip_reserved: gateway: 1 l2_network_device: 1 # l2_network_device will get the # IP address = 172.0.*.1 (net + 1) ip_ranges: default: [2, -2] # admin IP range for 'default' nodegroup name public-pool01: net: 12.34.56.0/26 # Some WAN routed to the test host. params: vlan_start: 100 ip_reserved: gateway: 12.34.56.1 l2_network_device: 12.34.56.62 # l2_network_device will be assumed # with this IP address. # It will be used for create libvirt # network if libvirt driver is used. ip_ranges: default: [2, 127] # public IP range for 'default' nodegroup name floating: [128, -2] # floating IP range storage-pool01: net: 172.0.0.0/16:24 params: vlan_start: 101 ip_reserved: l2_network_device: 1 # 172.0.*.1 management-pool01: net: 172.0.0.0/16:24 params: vlan_start: 102 ip_reserved: l2_network_device: 1 # 172.0.*.1 private-pool01: net: 192.168.0.0/24:26 params: vlan_start: 103 ip_reserved: l2_network_device: 1 # 192.168.*.1 """ class Meta(object): unique_together = ('name', 'environment') db_table = 'devops_address_pool' app_label = 'devops' environment = models.ForeignKey('Environment') name = models.CharField(max_length=255) net = models.CharField(max_length=255, unique=True) vlan_start = base.ParamField() vlan_end = base.ParamField() tag = base.ParamField() # DEPRECATED, use vlan_start instead # ip_reserved = {'l2_network_device': 'm.m.m.50', # 'gateway': 'n.n.n.254', ...} ip_reserved = base.ParamField(default={}) # ip_ranges = {'range_a': ('x.x.x.x', 'y.y.y.y'), # 'range_b': ('a.a.a.a', 'b.b.b.b'), ...} ip_ranges = base.ParamField(default={}) # NEW. Warning: Old implementation returned self.net @property def ip_network(self): """Return IPNetwork representation of self.ip_network field. :return: IPNetwork() """ return netaddr.IPNetwork(self.net) @property def gateway(self): """Get the default network gateway This property returns only default network gateway. :return: reserved IP address with key 'gateway', or the first address in the address pool (for fuel-qa compatibility). """ return self.get_ip('gateway') or str(self.ip_network[1]) def ip_range_start(self, range_name): """Return the IP address of start the IP range 'range_name' :return: str(IP) or None """ if range_name in self.ip_ranges: return str(self.ip_ranges.get(range_name)[0]) else: logger.debug("IP range '{0}' not found in the " "address pool {1}".format(range_name, self.name)) return None def ip_range_end(self, range_name): """Return the IP address of end the IP range 'range_name' :return: str(IP) or None """ if range_name in self.ip_ranges: return str(self.ip_ranges.get(range_name)[1]) else: logger.debug("IP range '{0}' not found in the " "address pool {1}".format(range_name, self.name)) return None def ip_range_set(self, range_name, ip_range_start, ip_range_end): """Set IP range in the address pool :param range_name: str, name of the range :param ip_range_start: str, first IP of the range :param ip_range_end: str, last IP of the range :rtype: None or exception DevopsError If range_name already exists, then DevopsError raises. """ if range_name in self.ip_ranges: raise error.DevopsError( "Setting IP range '{0}' for address pool '{1}' failed: range " "already exists".format(range_name, self.name)) self.ip_ranges[range_name] = (ip_range_start, ip_range_end) self.save() def get_ip(self, ip_name): """Return the reserved IP For example, 'gateway' is one of the common reserved IPs :return: str(IP) or None """ if ip_name in self.ip_reserved: return str(self.ip_reserved.get(ip_name)) else: logger.debug("Reserved IP '{0}' not found in the " "address pool {1}".format(ip_name, self.name)) return None def next_ip(self): """Get next IP address from the address pool If 'dhcp' ip_range specified for the address pool, then the IP addresses will be taken from this pool. Else, IP addresses will be taken from the range [ x.x.x.x + 2 : x.x.x.x - 2 ] """ range_start = netaddr.IPAddress( self.ip_range_start('dhcp') or self.ip_network[2]) range_end = netaddr.IPAddress( self.ip_range_end('dhcp') or self.ip_network[-2]) for ip in self.ip_network.iter_hosts(): # if ip < self.ip_pool_start or ip > self.ip_pool_end: # Skip net, gw and broadcast addresses in the address pool if ip < range_start or ip > range_end: continue already_exists = Address.objects.filter( interface__l2_network_device__address_pool=self, ip_address=str(ip)).exists() if already_exists: continue return ip raise error.DevopsError( "No more free addresses in the address pool {0}" " with CIDR {1}".format(self.name, self.net)) @classmethod def _safe_create_network(cls, name, pool, environment, **params): for ip_network in pool: if cls.objects.filter(net=str(ip_network)).exists(): continue new_params = deepcopy(params) new_params['net'] = ip_network try: with transaction.atomic(): return cls.objects.create( environment=environment, name=name, **new_params ) except IntegrityError as e: logger.debug(e) if 'name' in str(e): raise error.DevopsError( 'AddressPool with name "{}" already exists' ''.format(name)) continue raise error.DevopsError( "There is no network pool available for creating " "address pool {}".format(name)) @classmethod def address_pool_create(cls, name, environment, pool=None, **params): """Create network :rtype : Network """ if pool is None: pool = network.IpNetworksPool( networks=[netaddr.IPNetwork('10.0.0.0/16')], prefix=24, allocated_networks=environment.get_allocated_networks()) address_pool = cls._safe_create_network( environment=environment, name=name, pool=pool, **params ) # Translate indexes into IP addresses for ip_reserved and ip_ranges def _relative_to_ip(ip_network, ip_id): """Get an IP from IPNetwork ip's list by index :param ip_network: IPNetwork object :param ip_id: string, if contains '+' or '-' then it is used as index of an IP address in ip_network, else it is considered as IP address. :rtype : str(IP) """ if isinstance(ip_id, int): return str(ip_network[int(ip_id)]) else: return str(ip_id) if 'ip_reserved' in params: for ip_res in params['ip_reserved'].keys(): ip = _relative_to_ip(address_pool.ip_network, params['ip_reserved'][ip_res]) params['ip_reserved'][ip_res] = ip # Store to template address_pool.ip_reserved[ip_res] = ip # Store to the object if 'ip_ranges' in params: for ip_range in params['ip_ranges']: ipr_start = _relative_to_ip(address_pool.ip_network, params['ip_ranges'][ip_range][0]) ipr_end = _relative_to_ip(address_pool.ip_network, params['ip_ranges'][ip_range][1]) params['ip_ranges'][ip_range] = (ipr_start, ipr_end) address_pool.ip_ranges[ip_range] = (ipr_start, ipr_end) address_pool.save() return address_pool
class MyMultiModel(models.Driver): multi = base.ParamMultiField(sub1=base.ParamField(default='abc'), multi2=base.ParamMultiField( sub2=base.ParamField(default=13), ))
class Interface(base.ParamedModel): class Meta(object): db_table = 'devops_interface' app_label = 'devops' node = models.ForeignKey('Node') l2_network_device = models.ForeignKey('L2NetworkDevice', null=True) label = models.CharField(max_length=255, null=True) mac_address = models.CharField(max_length=255, unique=True, null=False) type = models.CharField(max_length=255, null=False) model = base.choices('virtio', 'e1000', 'pcnet', 'rtl8139', 'ne2k_pci') features = base.ParamField(default=[]) @property def driver(self): return self.node.driver # LEGACY, for fuel-qa compatibility if MULTIPLE_NETWORKS enabled @property def network(self): return self.l2_network_device @property def target_dev(self): return self.label @property def addresses(self): return self.address_set.all() @property def network_config(self): return self.node.networkconfig_set.get(label=self.label) def define(self): self.save() def remove(self): self.delete() def add_address(self): ip = self.l2_network_device.address_pool.next_ip() Address.objects.create( ip_address=str(ip), interface=self, ) @property def is_blocked(self): """Show state of interface""" return False def block(self): """Block traffic on interface""" pass def unblock(self): """Unblock traffic on interface""" pass @classmethod def interface_create(cls, l2_network_device, node, label, if_type='network', mac_address=None, model='virtio', features=None): """Create interface :rtype : Interface """ interface = cls.objects.create( l2_network_device=l2_network_device, node=node, label=label, type=if_type, mac_address=mac_address or helpers.generate_mac(), model=model, features=features or []) if (interface.l2_network_device and interface.l2_network_device.address_pool is not None): interface.add_address() return interface
class Node( six.with_metaclass( ExtendableNodeType, base.ParamedModel, base.BaseModel)): class Meta(object): unique_together = ('name', 'group') db_table = 'devops_node' app_label = 'devops' group = models.ForeignKey('Group', null=True) name = models.CharField(max_length=255, unique=False, null=False) role = models.CharField(max_length=255, null=True) kernel_cmd = base.ParamField() ssh_port = base.ParamField(default=22) bootstrap_timeout = base.ParamField(default=600) deploy_timeout = base.ParamField(default=3600) deploy_check_cmd = base.ParamField() @property def driver(self): drv = self.group.driver # LEGACY (fuel-qa compatibility requires), TO REMOVE def node_active(node): return node.is_active() drv.node_active = node_active return drv @functional.cached_property def ext(self): try: # noinspection PyPep8Naming ExtCls = loader.load_class( 'devops.models.node_ext.{ext_name}:NodeExtension' ''.format(ext_name=self.role or 'default')) return ExtCls(node=self) except ImportError: logger.debug('NodeExtension is not found for role: {!r}' ''.format(self.role)) return None def define(self, *args, **kwargs): for iface in self.interfaces: iface.define() self.save() def start(self, *args, **kwargs): pass def destroy(self, *args, **kwargs): ssh_client.SSHClient.close_connections() def erase(self, *args, **kwargs): self.remove() def remove(self, *args, **kwargs): ssh_client.SSHClient.close_connections() self.erase_volumes() for iface in self.interfaces: iface.remove() self.delete() def suspend(self, *args, **kwargs): ssh_client.SSHClient.close_connections() def resume(self, *args, **kwargs): pass def is_active(self): return False def snapshot(self, *args, **kwargs): ssh_client.SSHClient.close_connections() def revert(self, *args, **kwargs): ssh_client.SSHClient.close_connections() # for fuel-qa compatibility def has_snapshot(self, *args, **kwargs): return True def reboot(self): pass def shutdown(self): ssh_client.SSHClient.close_connections() def reset(self): ssh_client.SSHClient.close_connections() def get_vnc_port(self): return None # for fuel-qa compatibility def get_snapshots(self): """Return full snapshots objects""" return [] @property def disk_devices(self): return self.diskdevice_set.all() @property def interfaces(self): return self.interface_set.order_by('id') @property def network_configs(self): return self.networkconfig_set.all() # LEGACY, for fuel-qa compatibility @property def is_admin(self): return 'master' in str(self.role) # LEGACY, for fuel-qa compatibility @property def is_slave(self): return self.role == 'fuel_slave' def next_disk_name(self): disk_names = ('sd' + c for c in list('abcdefghijklmnopqrstuvwxyz')) for disk_name in disk_names: if not self.disk_devices.filter(target_dev=disk_name).exists(): return disk_name # TODO(astudenov): LEGACY, TO REMOVE def interface_by_network_name(self, network_name): logger.warning('interface_by_network_name is deprecated in favor of ' 'get_interface_by_network_name') warnings.warn( "'Node.interface_by_network_name' is deprecated. " "Use 'Node.get_interface_by_network_name' instead.", DeprecationWarning ) return self.get_interface_by_network_name(network_name=network_name) def get_interface_by_network_name(self, network_name): return self.interface_set.get( l2_network_device__name=network_name) def get_interface_by_nailgun_network_name(self, name): for net_conf in self.networkconfig_set.all(): if name in net_conf.networks: label = net_conf.label break else: return None return self.interface_set.get(label=label) # NOTE: this method works only for master node def get_ip_address_by_network_name(self, name, interface=None): interface = interface or self.interface_set.filter( l2_network_device__name=name).order_by('id')[0] return interface.address_set.get(interface=interface).ip_address # NOTE: this method works only for master node def get_ip_address_by_nailgun_network_name(self, name): interface = self.get_interface_by_nailgun_network_name(name) return interface.address_set.first().ip_address # LEGACY def remote( self, network_name, login=None, password=None, private_keys=None, auth=None): """Create SSH-connection to the network NOTE: this method works only for master node :rtype : SSHClient """ return ssh_client.SSHClient( self.get_ip_address_by_network_name(network_name), username=login, password=password, private_keys=private_keys, auth=auth) # LEGACY # NOTE: this method works only for master node def await(self, network_name, timeout=120, by_port=22): helpers.wait_pass( lambda: helpers.tcp_ping_( self.get_ip_address_by_network_name(network_name), by_port), timeout=timeout) # NEW def add_interfaces(self, interfaces): for interface in interfaces: label = interface['label'] l2_network_device_name = interface.get('l2_network_device') interface_model = interface.get('interface_model', 'virtio') mac_address = interface.get('mac_address') features = interface.get('features', None) self.add_interface( label=label, l2_network_device_name=l2_network_device_name, mac_address=mac_address, interface_model=interface_model, features=features) # NEW def add_interface(self, label, l2_network_device_name, interface_model, mac_address=None, features=None): if l2_network_device_name: env = self.group.environment l2_network_device = env.get_env_l2_network_device( name=l2_network_device_name) else: l2_network_device = None cls = self.driver.get_model_class('Interface') return cls.interface_create( node=self, label=label, l2_network_device=l2_network_device, mac_address=mac_address, model=interface_model, features=features, ) # NEW def add_network_configs(self, network_configs): for label, data in network_configs.items(): self.add_network_config( label=label, networks=data.get('networks', []), aggregation=data.get('aggregation'), parents=data.get('parents', []), ) # NEW def add_network_config(self, label, networks=None, aggregation=None, parents=None): if networks is None: networks = [] if parents is None: parents = [] network.NetworkConfig.objects.create( node=self, label=label, networks=networks, aggregation=aggregation, parents=parents, ) # NEW def add_volumes(self, volumes): for vol_params in volumes: self.add_volume( **vol_params ) # NEW def add_volume(self, name, device='disk', bus='virtio', **params): cls = self.driver.get_model_class('Volume') if 'backing_store' in params: # Backing storage volume have to be defined in group params['backing_store'] = self.group.get_volume( name=params['backing_store']) volume = cls.objects.create( node=self, name=name, **params ) # TODO(astudenov): make a separete section in template for disk devices self.attach_volume( volume=volume, device=device, bus=bus, ) return volume # NEW def attach_volume(self, volume, device='disk', type='file', bus='virtio', target_dev=None): """Attach volume to node :rtype : DiskDevice """ cls = self.driver.get_model_class('DiskDevice') return cls.objects.create( device=device, type=type, bus=bus, target_dev=target_dev or self.next_disk_name(), volume=volume, node=self) # NEW def get_volume(self, **kwargs): try: return self.volume_set.get(**kwargs) except volume.Volume.DoesNotExist: raise error.DevopsObjNotFound(volume.Volume, **kwargs) # NEW def get_volumes(self, **kwargs): return self.volume_set.filter(**kwargs).order_by('id') # NEW def erase_volumes(self): for vol in self.get_volumes(): vol.erase()
class IpmiNode(node.Node): """IPMI Node Intel IPMI specification: http://www.intel.ru/content/dam/www/public/us/en/documents/ product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf The Node shall provide ability to manage remote baremetal node through ipmi interface by using ipmitool: http://sourceforge.net/projects/ipmitool/ Take into account that it is suitable tool according to licence criteria. More info can be found here: http://ipmiutil.sourceforge.net/docs/ipmisw-compare.htm Note: Power management - on/off/reset User management - user list Chassis management - chassis info Virtual Storage management - ISO attache Sensors management - get sensors info Node management - start/stop/reset :param ipmi_user: str - the user login for IPMI board :param ipmi_password: str - the user password :param ipmi_previlegies: str - the user privileges level (OPERATOR) :param ipmi_host: str - remote host name :param ipmi_port: int - remote port number :param ipmi_lan_interface: str - the lan interface (lan, lanplus) """ uuid = base.ParamField() # LEGACY, for compatibility reason boot = base.ParamField(default='pxe') force_set_boot = base.ParamField(default=True) ipmi_user = base.ParamField() ipmi_password = base.ParamField() ipmi_previlegies = base.ParamField(default='OPERATOR') ipmi_host = base.ParamField() ipmi_lan_interface = base.ParamField(default="lanplus") ipmi_port = base.ParamField(default=623) @functional.cached_property def conn(self): """Connection to ipmi api""" return ipmi_client.IpmiClient(self.ipmi_user, self.ipmi_password, self.ipmi_host, self.ipmi_previlegies, self.ipmi_lan_interface, self.ipmi_port, self.name) def _wait_power_off(self): helpers.wait( lambda: not self.is_active(), timeout=60, timeout_msg="Node {0} / {1} wasn't stopped in 60 sec".format( self.name, self.ipmi_host)) def exists(self): """Check if node exists :param: None :return: bool - True if successful, False otherwise """ return self.conn.check_remote_host() def is_active(self): """Check if node is active Note: we have to check power on and we have take into account that OS is working on remote host TODO: let's double check remote OS despite power is on :param: None :return: bool - True if successful, False otherwise. """ return 0 == self.conn.power_status() def define(self): """Prepare node to start TODO: Mount ISO TODO: Set boot device Note: need to set boot device at first. Create record in DB """ self.uuid = uuid.uuid4() super(IpmiNode, self).define() def start(self): """Node start. Power on """ if self.force_set_boot: # Boot device is not stored in bios, so it should # be set every time when node starts. self.conn.chassis_set_boot(self.boot) if self.is_active(): # Re-start active node self.reboot() else: self.conn.power_on() helpers.wait( self.is_active, timeout=60, timeout_msg="Node {0} / {1} wasn't started in 60 sec".format( self.name, self.ipmi_host)) def destroy(self): """Node destroy. Power off """ self.conn.power_off() self._wait_power_off() super(IpmiNode, self).destroy() def remove(self): """Node remove. Power off """ if self.is_active(): self.conn.power_off() self._wait_power_off() super(IpmiNode, self).remove() def reset(self): """Node reset. Power reset """ self.conn.power_reset() def reboot(self): """Node reboot. Power reset """ self.conn.power_reset() def shutdown(self): """Shutdown Node """ self.conn.power_off() self._wait_power_off() super(IpmiNode, self).shutdown()
class IronicNode(node.Node): """Note: This class is imported as Node at .__init__.py """ uuid = base.ParamField() bootmenu_timeout = base.ParamField(default=0) numa = base.ParamField(default=[]) root_volume_name = base.ParamField() cloud_init_volume_name = base.ParamField() cloud_init_iface_up = base.ParamField() ironic_driver = base.ParamField(default='agent_ipmitool') boot = base.ParamField(default='pxe') force_set_boot = base.ParamField(default=True) ipmi_user = base.ParamField() ipmi_password = base.ParamField() ipmi_previlegies = base.ParamField(default='OPERATOR') ipmi_host = base.ParamField() ipmi_lan_interface = base.ParamField(default="lanplus") ipmi_port = base.ParamField(default=623) # Required in cases of changed provisioning states wait_active_timeout = base.ParamField(default=900) def exists(self, timeout=5): """Check if node exists :rtype : Boolean """ try: with helpers.RunLimit(timeout): return any([ self.uuid == node.uuid for node in self.driver.conn.node.list() ]) except error.TimeoutError: logger.error("Ironic API is not responded for {0}sec, assuming " "that node {1} is absent".format(timeout, self.name)) return False def is_active(self, timeout=5): """Check if node is active :rtype : Boolean """ try: with helpers.RunLimit(timeout): states = self.driver.conn.node.states(self.uuid) return (states['provision_state'] == 'active' and states['power_state'] == 'power on') except error.TimeoutError: logger.error("Ironic API is not responded for {0}sec, assuming " "that node {1} is absent".format(timeout, self.name)) return False @property def ironic_node_name(self): #return helpers.underscored( # helpers.deepgetattr(self, 'group.environment.name'), # self.name, #).replace("_", "-") return self.name.replace("_", "-") def define(self): """Define node :rtype : None """ root_volume = self.get_volume(name=self.root_volume_name) # Necessary only once, when node is registered to ironic node = self.driver.conn.node.create(driver=self.ironic_driver, name=self.ironic_node_name, driver_info={ 'deploy_kernel': self.driver.agent_kernel_url, 'deploy_ramdisk': self.driver.agent_ramdisk_url, 'ipmi_address': self.ipmi_host, 'ipmi_username': self.ipmi_user, 'ipmi_password': self.ipmi_password, 'ipmi_priv_level': self.ipmi_previlegies, }) logger.debug("Created Ironic node: {0}".format(node)) for interface in self.interfaces: if interface.mac_address: port = self.driver.conn.port.create( node_uuid=node.uuid, address=interface.mac_address, ) logger.debug("Created Ironic node port: {0}".format(port)) # Necessary for each deploy/redeploy patch = [{ 'path': '/instance_info/root_gb', 'value': root_volume.capacity, 'op': 'add' }, { 'path': '/instance_info/image_source', 'value': root_volume.source_image, 'op': 'add' }, { 'path': '/instance_info/image_checksum', 'value': root_volume.source_image_checksum, 'op': 'add' }] logger.debug("Updating Ironic node with: {0}".format(patch)) updated_node = self.driver.conn.node.update( node.uuid, patch=patch, ) logger.debug("Updated Ironic node: {0}".format(updated_node)) # TODO(ddmitriev): node-set-provision-state if self.cloud_init_volume_name is not None: configdrive = self.__create_configdrive() else: configdrive = None self.driver.conn.node.set_provision_state( node_uuid=node.uuid, configdrive=configdrive, state='active', ) logger.debug("Set provision state to 'active' for node {0} {1}".format( node.name, node.uuid)) self.uuid = node.uuid super(IronicNode, self).define() def wait_for_state(self, expected_state, timeout=600): threshold = time.time() + timeout while not timeout or time.time() < threshold: try: self.driver.conn.node.wait_for_provision_state( node_ident=self.uuid, expected_state='active', timeout=self.wait_active_timeout) return except exc.StateTransitionFailed: # When node is deploying, there can be non-critical errors # during state transitions, let's skip them. time.sleep(10) raise exc.StateTransitionTimeout( 'Node {0} with uuid={1} failed to reach state {2} in {3} seconds'. format(self.name, self.uuid, expected_state, timeout)) def start(self): """Start the node (power on)""" logger.info( "Starting Ironic node {0}(uuid={1}) with timeout={2}".format( self.name, self.uuid, self.wait_active_timeout)) self.wait_for_state(expected_state='active', timeout=self.wait_active_timeout) self.driver.conn.node.set_power_state( node_id=self.uuid, state='on', ) super(IronicNode, self).start() def destroy(self, *args, **kwargs): """Stop the node (power off)""" if self.is_active(): self.driver.conn.node.set_power_state( node_id=self.uuid, state='off', soft=False, ) super(IronicNode, self).destroy() @decorators.retry(Exception, count=10, delay=20) def remove(self, *args, **kwargs): if self.uuid: if self.exists(): # self.destroy() logger.info("Removing Ironic node {0}(uuid={1})".format( self.name, self.uuid)) try: self.driver.conn.node.set_maintenance( node_id=self.uuid, state=True, maint_reason="Removing the node from devops environment" ) self.driver.conn.node.delete(self.uuid) except common.apiclient.exceptions.BadRequest: # Allow to remove node from fuel-devops if ironic API down pass super(IronicNode, self).remove() def reboot(self): """Reboot node gracefully :rtype : None """ self.wait_for_state(expected_state='active', timeout=self.wait_active_timeout) self.driver.conn.node.set_power_state( node_id=self.uuid, state='reboot', soft=True, ) super(IronicNode, self).reboot() def shutdown(self): """Shutdown node gracefully :rtype : None """ super(IronicNode, self).shutdown() self.wait_for_state(expected_state='active', timeout=self.wait_active_timeout) self.driver.conn.node.set_power_state( node_id=self.uuid, state='off', soft=True, ) def reset(self): """Reboot node""" super(IronicNode, self).reset() self.wait_for_state(expected_state='active', timeout=self.wait_active_timeout) self.driver.conn.node.set_power_state( node_id=self.uuid, state='reboot', soft=False, ) def __create_configdrive(self): """Builds setting iso to send basic configuration for cloud image Returns a gzipped, base64-encoded configuration drive string. """ if self.cloud_init_volume_name is None: return None volume = self.get_volume(name=self.cloud_init_volume_name) interface = self.interface_set.get(label=self.cloud_init_iface_up) admin_ip = self.get_ip_address_by_network_name(name=None, interface=interface) env_name = self.group.environment.name dir_path = os.path.join(settings.CLOUD_IMAGE_DIR, env_name) cloud_image_settings_path = os.path.join( dir_path, 'configdrive_{0}.iso'.format(self.ironic_node_name)) meta_data_path = os.path.join(dir_path, "meta-data") user_data_path = os.path.join(dir_path, "user-data") interface_name = interface.label admin_ap = interface.l2_network_device.address_pool gateway = str(admin_ap.gateway) admin_netmask = str(admin_ap.ip_network.netmask) admin_network = str(admin_ap.ip_network) hostname = self.name cloud_image_settings.generate_cloud_image_settings( cloud_image_settings_path=cloud_image_settings_path, meta_data_path=meta_data_path, user_data_path=user_data_path, admin_network=admin_network, interface_name=interface_name, admin_ip=admin_ip, admin_netmask=admin_netmask, gateway=gateway, hostname=hostname, meta_data_content=volume.cloudinit_meta_data, user_data_content=volume.cloudinit_user_data, ) cmd = 'gzip -9 -c {0} | base64'.format(cloud_image_settings_path) result = subprocess_runner.Subprocess.check_call(cmd) # Clear temporary files if os.path.exists(dir_path): shutil.rmtree(dir_path) return ''.join(result['stdout'])