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