From fcebab8ef3b04135f94ee68192c56d878da02ed5 Mon Sep 17 00:00:00 2001 From: Andre Kantek Date: Thu, 29 Feb 2024 15:23:19 -0300 Subject: [PATCH] Introduce Puppet variables for primary and secondary pool addresses. Details: This change extracts the addresses from both the primary and secondary address pools and makes them available for use in Puppet manifests. To accommodate the dual stack configuration, the address allocation for non-controller nodes was updated for both management and cluster-host networks. Since the task for upgrade data-migration is not ready yet, a logic was added to access directly the network's field pool_uuid and get the addresses with it, if the network_addresspools is empty (as it would be the case after an upgrade) As the data migration functionality for the upgrade is still under development, a temporary solution was implemented. Logic was added to directly access the network's "pool_uuid" field and retrieve addresses through it whenever the "network_addresspools" list is empty, which is expected to occur immediately following an upgrade. This allows for uninterrupted network operation during the upgrade process. Variable Naming: The following naming convention will be used for the variables: $platform::network::[network_type]::[ipv4/ipv6]::params::{var_name} Variable Usage: Primary Pool: Existing variables will be maintained and populated with addresses from the primary pool. This ensures compatibility with applications that currently rely on them. They have the format $platform::network::[network_type]::params::{var_name} The variable platform::network::[network_type]::params::subnet_version indicates the primary pool protocol. Secondary Pool: New variables with the above naming convention will be introduced, allowing applications to utilize addresses from the secondary pool if needed. Benefits: Improved modularity and reusability of network configurations. Clear separation of concerns between primary and secondary pools. Easier implementation of applications requiring addresses from either pool. Notes: Replace [network_type] can be oam. mgmt, cluster_host, ... Replace [ipv4/ipv6] with either "ipv4" or "ipv6" depending on the address family. Replace [variable_name] with a descriptive name for the specific variable (e.g., "subnet_version", "interface_address"). Test Plan: [PASS] unit tests implemented [PASS] AIO-SX, Standard instalation (IPv4 and IPv6) - using the dependency change the secondary pool was introduced - system was lock/unlocked and no puppet manifests were detected - inspection of system.yaml and controller-0.yaml to verify variables content - no alarms or disabled services were found - in standard added hosts with dual-stack config and verified that addresses were allocated for mgmt and cluster-host and after unlock the interface id was assigned to the respective entries. [PASS] For standard systems during upgrade, simulate node unlock by: - Clearing the "network_addresspools" table after Ansible execution and before DM configuration. - Installing remaining nodes with the table empty. This mimics the post-upgrade scenario. Story: 2011027 Task: 49679 Depends-On: https://review.opendev.org/c/starlingx/config/+/908915 Change-Id: If252fa051b2ba5b5eb3033ff269683af741091d2 Signed-off-by: Andre Kantek --- .../sysinv/sysinv/sysinv/conductor/manager.py | 195 +++-- sysinv/sysinv/sysinv/sysinv/db/api.py | 8 + sysinv/sysinv/sysinv/sysinv/puppet/base.py | 8 + .../sysinv/sysinv/sysinv/puppet/networking.py | 257 +++++-- .../sysinv/tests/conductor/test_manager.py | 164 +++++ .../sysinv/tests/puppet/test_networking.py | 677 ++++++++++++++++++ 6 files changed, 1165 insertions(+), 144 deletions(-) create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/puppet/test_networking.py 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'])