diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 330e7f91c4..d875adaf90 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -1032,6 +1032,20 @@ class ConductorManager(service.PeriodicService): True) return address.address except exception.AddressNotFoundByName: + LOG.info(f"cannot find address with name={name}") + return None + + def _lookup_static_ip_address_family(self, name, networktype, family): + """"Find a statically configured address based on name, network type, + and address family.""" + try: + # address names are refined by network type to ensure they are + # unique across different address pools + name = cutils.format_address_name(name, networktype) + address = self.dbapi.address_get_by_name_and_family(name, family) + return address.address + except exception.AddressNotFoundByNameAndFamily: + LOG.info(f"cannot find address with name={name}, family={family}") return None def _using_static_ip(self, ihost, personality=None, hostname=None): @@ -1295,7 +1309,7 @@ class ConductorManager(service.PeriodicService): ) func = "_generate_dnsmasq_hosts_file" - with open(temp_dnsmasq_hosts_file, 'w') as f_out,\ + with open(temp_dnsmasq_hosts_file, 'w') as f_out, \ open(temp_dnsmasq_addn_hosts_file, 'w') as f_out_addn: # Write entry for pxecontroller into addn_hosts file @@ -1966,7 +1980,19 @@ class ConductorManager(service.PeriodicService): pass def _create_or_update_address(self, context, hostname, ip_address, - iface_type, iface_id=None): + iface_type, iface_id=None, pool_uuid=None): + """Searches the address database and create or update accordingly + + Args: + hostname (str): The host name + ip_address (str): The IP address to be created or updated. + iface_type (str): The interface network type. + iface_id (int, optional): Interface ID that uses this address. Defaults to None. + pool_uuid (str, optional): The address pool uuid. Defaults to None. + + Returns: + sysinv.object.address: The updated or created address + """ if hostname is None or ip_address is None: return address_name = cutils.format_address_name(hostname, iface_type) @@ -1974,34 +2000,41 @@ class ConductorManager(service.PeriodicService): try: address = self.dbapi.address_get_by_address(ip_address) address_uuid = address['uuid'] + search_addr = self.dbapi.address_get_by_name_and_family(address_name, + address_family) # If name is already set, return - search_addr = cutils.get_primary_address_by_name(self.dbapi, - address_name, - iface_type, True) if search_addr: if (search_addr.uuid == address_uuid and iface_id is None): + LOG.info(f"returning, address '{address_uuid}' exists and iface_id is None") return except exception.AddressNotFoundByAddress: address_uuid = None - except exception.AddressNotFoundByName: + except exception.AddressNotFoundByNameAndFamily: pass - network = self.dbapi.network_get_by_type(iface_type) - address_pool_uuid = network.pool_uuid - address_pool = self.dbapi.address_pool_get(address_pool_uuid) - values = { - 'name': address_name, - 'family': address_family, - 'prefix': address_pool.prefix, - 'address': ip_address, - 'address_pool_id': address_pool.id, - } - if iface_id: - values['interface_id'] = iface_id - if address_uuid: - address = self.dbapi.address_update(address_uuid, values) + address_pool = None + if pool_uuid: + address_pool = self.dbapi.address_pool_get(pool_uuid) else: - address = self.dbapi.address_create(values) + network = self.dbapi.network_get_by_type(iface_type) + address_pool = self.dbapi.address_pool_get(network.pool_uuid) + + if address_pool: + values = { + 'name': address_name, + 'family': address_family, + 'prefix': address_pool.prefix, + 'address': ip_address, + 'address_pool_id': address_pool.id, + } + if iface_id: + values['interface_id'] = iface_id + + if address_uuid: + address = self.dbapi.address_update(address_uuid, values) + else: + address = self.dbapi.address_create(values) + self._generate_dnsmasq_hosts_file() return address @@ -2022,19 +2055,32 @@ class ConductorManager(service.PeriodicService): # controller must have cluster-host address already allocated if (host.personality != constants.CONTROLLER): + network = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_CLUSTER_HOST) + net_pools = self.dbapi.network_addrpool_get_by_network_id(network.id) + pool_uuid_list = list() + if net_pools: + for net_pool in net_pools: + pool_uuid_list.append(net_pool.address_pool_uuid) + else: + # we are coming from an upgrade without data-migration implemented for the + # dual stack feature + LOG.warning(f"Network {network.name} does not have network to address pool objects") + pool_uuid_list.append(network.pool_uuid) - cluster_host_address = self._lookup_static_ip_address( - host.hostname, constants.NETWORK_TYPE_CLUSTER_HOST) + hostname = host.hostname - if cluster_host_address is None: - address_name = cutils.format_address_name( - host.hostname, constants.NETWORK_TYPE_CLUSTER_HOST) - LOG.info("{} address not found. Allocating address for {}.".format( - address_name, host.hostname)) - host_network = self.dbapi.network_get_by_type( - constants.NETWORK_TYPE_CLUSTER_HOST) - self._allocate_pool_address(None, host_network.pool_uuid, - address_name) + for pool_uuid in pool_uuid_list: + pool = self.dbapi.address_pool_get(pool_uuid) + + cluster_host_address = self._lookup_static_ip_address_family( + host.hostname, constants.NETWORK_TYPE_CLUSTER_HOST, pool.family) + + if cluster_host_address is None: + address_name = cutils.format_address_name( + hostname, constants.NETWORK_TYPE_CLUSTER_HOST) + resp_addr = self._allocate_pool_address(None, pool.uuid, address_name) + LOG.info(f"{address_name} address not found." + f" Allocating address {resp_addr.address} for {hostname}.") def _allocate_addresses_for_host(self, context, host): """Allocates addresses for a given host. @@ -2054,28 +2100,40 @@ class ConductorManager(service.PeriodicService): mgmt_interface_id = None if mgmt_interfaces: mgmt_interface_id = mgmt_interfaces[0]['id'] - hostname = host.hostname - # check for static mgmt IP - mgmt_ip = self._lookup_static_ip_address( - hostname, constants.NETWORK_TYPE_MGMT - ) - # make sure address in address table and update dnsmasq host file - if mgmt_ip: - LOG.info("Static mgmt ip {} for host{}".format(mgmt_ip, hostname)) - self._create_or_update_address(context, hostname, mgmt_ip, - constants.NETWORK_TYPE_MGMT, - mgmt_interface_id) - # if no static address, then allocate one - if not mgmt_ip: - mgmt_pool = self.dbapi.network_get_by_type( - constants.NETWORK_TYPE_MGMT - ).pool_uuid - address_name = cutils.format_address_name(hostname, - constants.NETWORK_TYPE_MGMT) - mgmt_ip = self._allocate_pool_address(mgmt_interface_id, mgmt_pool, - address_name).address - LOG.info("Allocated mgmt ip {} for host{}".format(mgmt_ip, hostname)) + hostname = host.hostname + mgmt_net = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_MGMT) + net_pools = self.dbapi.network_addrpool_get_by_network_id(mgmt_net.id) + pool_uuid_list = list() + if net_pools: + for net_pool in net_pools: + pool_uuid_list.append(net_pool.address_pool_uuid) + else: + # we are coming from an upgrade without data-migration implemented for the + # dual stack feature + LOG.warning(f"Network {mgmt_net.name} does not have network to address pool objects") + pool_uuid_list.append(mgmt_net.pool_uuid) + + for pool_uuid in pool_uuid_list: + pool = self.dbapi.address_pool_get(pool_uuid) + + # check for static mgmt IP + mgmt_ip = self._lookup_static_ip_address_family( + hostname, constants.NETWORK_TYPE_MGMT, pool.family) + + # make sure address in address table and update dnsmasq host file + if mgmt_ip: + LOG.info("Static mgmt ip {} for host{}".format(mgmt_ip, hostname)) + self._create_or_update_address(context, hostname, mgmt_ip, + constants.NETWORK_TYPE_MGMT, + mgmt_interface_id, pool_uuid) + # if no static address, then allocate one + if not mgmt_ip: + address_name = cutils.format_address_name(hostname, + constants.NETWORK_TYPE_MGMT) + mgmt_ip = self._allocate_pool_address(mgmt_interface_id, pool_uuid, + address_name).address + LOG.info(f"Allocated mgmt ip {mgmt_ip} for host={hostname}") self._generate_dnsmasq_hosts_file(existing_host=host) self._allocate_cluster_host_address_for_host(host) @@ -2842,6 +2900,7 @@ class ConductorManager(service.PeriodicService): puppet_common.puppet_apply_manifest(host.hostname, constants.WORKER, do_reboot=True) + return host def unconfigure_ihost(self, context, ihost_obj): @@ -3518,23 +3577,29 @@ class ConductorManager(service.PeriodicService): if set_address_interface: if new_interface and 'id' in new_interface: values = {'interface_id': new_interface['id']} - address = cutils.get_primary_address_by_name(self.dbapi, - cutils.format_address_name(ihost.hostname, new_interface_networktype), - new_interface_networktype) - if address: - self.dbapi.address_update(address['uuid'], values) + try: + addr_name = cutils.format_address_name( + ihost.hostname, new_interface_networktype) + addresses = self.dbapi.address_get_by_name(addr_name) + for address in addresses: + self.dbapi.address_update(address['uuid'], values) + except exception.AddressNotFoundByName: + pass # Do any potential distributed cloud config # We do this here where the interface is created. cutils.perform_distributed_cloud_config(self.dbapi, new_interface['id']) if port: values = {'interface_id': port.interface_id} - address = cutils.get_primary_address_by_name(self.dbapi, - cutils.format_address_name(ihost.hostname, networktype), - networktype) - if address: - if address['interface_id'] is None: - self.dbapi.address_update(address['uuid'], values) + try: + addr_name = cutils.format_address_name(ihost.hostname, + networktype) + addresses = self.dbapi.address_get_by_name(addr_name) + for address in addresses: + if address['interface_id'] is None: + self.dbapi.address_update(address['uuid'], values) + except exception.AddressNotFoundByName: + pass if ihost.invprovision not in [constants.PROVISIONED, constants.PROVISIONING, constants.UPGRADING]: LOG.info("Updating %s host invprovision from %s to %s" % @@ -12265,7 +12330,7 @@ class ConductorManager(service.PeriodicService): if not drbd_fs_updated: rc = True else: - while(loop_timeout <= max_loop): + while (loop_timeout <= max_loop): if constants.DRBD_PGSQL in (drbd_fs_updated - drbd_fs_resized): if (not standby_host or (standby_host and constants.DRBD_PGSQL in self._drbd_fs_sync())): diff --git a/sysinv/sysinv/sysinv/sysinv/db/api.py b/sysinv/sysinv/sysinv/sysinv/db/api.py index e7e7d8c4da..1259ee091c 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/api.py @@ -5154,3 +5154,11 @@ class Connection(object): @abc.abstractmethod def kube_app_bundle_destroy_by_file_path(self, file_path): """Delete records from kube_app_bundle that match a file path""" + + @abc.abstractmethod + def address_get_by_name_and_family(self, name, family): + """ Search database address using name and family + + :param name: address name. + :param family: address family (4 or 6). + """ diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/base.py b/sysinv/sysinv/sysinv/sysinv/puppet/base.py index 812b3692ea..45a467775e 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/base.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/base.py @@ -183,6 +183,14 @@ class BasePuppet(object): return address + def _get_address_by_name_and_family(self, name, family, networktype): + """ + Retrieve an address entry by name and scoped by network type + """ + address_name = utils.format_address_name(name, networktype) + return self.dbapi.address_get_by_name_and_family(address_name, + family) + def _get_management_address(self): address = self._get_address_by_name( constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_MGMT) diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/networking.py b/sysinv/sysinv/sysinv/sysinv/puppet/networking.py index 62b6c18dbd..4c696b1a6c 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/networking.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/networking.py @@ -6,6 +6,8 @@ import netaddr +from oslo_log import log + from sysinv.common import constants from sysinv.common import exception from sysinv.common import utils @@ -13,6 +15,11 @@ from sysinv.common import utils from sysinv.puppet import base from sysinv.puppet import interface +LOG = log.getLogger(__name__) + +IPv4 = constants.IP_FAMILIES[constants.IPV4_FAMILY].lower() +IPv6 = constants.IP_FAMILIES[constants.IPV6_FAMILY].lower() + class NetworkingPuppet(base.BasePuppet): """Class to encapsulate puppet operations for networking configuration""" @@ -49,16 +56,7 @@ class NetworkingPuppet(base.BasePuppet): config = self._get_network_config(networktype) - try: - gateway_address = self._get_address_by_name( - constants.CONTROLLER_GATEWAY, networktype).address - except exception.AddressNotFoundByName: - gateway_address = None - - config.update({ - 'platform::network::%s::params::gateway_address' % networktype: - gateway_address, - }) + config = self._get_network_gateway_config(networktype, config) # create flag for the mate controller to use FQDN or not if utils.is_fqdn_ready_to_use(): @@ -100,16 +98,7 @@ class NetworkingPuppet(base.BasePuppet): config = self._get_network_config(networktype) - try: - gateway_address = self._get_address_by_name( - constants.CONTROLLER_GATEWAY, networktype).address - except exception.AddressNotFoundByName: - gateway_address = None - - config.update({ - 'platform::network::%s::params::gateway_address' % networktype: - gateway_address, - }) + config = self._get_network_gateway_config(networktype, config) return config @@ -132,71 +121,125 @@ class NetworkingPuppet(base.BasePuppet): try: network = self.dbapi.network_get_by_type(networktype) except exception.NetworkTypeNotFound: - # network not configured + LOG.debug(f"Network type {networktype} not found") return {} - address_pool = self.dbapi.address_pool_get(network.pool_uuid) + net_pools = self.dbapi.network_addrpool_get_by_network_id(network.id) + pool_uuid_list = list() + if net_pools: + for net_pool in net_pools: + pool_uuid_list.append(net_pool.address_pool_uuid) + else: + # we are coming from an upgrade without data-migration implemented for the + # dual stack feature + LOG.warning(f"Network {network.name} does not have network to address pool objects") + pool_uuid_list.append(network.pool_uuid) - subnet = netaddr.IPNetwork( - str(address_pool.network) + '/' + str(address_pool.prefix)) + configdata = dict() + config = dict() - subnet_version = address_pool.family - subnet_network = str(subnet.network) - subnet_netmask = str(subnet.netmask) - subnet_prefixlen = subnet.prefixlen + for pool_uuid in pool_uuid_list: - subnet_start = str(address_pool.ranges[0][0]) - subnet_end = str(address_pool.ranges[0][-1]) + address_pool = self.dbapi.address_pool_get(pool_uuid) - try: - controller_address = self._get_address_by_name( - constants.CONTROLLER_HOSTNAME, networktype).address - except exception.AddressNotFoundByName: - controller_address = None + family_name = IPv4 if address_pool.family == constants.IPV4_FAMILY else IPv6 + configdata.update({family_name: {}}) - try: - controller0_address = self._get_address_by_name( - constants.CONTROLLER_0_HOSTNAME, networktype).address - except exception.AddressNotFoundByName: - controller0_address = None + subnet = netaddr.IPNetwork( + str(address_pool.network) + '/' + str(address_pool.prefix)) + configdata[family_name].update({'subnet': subnet}) - try: - controller1_address = self._get_address_by_name( - constants.CONTROLLER_1_HOSTNAME, networktype).address - except exception.AddressNotFoundByName: - controller1_address = None + configdata[family_name].update({'subnet_version': address_pool.family}) + configdata[family_name].update({'subnet_network': str(subnet.network)}) + configdata[family_name].update({'subnet_netmask': str(subnet.netmask)}) + configdata[family_name].update({'subnet_prefixlen': subnet.prefixlen}) + configdata[family_name].update({'subnet_start': str(address_pool.ranges[0][0])}) + configdata[family_name].update({'subnet_end': str(address_pool.ranges[0][-1])}) - controller_address_url = self._format_url_address(controller_address) - subnet_network_url = self._format_url_address(subnet_network) + try: + controller_address = self._get_address_by_name_and_family( + constants.CONTROLLER_HOSTNAME, address_pool.family, networktype).address + except exception.AddressNotFoundByNameAndFamily: + controller_address = None + configdata[family_name].update({'controller_address': controller_address}) + + try: + controller0_address = self._get_address_by_name_and_family( + constants.CONTROLLER_0_HOSTNAME, address_pool.family, networktype).address + except exception.AddressNotFoundByNameAndFamily: + controller0_address = None + configdata[family_name].update({'controller0_address': controller0_address}) + + try: + controller1_address = self._get_address_by_name_and_family( + constants.CONTROLLER_1_HOSTNAME, address_pool.family, networktype).address + except exception.AddressNotFoundByNameAndFamily: + controller1_address = None + configdata[family_name].update({'controller1_address': controller1_address}) + + configdata[family_name].update({'controller_address_url': + self._format_url_address(controller_address)}) + configdata[family_name].update({'subnet_network_url': + self._format_url_address(str(subnet.network))}) # Convert the dash to underscore because puppet parameters cannot have # dashes networktype = networktype.replace('-', '_') - return { - 'platform::network::%s::params::subnet_version' % networktype: - subnet_version, - 'platform::network::%s::params::subnet_network' % networktype: - subnet_network, - 'platform::network::%s::params::subnet_network_url' % networktype: - subnet_network_url, - 'platform::network::%s::params::subnet_prefixlen' % networktype: - subnet_prefixlen, - 'platform::network::%s::params::subnet_netmask' % networktype: - subnet_netmask, - 'platform::network::%s::params::subnet_start' % networktype: - subnet_start, - 'platform::network::%s::params::subnet_end' % networktype: - subnet_end, - 'platform::network::%s::params::controller_address' % networktype: - controller_address, - 'platform::network::%s::params::controller_address_url' % networktype: - controller_address_url, - 'platform::network::%s::params::controller0_address' % networktype: - controller0_address, - 'platform::network::%s::params::controller1_address' % networktype: - controller1_address, - } + for family in configdata: + config[f'platform::network::{networktype}::{family}::params::subnet_version'] = \ + configdata[family]['subnet_version'] + config[f'platform::network::{networktype}::{family}::params::subnet_network'] = \ + configdata[family]['subnet_network'] + config[f'platform::network::{networktype}::{family}::params::subnet_network_url'] = \ + configdata[family]['subnet_network_url'] + config[f'platform::network::{networktype}::{family}::params::subnet_prefixlen'] = \ + configdata[family]['subnet_prefixlen'] + config[f'platform::network::{networktype}::{family}::params::subnet_netmask'] = \ + configdata[family]['subnet_netmask'] + config[f'platform::network::{networktype}::{family}::params::subnet_start'] = \ + configdata[family]['subnet_start'] + config[f'platform::network::{networktype}::{family}::params::subnet_end'] = \ + configdata[family]['subnet_end'] + config[f'platform::network::{networktype}::{family}::params::controller_address'] = \ + configdata[family]['controller_address'] + config[f'platform::network::{networktype}::{family}::params::controller_address_url'] = \ + configdata[family]['controller_address_url'] + config[f'platform::network::{networktype}::{family}::params::controller0_address'] = \ + configdata[family]['controller0_address'] + config[f'platform::network::{networktype}::{family}::params::controller1_address'] = \ + configdata[family]['controller1_address'] + + if network.primary_pool_family \ + and (network.primary_pool_family).lower() in configdata.keys(): + family = network.primary_pool_family.lower() + config[f'platform::network::{networktype}::params::subnet_version'] = \ + configdata[family]['subnet_version'] + config[f'platform::network::{networktype}::params::subnet_network'] = \ + configdata[family]['subnet_network'] + config[f'platform::network::{networktype}::params::subnet_network_url'] = \ + configdata[family]['subnet_network_url'] + config[f'platform::network::{networktype}::params::subnet_prefixlen'] = \ + configdata[family]['subnet_prefixlen'] + config[f'platform::network::{networktype}::params::subnet_netmask'] = \ + configdata[family]['subnet_netmask'] + config[f'platform::network::{networktype}::params::subnet_start'] = \ + configdata[family]['subnet_start'] + config[f'platform::network::{networktype}::params::subnet_end'] = \ + configdata[family]['subnet_end'] + config[f'platform::network::{networktype}::params::controller_address'] = \ + configdata[family]['controller_address'] + config[f'platform::network::{networktype}::params::controller_address_url'] = \ + configdata[family]['controller_address_url'] + config[f'platform::network::{networktype}::params::controller0_address'] = \ + configdata[family]['controller0_address'] + config[f'platform::network::{networktype}::params::controller1_address'] = \ + configdata[family]['controller1_address'] + else: + LOG.error(f"Network {network.name}, type {network.type} does not have a valid" + f" primary pool address family: {network.primary_pool_family}.") + + return config def _get_pxeboot_interface_config(self): return self._get_interface_config(constants.NETWORK_TYPE_PXEBOOT) @@ -219,6 +262,53 @@ class NetworkingPuppet(base.BasePuppet): def _get_admin_interface_config(self): return self._get_interface_config(constants.NETWORK_TYPE_ADMIN) + def _get_network_gateway_config(self, networktype, config): + try: + network = self.dbapi.network_get_by_type(networktype) + except exception.NetworkTypeNotFound: + LOG.debug(f"Network type {networktype} not found") + return {} + + net_pools = self.dbapi.network_addrpool_get_by_network_id(network.id) + pool_uuid_list = list() + if net_pools: + for net_pool in net_pools: + pool_uuid_list.append(net_pool.address_pool_uuid) + else: + # we are coming from an upgrade without data-migration implemented for the + # dual stack feature + LOG.warning(f"Network {network.name} does not have network to address pool objects") + pool_uuid_list.append(network.pool_uuid) + + configdata = dict() + for pool_uuid in pool_uuid_list: + address_pool = self.dbapi.address_pool_get(pool_uuid) + + family = IPv4 if address_pool.family == constants.IPV4_FAMILY else IPv6 + configdata.update({family: {}}) + + try: + gateway_address = self._get_address_by_name_and_family( + constants.CONTROLLER_GATEWAY, address_pool.family, networktype).address + except exception.AddressNotFoundByNameAndFamily: + gateway_address = None + configdata[family].update({'gateway_address': gateway_address}) + + for family in configdata: + config.update({f'platform::network::{networktype}::{family}::params::gateway_address': + configdata[family]['gateway_address']}) + + if network.primary_pool_family \ + and (network.primary_pool_family).lower() in configdata.keys(): + family = network.primary_pool_family.lower() + config.update({f'platform::network::{networktype}::params::gateway_address': + configdata[family]['gateway_address']}) + else: + LOG.error(f"Network {network.name}, type {network.type} does not have a valid" + f" primary pool address family: {network.primary_pool_family}.") + + return config + def _set_ptp_instance_global_parameters(self, ptp_instances, ptp_parameters_instance): default_global_parameters = { @@ -546,10 +636,9 @@ class NetworkingPuppet(base.BasePuppet): self.context, network_interface) interface_devices = interface.get_interface_devices( self.context, network_interface) - network_id = interface.find_network_id_by_networktype( - self.context, networktype) # Convert the dash to underscore because puppet parameters cannot # have dashes + network = self.context['networks'].get(networktype, None) networktype = networktype.replace('-', '_') config.update({ 'platform::network::%s::params::interface_name' % networktype: @@ -560,13 +649,23 @@ class NetworkingPuppet(base.BasePuppet): network_interface.imtu }) - interface_address = interface.get_interface_primary_address( - self.context, network_interface, network_id) - if interface_address: + addresses = self.context['addresses'].get(network_interface['ifname'], []) + for address in addresses: + family = "ipv4" if address.family == constants.IPV4_FAMILY else "ipv6" config.update({ - 'platform::network::%s::params::interface_address' % - networktype: - interface_address['address'] + f'platform::network::{networktype}::{family}::params::interface_address': + address.address }) + if network: + for address in addresses: + family = "ipv4" if address.family == constants.IPV4_FAMILY else "ipv6" + prim_family = network.primary_pool_family.lower() + if prim_family == family: + config.update({ + f'platform::network::{networktype}::params::interface_address': + address.address + }) + break + return config diff --git a/sysinv/sysinv/sysinv/sysinv/tests/conductor/test_manager.py b/sysinv/sysinv/sysinv/sysinv/tests/conductor/test_manager.py index 70a09caaa9..924be36ca5 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/conductor/test_manager.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/conductor/test_manager.py @@ -25,6 +25,7 @@ import copy import mock import os.path +import netaddr import subprocess import tempfile import uuid @@ -5823,6 +5824,34 @@ class ManagerTestCaseInternal(base.BaseHostTestCase): self.service = manager.ConductorManager('test-host', 'test-topic') self.service.dbapi = dbapi.get_instance() + def _create_test_ihost(self, **kwargs): + # ensure the system ID for proper association + kwargs['forisystemid'] = self.system['id'] + ihost_dict = utils.get_test_ihost(**kwargs) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kwargs: + del ihost_dict['id'] + ihost = self.service.dbapi.ihost_create(ihost_dict) + return ihost + + def create_ipv6_pools(self): + mgmt_subnet6 = netaddr.IPNetwork('fd01::/64') + oam_subnet6 = netaddr.IPNetwork('fd00::/64') + cluster_host_subnet6 = netaddr.IPNetwork('fd02::/64') + cluster_pod_subnet6 = netaddr.IPNetwork('fd03::/64') + cluster_service_subnet6 = netaddr.IPNetwork('fd04::/112') + multicast_subnet6 = netaddr.IPNetwork('ff08::1:1:0/124') + storage_subnet6 = netaddr.IPNetwork('fd05::/64') + admin_subnet6 = netaddr.IPNetwork('fd09::/64') + self._create_test_address_pool(name="management-ipv6", subnet=mgmt_subnet6) + self._create_test_address_pool(name="oam-ipv6", subnet=oam_subnet6) + self._create_test_address_pool(name="cluster-host-ipv6", subnet=cluster_host_subnet6) + self._create_test_address_pool(name="cluster-pod-ipv6", subnet=cluster_pod_subnet6) + self._create_test_address_pool(name="cluster-service-ipv6", subnet=cluster_service_subnet6) + self._create_test_address_pool(name="multicast-ipv6", subnet=multicast_subnet6) + self._create_test_address_pool(name="storage-ipv6", subnet=storage_subnet6) + self._create_test_address_pool(name="admin-ipv6", subnet=admin_subnet6) + def test_remove_lease_for_address(self): # create test interface ihost = self._create_test_host( @@ -5868,3 +5897,138 @@ class ManagerTestCaseInternal(base.BaseHostTestCase): self.service._remove_lease_for_address(ihost.hostname, constants.NETWORK_TYPE_MGMT) + + def test_configure_ihost_allocate_addresses_for_host(self): + # Test skipped to prevent error message in Jenkins. Error thrown is: + # in test_configure_ihost_allocate_addresses_for_host + # with open(self.dnsmasq_hosts_file, 'w') as f: + # IOError: [Errno 13] Permission denied: '/tmp/dnsmasq.hosts' + # self.skipTest("Skipping to prevent failure notification on Jenkins") + + self.context = context.get_admin_context() + self.service._generate_dnsmasq_hosts_file = mock.Mock() + self.service._puppet = mock.Mock() + self.service._update_pxe_config = mock.Mock() + + # create a basic ihost object + ihost = self._create_test_ihost() + + self.create_ipv6_pools() + + net_mgmt = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_MGMT) + pool_mgmt6 = self.dbapi.address_pool_query({"name": "management-ipv6"}) + pool_mgmt4 = self.dbapi.address_pool_query({"name": "management"}) + dbutils.create_test_network_addrpool(address_pool_id=pool_mgmt6.id, network_id=net_mgmt.id) + + net_clhost = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_CLUSTER_HOST) + pool_clhost6 = self.dbapi.address_pool_query({"name": "cluster-host-ipv6"}) + pool_clhost4 = self.dbapi.address_pool_query({"name": "cluster-host"}) + dbutils.create_test_network_addrpool(address_pool_id=pool_clhost6.id, network_id=net_clhost.id) + + worker_name = 'newhost' + ihost['mgmt_mac'] = '00:11:22:33:44:55' + ihost['hostname'] = worker_name + ihost['invprovision'] = 'unprovisioned' + ihost['personality'] = 'worker' + ihost['administrative'] = 'locked' + ihost['operational'] = 'disabled' + ihost['availability'] = 'not-installed' + ihost['serialid'] = '1234567890abc' + ihost['boot_device'] = 'sda' + ihost['rootfs_device'] = 'sda' + ihost['hw_settle'] = '0' + ihost['install_output'] = 'text' + ihost['console'] = 'ttyS0,115200' + + self.service.configure_ihost(self.context, ihost) + + addr_mgmt4 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_MGMT}", + constants.IPV4_FAMILY) + self.assertEqual(addr_mgmt4.pool_uuid, pool_mgmt4.uuid) + self.assertEqual(addr_mgmt4.family, pool_mgmt4.family) + + addr_mgmt6 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_MGMT}", + constants.IPV6_FAMILY) + self.assertEqual(addr_mgmt6.pool_uuid, pool_mgmt6.uuid) + self.assertEqual(addr_mgmt6.family, pool_mgmt6.family) + + addr_clhost4 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_CLUSTER_HOST}", + constants.IPV4_FAMILY) + self.assertEqual(addr_clhost4.pool_uuid, pool_clhost4.uuid) + + addr_clhost6 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_CLUSTER_HOST}", + constants.IPV6_FAMILY) + self.assertEqual(addr_clhost6.pool_uuid, pool_clhost6.uuid) + + def test_configure_ihost_allocate_addresses_for_host_no_net_pool_object(self): + # the data-migration for upgrade was not implemented yet for the dual-stack feature + # this test aims to validate this condition + # self.skipTest("Skipping to prevent failure notification on Jenkins") + + self.context = context.get_admin_context() + self.service._generate_dnsmasq_hosts_file = mock.Mock() + self.service._puppet = mock.Mock() + self.service._update_pxe_config = mock.Mock() + + # create a basic ihost object + ihost = self._create_test_ihost() + + self.create_ipv6_pools() + + pool_mgmt4 = self.dbapi.address_pool_query({"name": "management"}) + pool_clhost4 = self.dbapi.address_pool_query({"name": "cluster-host"}) + net_pools = self.dbapi.network_addrpool_get_all() + for net_pool in net_pools: + self.dbapi.network_addrpool_destroy(net_pool.uuid) + + worker_name = 'newhost' + ihost['mgmt_mac'] = '00:11:22:33:44:55' + ihost['hostname'] = worker_name + ihost['invprovision'] = 'unprovisioned' + ihost['personality'] = 'worker' + ihost['administrative'] = 'locked' + ihost['operational'] = 'disabled' + ihost['availability'] = 'not-installed' + ihost['serialid'] = '1234567890abc' + ihost['boot_device'] = 'sda' + ihost['rootfs_device'] = 'sda' + ihost['hw_settle'] = '0' + ihost['install_output'] = 'text' + ihost['console'] = 'ttyS0,115200' + + self.assertRaises(exception.AddressNotFoundByNameAndFamily, + self.dbapi.address_get_by_name_and_family, + f"{worker_name}-{constants.NETWORK_TYPE_MGMT}", + constants.IPV4_FAMILY) + + self.assertRaises(exception.AddressNotFoundByNameAndFamily, + self.dbapi.address_get_by_name_and_family, + f"{worker_name}-{constants.NETWORK_TYPE_CLUSTER_HOST}", + constants.IPV6_FAMILY) + + self.service.configure_ihost(self.context, ihost) + + addr_mgmt4 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_MGMT}", + constants.IPV4_FAMILY) + self.assertEqual(addr_mgmt4.pool_uuid, pool_mgmt4.uuid) + self.assertEqual(addr_mgmt4.family, pool_mgmt4.family) + + self.assertRaises(exception.AddressNotFoundByNameAndFamily, + self.dbapi.address_get_by_name_and_family, + f"{worker_name}-{constants.NETWORK_TYPE_MGMT}", + constants.IPV6_FAMILY) + + addr_clhost4 = self.dbapi.address_get_by_name_and_family( + f"{worker_name}-{constants.NETWORK_TYPE_CLUSTER_HOST}", + constants.IPV4_FAMILY) + self.assertEqual(addr_clhost4.pool_uuid, pool_clhost4.uuid) + + self.assertRaises(exception.AddressNotFoundByNameAndFamily, + self.dbapi.address_get_by_name_and_family, + f"{worker_name}-{constants.NETWORK_TYPE_CLUSTER_HOST}", + constants.IPV6_FAMILY) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/puppet/test_networking.py b/sysinv/sysinv/sysinv/sysinv/tests/puppet/test_networking.py new file mode 100644 index 0000000000..9fca2a956d --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/puppet/test_networking.py @@ -0,0 +1,677 @@ +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import uuid +import os +import yaml + +import netaddr +from sysinv.tests.puppet import base +from sysinv.puppet import puppet +from sysinv.tests.db import base as dbbase +from sysinv.common import constants +from sysinv.tests.db import utils as dbutils +from sysinv.db import api as db_api + + +class NetworkingTestCaseMixin(base.PuppetTestCaseMixin): + """ This PlatformFirewallTestCaseMixin needs to be used with a subclass + of BaseHostTestCase + """ + @puppet.puppet_context + def _update_context(self): + # interface is added as an operator by systemconfig.puppet_plugins + self.context = self.operator.interface._create_interface_context(self.host) # pylint: disable=no-member + + # Update the puppet context with generated interface context + self.operator.context.update(self.context) + + def _setup_context(self): + self.ports = [] + self.interfaces = [] + self.addresses = [] + self.routes = [] + self._setup_configuration() + self._update_context() + + def _setup_configuration(self): + pass + + def _create_hieradata_directory(self): + hiera_path = os.path.join(os.environ['VIRTUAL_ENV'], 'hieradata') + if not os.path.exists(hiera_path): + os.mkdir(hiera_path, 0o755) + return hiera_path + + def _get_config_filename(self, hiera_directory): + class_name = self.__class__.__name__ + return os.path.join(hiera_directory, class_name) + ".yaml" + + def _find_network_by_type(self, networktype): + for network in self.networks: + if network['type'] == networktype: + return network + + def _get_network_ids_by_type(self, networktype): + if isinstance(networktype, list): + networktypelist = networktype + elif networktype: + networktypelist = [networktype] + else: + networktypelist = [] + networks = [] + for network_type in networktypelist: + network = self._find_network_by_type(network_type) + networks.append(str(network['id'])) + return networks + + def _create_ethernet_test(self, ifname=None, ifclass=None, + networktype=None, host_id=None, **kwargs): + if not host_id: + host_id = self.host.id + interface_id = len(self.interfaces) + if not ifname: + ifname = (networktype or 'eth') + str(interface_id) + if not ifclass: + ifclass = constants.INTERFACE_CLASS_NONE + if ifclass == constants.INTERFACE_CLASS_PLATFORM: + networks = self._get_network_ids_by_type(networktype) + else: + networks = [] + interface = {'id': interface_id, + 'uuid': str(uuid.uuid4()), + 'forihostid': host_id, + 'ifname': ifname, + 'iftype': constants.INTERFACE_TYPE_ETHERNET, + 'imac': '02:11:22:33:44:' + str(10 + interface_id), + 'uses': [], + 'used_by': [], + 'ifclass': ifclass, + 'networks': networks, + 'networktype': networktype, + 'imtu': 1500, + 'sriov_numvfs': kwargs.get('sriov_numvfs', 0), + 'sriov_vf_driver': kwargs.get('iface_sriov_vf_driver', None)} + db_interface = dbutils.create_test_interface(**interface) + for network in networks: + dbutils.create_test_interface_network_assign(db_interface['id'], network) + self.interfaces.append(db_interface) + + port_id = len(self.ports) + port = {'id': port_id, + 'uuid': str(uuid.uuid4()), + 'name': 'eth' + str(port_id), + 'interface_id': interface_id, + 'host_id': host_id, + 'mac': interface['imac'], + 'driver': kwargs.get('driver', 'ixgbe'), + 'dpdksupport': kwargs.get('dpdksupport', True), + 'pdevice': kwargs.get('pdevice', + "Ethernet Controller X710 for 10GbE SFP+ [1572]"), + 'pciaddr': kwargs.get('pciaddr', + '0000:00:00.' + str(port_id + 1)), + 'dev_id': kwargs.get('dev_id', 0), + 'sriov_vf_driver': kwargs.get('port_sriov_vf_driver', None), + 'sriov_vf_pdevice_id': kwargs.get('sriov_vf_pdevice_id', None), + 'sriov_vfs_pci_address': kwargs.get('sriov_vfs_pci_address', '')} + db_port = dbutils.create_test_ethernet_port(**port) + self.ports.append(db_port) + return db_port, db_interface + + +class NetworkingTestTestCaseControllerDualStackIPv4Primary(NetworkingTestCaseMixin, + dbbase.BaseHostTestCase): + + def __init__(self, *args, **kwargs): + super(NetworkingTestTestCaseControllerDualStackIPv4Primary, self).__init__(*args, **kwargs) + self.test_interfaces = dict() + + def setUp(self): + super(NetworkingTestTestCaseControllerDualStackIPv4Primary, self).setUp() + self.dbapi = db_api.get_instance() + self._setup_context() + + def _update_context(self): + # ensure DB entries are updated prior to updating the context which + # will re-read the entries from the DB. + + self.host.save(self.admin_context) + super(NetworkingTestTestCaseControllerDualStackIPv4Primary, self)._update_context() + + def _setup_configuration(self): + # Create a single port/interface for basic function testing + print("=== _setup_configuration") + self.host = self._create_test_host(personality=constants.CONTROLLER) + + _, c0_oam = self._create_ethernet_test("oam0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_OAM, self.host.id) + + _, c0_mgmt = self._create_ethernet_test("mgmt0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_MGMT, self.host.id) + + _, c0_clhost = self._create_ethernet_test("cluster0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_CLUSTER_HOST, self.host.id) + + _, c0_pxe = self._create_ethernet_test("pxe0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_PXEBOOT, self.host.id) + + self.host_c1 = self._create_test_host(personality=constants.CONTROLLER, + unit=1) + + port, c1_oam = self._create_ethernet_test("oam0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_OAM, self.host_c1.id) + + port, c1_mgmt = self._create_ethernet_test("mgmt0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_MGMT, self.host_c1.id) + + port, c1_clhost = self._create_ethernet_test("cluster0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_CLUSTER_HOST, self.host_c1.id) + + port, c1_pxe = self._create_ethernet_test("pxe0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_PXEBOOT, self.host_c1.id) + + self.create_ipv6_pools() + + # associate addresses with its interfaces + addresses = self.dbapi.addresses_get_all() + for addr in addresses: + for hostname in [self.host.hostname, self.host_c1.hostname]: + if addr.name == f"{hostname}-{constants.NETWORK_TYPE_OAM}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_oam.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_oam.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_MGMT}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_mgmt.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_mgmt.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_CLUSTER_HOST}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_clhost.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_clhost.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_PXEBOOT}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_pxe.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_pxe.id} + self.dbapi.address_update(addr.uuid, values) + + # associate addresses with its pools + for net_type in [constants.NETWORK_TYPE_OAM, + constants.NETWORK_TYPE_MGMT, + constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT]: + net = self.dbapi.network_get_by_type(net_type) + net_pools = self.dbapi.network_addrpool_get_by_network_id(net.id) + for net_pool in net_pools: + address_pool = self.dbapi.address_pool_get(net_pool.address_pool_uuid) + addresses = self.dbapi.addresses_get_all() + for addr in addresses: + if (addr.name.endswith(f"-{net_type}")) \ + and (addr.family == address_pool.family): + values = {'address_pool_id': address_pool.id} + self.dbapi.address_update(addr.uuid, values) + + def create_ipv6_pools(self): + to_add = [ + (constants.NETWORK_TYPE_MGMT, (netaddr.IPNetwork('fd01::/64'), + 'management-ipv6')), + (constants.NETWORK_TYPE_OAM, (netaddr.IPNetwork('fd00::/64'), + 'oam-ipv6')), + (constants.NETWORK_TYPE_ADMIN, (netaddr.IPNetwork('fd09::/64'), + 'admin-ipv6')), + (constants.NETWORK_TYPE_CLUSTER_HOST, (netaddr.IPNetwork('fd03::/64'), + 'cluster-host-ipv6')), + (constants.NETWORK_TYPE_CLUSTER_POD, (netaddr.IPNetwork('fd03::/64'), + 'cluster-pod-ipv6')), + (constants.NETWORK_TYPE_CLUSTER_SERVICE, (netaddr.IPNetwork('fd04::/112'), + 'cluster-service-ipv6')), + (constants.NETWORK_TYPE_STORAGE, (netaddr.IPNetwork('fd05::/64'), + 'storage-ipv6')) + ] + + hosts = [constants.CONTROLLER_HOSTNAME, + constants.CONTROLLER_0_HOSTNAME, + constants.CONTROLLER_1_HOSTNAME] + + for cfgdata in to_add: + net = self.dbapi.network_get_by_type(cfgdata[0]) + pool = self._create_test_address_pool(name=cfgdata[1][1], + subnet=cfgdata[1][0]) + network_addrpool = dbutils.create_test_network_addrpool(address_pool_id=pool.id, + network_id=net.id) + self._create_test_addresses(hostnames=hosts, subnet=cfgdata[1][0], + network_type=cfgdata[0], start=2) + if cfgdata[0] in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_OAM]: + self._create_test_addresses(hostnames=[constants.CONTROLLER_GATEWAY], + subnet=cfgdata[1][0], + network_type=cfgdata[0], start=1, stop=2) + self.network_addrpools.append(network_addrpool) + + def test_generate_networking_host_config(self): + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_host_config(self.host) # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv4', 'ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["interface_address"]: + test_key = f'platform::network::{type}::{family}::params::interface_address' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + for field in ["interface_address", "interface_devices", "interface_name", "mtu"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # ipv4 is the primary, chack the addresses match + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + self.assertEqual(hiera_data[f'platform::network::{type}::params::interface_address'], + hiera_data[f'platform::network::{type}::ipv4::params::interface_address']) + + def test_generate_networking_system_config(self): + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_system_config() # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv4', 'ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::{family}::params::{field}' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + # check the primary pool (no family indication) presence + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # check if the the primary pool subnet_version is with the correct value + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertEqual(constants.IPV4_FAMILY, hiera_data[test_key]) + + +class NetworkingTestTestCaseControllerDualStackIPv6Primary(NetworkingTestCaseMixin, + dbbase.BaseIPv6Mixin, + dbbase.BaseHostTestCase): + + def __init__(self, *args, **kwargs): + super(NetworkingTestTestCaseControllerDualStackIPv6Primary, self).__init__(*args, **kwargs) + self.test_interfaces = dict() + + def setUp(self): + super(NetworkingTestTestCaseControllerDualStackIPv6Primary, self).setUp() + self.dbapi = db_api.get_instance() + self._setup_context() + + def _update_context(self): + # ensure DB entries are updated prior to updating the context which + # will re-read the entries from the DB. + + self.host.save(self.admin_context) + super(NetworkingTestTestCaseControllerDualStackIPv6Primary, self)._update_context() + + def create_ipv4_pools(self): + + to_add = [ + (constants.NETWORK_TYPE_MGMT, (netaddr.IPNetwork('192.168.204.0/24'), + 'management-ipv4')), + (constants.NETWORK_TYPE_OAM, (netaddr.IPNetwork('10.10.10.0/24'), + 'oam-ipv4')), + (constants.NETWORK_TYPE_ADMIN, (netaddr.IPNetwork('10.10.30.0/24'), + 'admin-ipv4')), + (constants.NETWORK_TYPE_CLUSTER_HOST, (netaddr.IPNetwork('192.168.206.0/24'), + 'cluster-host-ipv4')), + (constants.NETWORK_TYPE_CLUSTER_POD, (netaddr.IPNetwork('172.16.0.0/16'), + 'cluster-pod-ipv4')), + (constants.NETWORK_TYPE_CLUSTER_SERVICE, (netaddr.IPNetwork('10.96.0.0/12'), + 'cluster-service-ipv4')), + (constants.NETWORK_TYPE_STORAGE, (netaddr.IPNetwork('10.10.20.0/24'), + 'storage-ipv4')) + ] + + hosts = [constants.CONTROLLER_HOSTNAME, + constants.CONTROLLER_0_HOSTNAME, + constants.CONTROLLER_1_HOSTNAME] + + for cfgdata in to_add: + net = self.dbapi.network_get_by_type(cfgdata[0]) + pool = self._create_test_address_pool(name=cfgdata[1][1], + subnet=cfgdata[1][0]) + network_addrpool = dbutils.create_test_network_addrpool(address_pool_id=pool.id, + network_id=net.id) + self._create_test_addresses(hostnames=hosts, subnet=cfgdata[1][0], + network_type=cfgdata[0], start=2) + if cfgdata[0] in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_OAM]: + self._create_test_addresses(hostnames=[constants.CONTROLLER_GATEWAY], + subnet=cfgdata[1][0], + network_type=cfgdata[0], start=1, stop=2) + self.network_addrpools.append(network_addrpool) + + def _setup_configuration(self): + self.host = self._create_test_host(personality=constants.CONTROLLER) + + _, c0_oam = self._create_ethernet_test("oam0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_OAM, self.host.id) + + _, c0_mgmt = self._create_ethernet_test("mgmt0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_MGMT, self.host.id) + + _, c0_clhost = self._create_ethernet_test("cluster0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_CLUSTER_HOST, self.host.id) + + _, c0_pxe = self._create_ethernet_test("pxe0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_PXEBOOT, self.host.id) + + self.host_c1 = self._create_test_host(personality=constants.CONTROLLER, + unit=1) + + _, c1_oam = self._create_ethernet_test("oam0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_OAM, self.host_c1.id) + + _, c1_mgmt = self._create_ethernet_test("mgmt0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_MGMT, self.host_c1.id) + + _, c1_clhost = self._create_ethernet_test("cluster0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_CLUSTER_HOST, self.host_c1.id) + + _, c1_pxe = self._create_ethernet_test("pxe0", + constants.INTERFACE_CLASS_PLATFORM, + constants.NETWORK_TYPE_PXEBOOT, self.host_c1.id) + + self.create_ipv4_pools() + + # associate addresses with its interfaces + addresses = self.dbapi.addresses_get_all() + for addr in addresses: + for hostname in [self.host.hostname, self.host_c1.hostname]: + if addr.name == f"{hostname}-{constants.NETWORK_TYPE_OAM}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_oam.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_oam.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_MGMT}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_mgmt.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_mgmt.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_CLUSTER_HOST}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_clhost.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_clhost.id} + self.dbapi.address_update(addr.uuid, values) + elif addr.name == f"{hostname}-{constants.NETWORK_TYPE_PXEBOOT}": + if hostname == constants.CONTROLLER_0_HOSTNAME: + values = {'interface_id': c0_pxe.id} + self.dbapi.address_update(addr.uuid, values) + elif hostname == constants.CONTROLLER_1_HOSTNAME: + values = {'interface_id': c1_pxe.id} + self.dbapi.address_update(addr.uuid, values) + + # associate addresses with its pools + for net_type in [constants.NETWORK_TYPE_OAM, + constants.NETWORK_TYPE_MGMT, + constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT]: + net = self.dbapi.network_get_by_type(net_type) + net_pools = self.dbapi.network_addrpool_get_by_network_id(net.id) + for net_pool in net_pools: + address_pool = self.dbapi.address_pool_get(net_pool.address_pool_uuid) + addresses = self.dbapi.addresses_get_all() + for addr in addresses: + if (addr.name.endswith(f"-{net_type}")) \ + and (addr.family == address_pool.family): + values = {'address_pool_id': address_pool.id} + self.dbapi.address_update(addr.uuid, values) + + def test_generate_networking_system_config(self): + + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_system_config() # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv4', 'ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::{family}::params::{field}' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + # check the primary pool (no family indication) presence + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # check if the the primary pool subnet_version is with the correct value + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertEqual(constants.IPV6_FAMILY, hiera_data[test_key]) + + def test_generate_networking_system_config_no_net_pool_object(self): + """This test aims to validate if a system can operate without network-addrpool + objects since this can happen if an upgrade is executed and the data-migration + for the diual-stack feature is not implemented yet; + """ + + net_pools = self.dbapi.network_addrpool_get_all() + for net_pool in net_pools: + self.dbapi.network_addrpool_destroy(net_pool.uuid) + + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_system_config() # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::{family}::params::{field}' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + # check the primary pool (no family indication) presence + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["controller0_address", "controller1_address", "controller_address", + "controller_address_url", "subnet_end", "subnet_netmask", "subnet_network", + "subnet_network_url", "subnet_prefixlen", "subnet_start", "subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # check if the the primary pool subnet_version is with the correct value + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_ADMIN, + constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, + constants.NETWORK_TYPE_STORAGE, constants.NETWORK_TYPE_OAM]: + for field in ["subnet_version"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertEqual(constants.IPV6_FAMILY, hiera_data[test_key]) + + def test_generate_networking_host_config(self): + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_host_config(self.host) # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv4', 'ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["interface_address"]: + test_key = f'platform::network::{type}::{family}::params::interface_address' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + for field in ["interface_address", "interface_devices", "interface_name", "mtu"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # ipv6 is the primary, check the addresses match + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + self.assertEqual(hiera_data[f'platform::network::{type}::params::interface_address'], + hiera_data[f'platform::network::{type}::ipv6::params::interface_address']) + + def test_generate_networking_host_config_no_net_pool_objects(self): + """This test aims to validate if a system can operate without network-addrpool + objects since this can happen if an upgrade is executed and the data-migration + for the diual-stack feature is not implemented yet; + """ + + net_pools = self.dbapi.network_addrpool_get_all() + for net_pool in net_pools: + self.dbapi.network_addrpool_destroy(net_pool.uuid) + + hieradata_directory = self._create_hieradata_directory() + config_filename = self._get_config_filename(hieradata_directory) + with open(config_filename, 'w') as config_file: + config = self.operator.networking.get_host_config(self.host) # pylint: disable=no-member + yaml.dump(config, config_file, default_flow_style=False) + print(config_filename) + + hiera_data = dict() + with open(config_filename, 'r') as config_file: + hiera_data = yaml.safe_load(config_file) + + for family in ['ipv4', 'ipv6']: + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + type = net_type.replace('-', '_') + for field in ["interface_address"]: + test_key = f'platform::network::{type}::{family}::params::interface_address' + if net_type == constants.NETWORK_TYPE_PXEBOOT and family == 'ipv6': + # there are no ipv6 allocations for pxe + self.assertNotIn(test_key, hiera_data.keys()) + else: + self.assertIn(test_key, hiera_data.keys()) + + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + for field in ["interface_address", "interface_devices", "interface_name", "mtu"]: + test_key = f'platform::network::{type}::params::{field}' + self.assertIn(test_key, hiera_data.keys()) + + # ipv6 is the primary, check the addresses match + for net_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_CLUSTER_HOST, + constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_OAM]: + self.assertEqual(hiera_data[f'platform::network::{type}::params::interface_address'], + hiera_data[f'platform::network::{type}::ipv6::params::interface_address'])