From 8503921d559c3455d842f44de062a8353ee17aee Mon Sep 17 00:00:00 2001 From: Fabiano Correa Mercer Date: Wed, 26 Jul 2023 12:16:04 -0300 Subject: [PATCH] MGMT address_pool reconfiguration for AIO-SX This change allows the reconfiguration of the management address_pool for an AIO-SX installation. The reconfiguration can be done even when the system was already configured and unlocked, but it needs to lock the controller in order to reconfigure the management network. Since there are ansible rules using the name: "management" as the address_pool, it is necessary to enforce the use of the address_pool named "management" in order to create the mgmt network. During a management network reconfiguration the DNSMASQ changes must not be applied in runtime, to all changes take effect the host lock/unlock is mandatory. Test plan PASS: AIO-SX IPv4 delete and create another management address-pool and create the management network with it PASS: AIO-SX IPv6 delete and create another management address-pool and create the management network with it PASS: AIO-SX IPv4 fresh install PASS: AIO-SX IPv6 fresh install PASS: AIO-DX IPv4 fresh install PASS: AIO-DX IPv6 fresh install PASS: STANDARD IPv4 fresh install PASS: DC with AIO-SX IPv4 fresh install Story: 2010722 Task: 48469 Change-Id: I2de156162dc83d2c16437d2d8068054de19a3b20 Signed-off-by: Fabiano Correa Mercer Signed-off-by: Teresa Ho --- .../scripts/controller_config | 49 ++++++- sysinv/sysinv/sysinv/sysinv/agent/manager.py | 5 + .../sysinv/api/controllers/v1/address_pool.py | 66 ++++++++- .../sysinv/sysinv/api/controllers/v1/host.py | 13 +- .../api/controllers/v1/interface_network.py | 9 ++ .../sysinv/api/controllers/v1/network.py | 73 +++++++--- .../sysinv/sysinv/sysinv/conductor/manager.py | 77 +++++++++-- .../sysinv/sysinv/sysinv/conductor/rpcapi.py | 7 + sysinv/sysinv/sysinv/sysinv/puppet/puppet.py | 12 ++ .../sysinv/sysinv/tests/api/test_network.py | 127 ++++++++++++++++++ sysinv/sysinv/sysinv/sysinv/tests/db/base.py | 4 +- tsconfig/tsconfig/tsconfig/tsconfig.py | 10 ++ 12 files changed, 414 insertions(+), 38 deletions(-) diff --git a/controllerconfig/controllerconfig/scripts/controller_config b/controllerconfig/controllerconfig/scripts/controller_config index 3e9fb964ea..7f0c8fdf84 100755 --- a/controllerconfig/controllerconfig/scripts/controller_config +++ b/controllerconfig/controllerconfig/scripts/controller_config @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright (c) 2013-2022 Wind River Systems, Inc. +# Copyright (c) 2013-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -22,6 +22,7 @@ . /etc/platform/platform.conf PLATFORM_DIR=/opt/platform +ETC_PLATFORM_DIR=/etc/platform VAULT_DIR=$PLATFORM_DIR/.keyring/${SW_VERSION}/python_keyring CONFIG_DIR=$CONFIG_PATH VOLATILE_CONFIG_PASS="/var/run/.config_pass" @@ -98,9 +99,31 @@ EOF get_ip() { local host=$1 + local ipaddr="" + + # the host IP will be in the DNSMASQ files in /etc/platform/ + if [ "$system_mode" = "simplex" ] && [ -e $COMPLETED ]; then + + local host_local="${host}.internal" + local dnsmasq_file=dnsmasq.addn_hosts + + # Replace the dnsmasq files with new Management Network range + if [ -e $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_unlock ] && \ + [ -e $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_ongoing ]; then + dnsmasq_file=dnsmasq.addn_hosts.temp + fi + + ipaddr=$(cat $ETC_PLATFORM_DIR/${dnsmasq_file} | awk -v host=$host_local '$2 == host {print $1}') + + if [ -n "$ipaddr" ] + then + echo $ipaddr + return + fi + fi # Check /etc/hosts for the hostname - local ipaddr=$(cat /etc/hosts | awk -v host=$host '$2 == host {print $1}') + ipaddr=$(cat /etc/hosts | awk -v host=$host '$2 == host {print $1}') if [ -n "$ipaddr" ] then echo $ipaddr @@ -249,7 +272,7 @@ start() fatal_error "Initial manifest application failed; Host must be re-installed." fi - echo "Configuring controller node..." + echo "Configuring controller node... ( IP: ${IPADDR} )" # Remove the flag if it exists rm -f ${ACTIVE_CONTROLLER_NOT_FOUND_FLAG} @@ -514,6 +537,26 @@ start() fi fi + # Replace the dnsmasq files with new Management Network range + if [ -e $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_unlock ] && \ + [ -e $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_ongoing ]; then + echo "Management networking reconfiguration ongoing, replacing dnsmasq config files." + if [ -e $CONFIG_DIR/dnsmasq.addn_hosts.temp ] && \ + [ -e $CONFIG_DIR/dnsmasq.hosts.temp ]; then + mv -f $CONFIG_DIR/dnsmasq.hosts.temp $CONFIG_DIR/dnsmasq.hosts + mv -f $CONFIG_DIR/dnsmasq.addn_hosts.temp $CONFIG_DIR/dnsmasq.addn_hosts + + # update the cached files too + mv -f $ETC_PLATFORM_DIR/dnsmasq.hosts.temp $ETC_PLATFORM_DIR/dnsmasq.hosts + mv -f $ETC_PLATFORM_DIR/dnsmasq.addn_hosts.temp $ETC_PLATFORM_DIR/dnsmasq.addn_hosts + else + fatal_error "Management networking reconfiguration ongoing and dnsmasq files do not exist." + fi + # delete flags + rm -f $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_ongoing + rm -f $ETC_PLATFORM_DIR/.mgmt_network_reconfiguration_unlock + fi + hostname > /etc/hostname if [ $? -ne 0 ] then diff --git a/sysinv/sysinv/sysinv/sysinv/agent/manager.py b/sysinv/sysinv/sysinv/sysinv/agent/manager.py index f551c57c07..f2f9fff13c 100644 --- a/sysinv/sysinv/sysinv/sysinv/agent/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/agent/manager.py @@ -2062,6 +2062,11 @@ class AgentManager(service.PeriodicService): # Set ready flag for maintenance to proceed with the unlock of # the initial controller. utils.touch(constants.UNLOCK_READY_FLAG) + elif (os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING) and + applied_classes == ['openstack::keystone::endpoint::reconfig']): + # Set ready flag for maintenance to proceed with the unlock + # after mgmt ip reconfiguration + utils.touch(constants.UNLOCK_READY_FLAG) except Exception: LOG.exception("failed to apply runtime manifest") raise diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/address_pool.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/address_pool.py index 1ad18ba2c5..2d69ff56fe 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/address_pool.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/address_pool.py @@ -63,6 +63,11 @@ SUBCLOUD_WRITABLE_ADDRPOOLS = ['system-controller-subnet', # so we can't depend on the address pool having a static name. SUBCLOUD_WRITABLE_NETWORK_TYPES = ['admin'] +# Address pool for the management network in an AIO-SX installation +# is allowed to be deleted/modified post install +MANAGEMENT_ADDRESS_POOL = 'management' +AIOSX_WRITABLE_ADDRPOOLS = [MANAGEMENT_ADDRESS_POOL] + class AddressPoolPatchType(types.JsonPatchType): """A complex type that represents a single json-patch operation.""" @@ -346,15 +351,55 @@ class AddressPoolController(rest.RestController): addr = netaddr.IPAddress(address) utils.is_valid_address_within_subnet(addr, subnet) + def _is_aiosx_writable_pool(self, addrpool, check_host_locked): + if (utils.get_system_mode() == constants.SYSTEM_MODE_SIMPLEX and + addrpool.name in AIOSX_WRITABLE_ADDRPOOLS): + + # The mgmt address pool is just writable when the controller is locked + if(check_host_locked): + chosts = pecan.request.dbapi.ihost_get_by_personality( + constants.CONTROLLER) + for host in chosts: + if utils.is_aio_simplex_host_unlocked(host): + msg = _("Cannot complete the action because Host {} " + "is in administrative state = unlocked" + .format(host['hostname'])) + raise wsme.exc.ClientSideError(msg) + + return True + return False + + def _validate_aiosx_mgmt_update(self, addrpool, new_name=None): + # There are ansible rules using the explicit name: 'management' in the addrpool + # since the AIO-SX allows mgmt network reconfiguration it is necessary to enforce + # the use of addrpool named 'management'. + + if (utils.get_system_mode() == constants.SYSTEM_MODE_SIMPLEX and + addrpool.name in AIOSX_WRITABLE_ADDRPOOLS): + + networks = pecan.request.dbapi.networks_get_by_pool(addrpool.id) + + if networks and cutils.is_initial_config_complete() and \ + any(network.type == constants.NETWORK_TYPE_MGMT + for network in networks): + + if (new_name != MANAGEMENT_ADDRESS_POOL): + msg = _("Cannot complete the action because the " + "address pool for mgmt network must be named as '{}'." + .format(MANAGEMENT_ADDRESS_POOL)) + raise ValueError(msg) + def _check_pool_readonly(self, addrpool): # The admin and system controller address pools which exist on the # subcloud are expected for re-home a subcloud to new system controllers. - if addrpool.name not in SUBCLOUD_WRITABLE_ADDRPOOLS: + if (addrpool.name not in SUBCLOUD_WRITABLE_ADDRPOOLS and + not self._is_aiosx_writable_pool(addrpool, True)): networks = pecan.request.dbapi.networks_get_by_pool(addrpool.id) # An addresspool except the admin and system controller's pools # are considered read-only after the initial configuration is # complete. During bootstrap it should be modifiable even though # it is allocated to a network. + # The management address pool can be changed just for AIO-SX if networks and cutils.is_initial_config_complete(): if any(network.type in SUBCLOUD_WRITABLE_NETWORK_TYPES for network in networks): @@ -461,6 +506,7 @@ class AddressPoolController(rest.RestController): def _validate_updates(self, addrpool, updates): if 'name' in updates: AddressPool._validate_name(updates['name']) + self._validate_aiosx_mgmt_update(addrpool, updates['name']) if 'order' in updates: AddressPool._validate_allocation_order(updates['order']) if 'ranges' in updates: @@ -608,13 +654,25 @@ class AddressPoolController(rest.RestController): addresses = pecan.request.dbapi.addresses_get_by_pool( addrpool.id) if addresses: + # check if an address of this pool was assigned to an interface + # e.g: address assigned to a data interface + addr_assigned_to_interface = False + for addr in addresses: + if(addr.interface_id): + addr_assigned_to_interface = True + break + # All of the initial configured addresspools are not deleteable, - # except the admin and system controller address pools on the - # subcloud. These can be deleted/re-added during re-homing + # except: + # - The admin and system controller address pools on the subcloud. + # - The management address pool for AIO-SX + # The admin and system controller can be deleted/re-added during re-homing # a subcloud to new system controllers if cutils.is_initial_config_complete() and \ + (networks or addr_assigned_to_interface) and \ (addrpool.name not in SUBCLOUD_WRITABLE_ADDRPOOLS) and \ - not any(network.type == constants.NETWORK_TYPE_ADMIN + not self._is_aiosx_writable_pool(addrpool, True) and \ + not any(network.type == constants.NETWORK_TYPE_ADMIN for network in networks): raise exception.AddressPoolInUseByAddresses() else: diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index 9bd3118198..c28266b362 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -2187,10 +2187,15 @@ class HostController(rest.RestController): ihost_obj['uuid'], {'capabilities': ihost_obj['capabilities']}) # Notify maintenance about updated mgmt_ip - address_name = cutils.format_address_name(ihost_obj.hostname, - constants.NETWORK_TYPE_MGMT) - address = pecan.request.dbapi.address_get_by_name(address_name) - ihost_obj['mgmt_ip'] = address.address + # During mgmt network reconfiguration, do not change the mgmt IP + # in maintencance as it will be updated after the unlock. + if os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING): + ihost_obj['mgmt_ip'] = cutils.gethostbyname(constants.CONTROLLER_0_FQDN) + else: + address_name = cutils.format_address_name(ihost_obj.hostname, + constants.NETWORK_TYPE_MGMT) + address = pecan.request.dbapi.address_get_by_name(address_name) + ihost_obj['mgmt_ip'] = address.address hostupdate.notify_mtce = True diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_network.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_network.py index bb7f8854c8..793c8785a4 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_network.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_network.py @@ -160,6 +160,14 @@ class InterfaceNetworkController(rest.RestController): result = pecan.request.dbapi.interface_network_create(interface_network_dict) + # Management Network reconfiguration after initial config complete + # is just supported by AIO-SX, set the flag + if (network_type == constants.NETWORK_TYPE_MGMT and + utils.get_system_mode() == constants.SYSTEM_MODE_SIMPLEX and + cutils.is_initial_config_complete() and + host.hostname == constants.CONTROLLER_0_HOSTNAME): + pecan.request.rpcapi.set_mgmt_network_reconfig_flag(pecan.request.context) + # Update address mode based on network type if network_type in [constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_OAM, @@ -180,6 +188,7 @@ class InterfaceNetworkController(rest.RestController): # Assign an address to the interface _update_host_address(host, interface_obj, network_type) + if network_type == constants.NETWORK_TYPE_MGMT: ethernet_port_mac = None if not interface_obj.uses: diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/network.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/network.py index eeb54fbb79..d46417b103 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/network.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/network.py @@ -161,7 +161,7 @@ class NetworkController(rest.RestController): pecan.request.context, network_uuid) return Network.convert_with_links(rpc_network) - def _check_network_type(self, networktype): + def _check_network_type(self, networktype, pool_uuid=None): networks = pecan.request.dbapi.networks_get_by_type(networktype) if networks: raise exception.NetworkAlreadyExists(type=networktype) @@ -172,6 +172,16 @@ class NetworkController(rest.RestController): "role of {}." .format(networktype, constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD)) raise wsme.exc.ClientSideError(msg) + if (networktype == constants.NETWORK_TYPE_MGMT): + # There are ansible rules using the explicit name: 'management' in the addrpool + # since the AIO-SX allows mgmt network reconfiguration it is necessary to enforce + # the use of addrpool named 'management'. + if pool_uuid: + pool = pecan.request.dbapi.address_pool_get(pool_uuid) + if pool['name'] != "management": + msg = _("Network of type {} must use the addrpool named '{}'." + .format(networktype, address_pool.MANAGEMENT_ADDRESS_POOL)) + raise wsme.exc.ClientSideError(msg) def _check_network_pool(self, pool): # ensure address pool exists and is not already inuse @@ -203,10 +213,25 @@ class NetworkController(rest.RestController): self._populate_network_addresses(pool, network, addresses) def _create_mgmt_network_address(self, pool): - addresses = collections.OrderedDict() - addresses[constants.CONTROLLER_HOSTNAME] = None - addresses[constants.CONTROLLER_0_HOSTNAME] = None - addresses[constants.CONTROLLER_1_HOSTNAME] = None + addresses = {} + + if pool.floating_address: + addresses.update( + {constants.CONTROLLER_HOSTNAME: pool.floating_address}) + else: + addresses.update({constants.CONTROLLER_HOSTNAME: None}) + + if pool.controller0_address: + addresses.update( + {constants.CONTROLLER_0_HOSTNAME: pool.controller0_address}) + else: + addresses.update({constants.CONTROLLER_0_HOSTNAME: None}) + + if pool.controller1_address: + addresses.update( + {constants.CONTROLLER_1_HOSTNAME: pool.controller1_address}) + else: + addresses.update({constants.CONTROLLER_1_HOSTNAME: None}) if pool.gateway_address is not None: if utils.get_distributed_cloud_role() == \ @@ -362,10 +387,11 @@ class NetworkController(rest.RestController): network = network.as_dict() network['uuid'] = str(uuid.uuid4()) - # Perform semantic validation - self._check_network_type(network['type']) - pool_uuid = network.pop('pool_uuid', None) + + # Perform semantic validation + self._check_network_type(network['type'], pool_uuid) + if pool_uuid: pool = pecan.request.dbapi.address_pool_get(pool_uuid) network.update({'address_pool_id': pool.id}) @@ -431,16 +457,31 @@ class NetworkController(rest.RestController): def delete(self, network_uuid): """Delete a network.""" network = pecan.request.dbapi.network_get(network_uuid) - if cutils.is_initial_config_complete() and \ - network['type'] in [constants.NETWORK_TYPE_MGMT, - constants.NETWORK_TYPE_OAM, + if cutils.is_initial_config_complete(): + if (network['type'] in [constants.NETWORK_TYPE_OAM, constants.NETWORK_TYPE_CLUSTER_HOST, constants.NETWORK_TYPE_PXEBOOT, constants.NETWORK_TYPE_CLUSTER_POD, constants.NETWORK_TYPE_CLUSTER_SERVICE, - constants.NETWORK_TYPE_STORAGE]: - msg = _("Cannot delete type {} network {} after initial " - "configuration completion" - .format(network['type'], network_uuid)) - raise wsme.exc.ClientSideError(msg) + constants.NETWORK_TYPE_STORAGE] or + (network['type'] in [constants.NETWORK_TYPE_MGMT] and + utils.get_system_mode() != constants.SYSTEM_MODE_SIMPLEX)): + msg = _("Cannot delete type {} network {} after initial " + "configuration completion" + .format(network['type'], network_uuid)) + raise wsme.exc.ClientSideError(msg) + + elif (network['type'] in [constants.NETWORK_TYPE_MGMT] and + utils.get_system_mode() == constants.SYSTEM_MODE_SIMPLEX): + + # For AIO-SX the mgmt network can be be reconfigured if host is locked + chosts = pecan.request.dbapi.ihost_get_by_personality( + constants.CONTROLLER) + for host in chosts: + if utils.is_aio_simplex_host_unlocked(host): + msg = _("Cannot delete type {} network {} because Host {} " + "is in administrative state = unlocked" + .format(network['type'], network_uuid, host['hostname'])) + raise wsme.exc.ClientSideError(msg) + pecan.request.dbapi.network_destroy(network_uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 83030986f0..8ca2488803 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -1205,15 +1205,6 @@ class ConductorManager(service.PeriodicService): mac_address) f_out.write(line) - # Update host files atomically and reload dnsmasq - if (not os.path.isfile(dnsmasq_hosts_file) or - not filecmp.cmp(temp_dnsmasq_hosts_file, dnsmasq_hosts_file)): - os.rename(temp_dnsmasq_hosts_file, dnsmasq_hosts_file) - if (not os.path.isfile(dnsmasq_addn_hosts_file) or - not filecmp.cmp(temp_dnsmasq_addn_hosts_file, - dnsmasq_addn_hosts_file)): - os.rename(temp_dnsmasq_addn_hosts_file, dnsmasq_addn_hosts_file) - # If there is no distributed cloud addn_hosts file, create an empty one # so dnsmasq will not complain. dnsmasq_addn_hosts_dc_file = os.path.join(tsc.CONFIG_PATH, 'dnsmasq.addn_hosts_dc') @@ -1224,6 +1215,38 @@ class ConductorManager(service.PeriodicService): f_out_addn_dc.write(' ') os.rename(temp_dnsmasq_addn_hosts_dc_file, dnsmasq_addn_hosts_dc_file) + # The controller IP will be in the dnsmasq.addn_hosts + # since the /opt/platform is not mounted during the startup it is necessary to copy + # DNSMASQ files to /etc/platform/ + if cutils.is_aio_simplex_system(self.dbapi): + ETC_PLAT = tsc.PLATFORM_CONF_PATH + '/' + + if os.path.isfile(dnsmasq_hosts_file): + shutil.copy2(dnsmasq_hosts_file, ETC_PLAT) + if os.path.isfile(dnsmasq_addn_hosts_file): + shutil.copy2(dnsmasq_addn_hosts_file, ETC_PLAT) + if os.path.isfile(temp_dnsmasq_hosts_file): + shutil.copy2(temp_dnsmasq_hosts_file, ETC_PLAT) + if os.path.isfile(temp_dnsmasq_addn_hosts_file): + shutil.copy2(temp_dnsmasq_addn_hosts_file, ETC_PLAT) + + # Ignore the dnsmasq restart when an management network reconfiguration is in process. + # This is necessary, otherwise the DNSMASQ will answer DNS requests with the new MGMT IP + # but the new mgmt IP range was not configured in the system yet. + # The new Management Network IP range will be applied after the host-unlock + if os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING): + LOG.info("Ignoring DNSMASQ changes in runtime due to Management Network reconfiguration.") + return + + # Update host files atomically and reload dnsmasq + if (not os.path.isfile(dnsmasq_hosts_file) or + not filecmp.cmp(temp_dnsmasq_hosts_file, dnsmasq_hosts_file)): + os.rename(temp_dnsmasq_hosts_file, dnsmasq_hosts_file) + if (not os.path.isfile(dnsmasq_addn_hosts_file) or + not filecmp.cmp(temp_dnsmasq_addn_hosts_file, + dnsmasq_addn_hosts_file)): + os.rename(temp_dnsmasq_addn_hosts_file, dnsmasq_addn_hosts_file) + os.system("pkill -HUP dnsmasq") def _generate_dnsmasq_conf_file(self): @@ -2034,6 +2057,33 @@ class ConductorManager(service.PeriodicService): if utils.config_is_reboot_required(host.config_target): config_uuid = self._config_set_reboot_required(config_uuid) self._puppet.update_host_config(host, config_uuid) + elif os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING): + # Remove unlock ready flag to prevent maintenance rebooting the + # node until the runtime manifest is finished. + try: + if os.path.isfile(constants.UNLOCK_READY_FLAG): + os.remove(constants.UNLOCK_READY_FLAG) + except OSError: + LOG.exception("Failed to remove unlock ready flag: %s" % + constants.UNLOCK_READY_FLAG) + + personalities = [constants.CONTROLLER] + # Update sysinv and keystone endpoints before the reboot + config_uuid = self._config_update_hosts(context, personalities, + host_uuids=[host.uuid]) + config_dict = { + "personalities": personalities, + "host_uuids": [host.uuid], + "classes": ['openstack::keystone::endpoint::reconfig'] + } + self._config_apply_runtime_manifest( + context, config_uuid, config_dict, force=True) + + # Regenerate config target uuid, node is going for reboot! + config_uuid = self._config_update_hosts(context, personalities) + if utils.config_is_reboot_required(host.config_target): + config_uuid = self._config_set_reboot_required(config_uuid) + self._puppet.update_host_config(host, config_uuid) def _ceph_mon_create(self, host): if not StorageBackendConfig.has_backend( @@ -12516,6 +12566,15 @@ class ConductorManager(service.PeriodicService): return iinterfaces + def set_mgmt_network_reconfig_flag(self, context): + """set the management network reconfiguration + flag to ignore the DNSMASQ changes in runtime. + """ + + if not os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING): + LOG.info("Management Network reconfiguration detected.") + open(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING, 'w').close() + def mgmt_ip_set_by_ihost(self, context, ihost_uuid, diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py index 72f322db2c..8c45ff9d0f 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py @@ -837,6 +837,13 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy): return self.call(context, self.make_msg( 'remove_admin_firewall_config')) + def set_mgmt_network_reconfig_flag(self, context): + """Synchronously, have the conductor update the mgmt network reconfig flag. + :param context: request context. + """ + return self.call(context, self.make_msg( + 'set_mgmt_network_reconfig_flag')) + def update_host_filesystem_config(self, context, host=None, filesystem_list=None): diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/puppet.py b/sysinv/sysinv/sysinv/sysinv/puppet/puppet.py index 3ec96922b5..9107991b97 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/puppet.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/puppet.py @@ -13,9 +13,11 @@ import io import os import tempfile import yaml +import tsconfig.tsconfig as tsc from stevedore import extension from tsconfig import tsconfig +from sysinv.common import constants from oslo_log import log as logging from sysinv.puppet import common @@ -194,6 +196,16 @@ class PuppetOperator(object): self._write_host_config(host, config) + # Hiera file updated. Check if Management Network reconfiguration is ongoing + if (os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING) and + (host.action == constants.FORCE_UNLOCK_ACTION or + host.action == constants.UNLOCK_ACTION)): + + if not os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_UNLOCK): + LOG.info("Management Network reconfiguration will be applied during " + "the startup. Hiera files updated and host-unlock detected") + open(tsc.MGMT_NETWORK_RECONFIGURATION_UNLOCK, 'w').close() + def read_host_config(self, host, version=None): """""" path = self.get_hieradata_path(version) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_network.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_network.py index fc9efa3252..1d07fb7da4 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/api/test_network.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_network.py @@ -522,6 +522,133 @@ class TestDelete(NetworkTestCase): ) +class TestDeleteAIOSimplex(NetworkTestCase): + """ Tests AIO Simplex deletion. + Typically delete APIs return NO CONTENT. + python2 and python3 libraries may return different + content_type (None, or empty json) when NO_CONTENT returned. + """ + system_type = constants.TIS_AIO_BUILD + system_mode = constants.SYSTEM_MODE_SIMPLEX + + def setUp(self): + super(TestDeleteAIOSimplex, self).setUp() + + def _setup_context(self, host_locked=False): + if host_locked: + admin = constants.ADMIN_LOCKED + else: + admin = constants.ADMIN_UNLOCKED + + self.host = self._create_test_host(constants.CONTROLLER, constants.WORKER, + administrative=admin, + operational=constants.OPERATIONAL_ENABLED, + availability=constants.AVAILABILITY_AVAILABLE, + invprovision=constants.PROVISIONED, + vim_progress_status=constants.VIM_SERVICES_ENABLED) + + self._create_test_host_cpus(self.host, platform=2, vswitch=2, application=11) + + def _test_delete_allowed(self, network_type): + # Delete the API object + self.delete_object = self._create_db_object(network_type=network_type) + uuid = self.delete_object.uuid + response = self.delete(self.get_single_url(uuid), + headers=self.API_HEADERS) + + # Verify the expected API response for the delete + self.assertEqual(response.status_code, http_client.NO_CONTENT) + + def _test_delete_after_initial_config_not_allowed(self, network_type): + # Delete the API object + self.delete_object = self._create_db_object(network_type=network_type) + with mock.patch('sysinv.common.utils.is_initial_config_complete', + lambda: True): + uuid = self.delete_object.uuid + response = self.delete(self.get_single_url(uuid), + headers=self.API_HEADERS, + expect_errors=True) + + # Verify the expected API response for the delete + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + expected_error = ("Cannot delete type %s network %s after" + " initial configuration completion" % + (network_type, uuid)) + self.assertIn(expected_error, response.json['error_message']) + + def _test_delete_mgmt_after_initial_config_not_allowed(self, network_type): + # Delete the API object + self.delete_object = self._create_db_object(network_type=network_type) + with mock.patch('sysinv.common.utils.is_initial_config_complete', + lambda: True): + uuid = self.delete_object.uuid + response = self.delete(self.get_single_url(uuid), + headers=self.API_HEADERS, + expect_errors=True) + + # Verify the expected API response for the delete + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + expected_error = ("Cannot delete type %s network %s because Host " + "controller-0 is in administrative state = unlocked" % + (network_type, uuid)) + self.assertIn(expected_error, response.json['error_message']) + + def _test_delete_after_initial_config_allowed(self, network_type): + # Delete the API object + self.delete_object = self._create_db_object(network_type=network_type) + with mock.patch('sysinv.common.utils.is_initial_config_complete', + lambda: True): + uuid = self.delete_object.uuid + response = self.delete(self.get_single_url(uuid), + headers=self.API_HEADERS) + + # Verify the expected API response for the delete + self.assertEqual(response.status_code, http_client.NO_CONTENT) + + def test_delete_management(self): + + self._test_delete_allowed(constants.NETWORK_TYPE_MGMT) + + def test_delete_management_after_initial_config_not_allowed_host_unlocked(self): + self._setup_context(host_locked=False) + + self._test_delete_mgmt_after_initial_config_not_allowed( + constants.NETWORK_TYPE_MGMT + ) + + def test_delete_management_after_initial_config_allowed_host_locked(self): + self._setup_context(host_locked=True) + + self._test_delete_after_initial_config_allowed( + constants.NETWORK_TYPE_MGMT + ) + + # just to make sure that the other networks can't be deleted + def test_delete_oam(self): + self._setup_context(host_locked=False) + + self._test_delete_allowed(constants.NETWORK_TYPE_OAM) + + def test_delete_oam_after_initial_config(self): + self._setup_context(host_locked=False) + + self._test_delete_after_initial_config_not_allowed( + constants.NETWORK_TYPE_OAM + ) + + def test_delete_data(self): + self._setup_context(host_locked=False) + + self._test_delete_allowed(constants.NETWORK_TYPE_DATA) + + def test_delete_data_after_initial_config(self): + self._setup_context(host_locked=False) + + self._test_delete_after_initial_config_allowed( + constants.NETWORK_TYPE_DATA + ) + + class TestList(NetworkTestCase): """ Network list operations """ diff --git a/sysinv/sysinv/sysinv/sysinv/tests/db/base.py b/sysinv/sysinv/sysinv/sysinv/tests/db/base.py index 5454a9c504..6a041c59c7 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/db/base.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/db/base.py @@ -558,7 +558,7 @@ class StorageHostTestCase(BaseHostTestCase): class AIOHostTestCase(BaseHostTestCase): - system_mode = constants.TIS_AIO_BUILD + system_type = constants.TIS_AIO_BUILD def setUp(self): super(AIOHostTestCase, self).setUp() @@ -568,7 +568,7 @@ class AIOHostTestCase(BaseHostTestCase): class ProvisionedAIOHostTestCase(BaseHostTestCase): - system_mode = constants.TIS_AIO_BUILD + system_type = constants.TIS_AIO_BUILD def setUp(self): super(ProvisionedAIOHostTestCase, self).setUp() diff --git a/tsconfig/tsconfig/tsconfig/tsconfig.py b/tsconfig/tsconfig/tsconfig/tsconfig.py index 0b6a0db6ee..673f092105 100644 --- a/tsconfig/tsconfig/tsconfig/tsconfig.py +++ b/tsconfig/tsconfig/tsconfig/tsconfig.py @@ -208,6 +208,16 @@ INITIAL_K8S_CONFIG_COMPLETE = os.path.join( VOLATILE_CONTROLLER_CONFIG_COMPLETE = os.path.join( VOLATILE_PATH, ".controller_config_complete") +# Set when mgmt network reconfiguration is executed after +# INITIAL_CONTROLLER_CONFIG_COMPLETE +MGMT_NETWORK_RECONFIGURATION_ONGOING = os.path.join( + PLATFORM_CONF_PATH, ".mgmt_network_reconfiguration_ongoing") + +# Set when host-unlock was executed and hieradata was updated +# with new MGMT IP RANGE. +MGMT_NETWORK_RECONFIGURATION_UNLOCK = os.path.join( + PLATFORM_CONF_PATH, ".mgmt_network_reconfiguration_unlock") + # Worker configuration flags # Set after initial application of node manifest