diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index e023c2c6a..5fcf85d63 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -140,6 +140,7 @@ serviceUnavailable (503) - management_subnet: management_subnet - migrate: migrate - name: subcloud_name + - release: release - sysadmin_password: sysadmin_password - systemcontroller_gateway_address: systemcontroller_gateway_ip - system_mode: system_mode @@ -493,6 +494,7 @@ serviceUnavailable (503) - subcloud: subcloud_uri - bootstrap_values: bootstrap_values - deploy_config: deploy_config + - release: release - sysadmin_password: sysadmin_password Request Example @@ -1601,9 +1603,7 @@ files which include deploy playbook, deploy overrides, deploy helm charts, and p Show Subcloud Deploy Files ************************** -.. rest_method:: GET /v1.0/subcloud-deploy - -This operation does not accept a request body. +.. rest_method:: GET /v1.0/subcloud-deploy/​{release}​ **Normal response codes** @@ -1615,6 +1615,13 @@ badRequest (400), unauthorized (401), forbidden (403), badMethod (405), HTTPUnprocessableEntity (422), internalServerError (500), serviceUnavailable (503) +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - release: release_uri + +This operation does not accept a request body. **Response parameters** @@ -1625,6 +1632,7 @@ internalServerError (500), serviceUnavailable (503) - deploy_playbook: subcloud_deploy_playbook - deploy_overrides: subcloud_deploy_overrides - prestage_images: subcloud_deploy_prestage_images + - software_version: software_version Response Example ---------------- @@ -1659,6 +1667,7 @@ serviceUnavailable (503) - deploy_playbook: subcloud_deploy_playbook_content - deploy_overrides: subcloud_deploy_overrides_content - prestage_images: subcloud_deploy_prestage_images_content + - release: release Request Example ---------------- @@ -1674,6 +1683,7 @@ Request Example - deploy_playbook: subcloud_deploy_playbook - deploy_overrides: subcloud_deploy_overrides - prestage_images: subcloud_deploy_prestage_images + - software_version: software_version Response Example ---------------- diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 0f85214b1..7b79caa9f 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -6,6 +6,12 @@ backup_delete_release: in: path required: true type: string +release_uri: + description: | + The subcloud software version. + in: path + required: false + type: string subcloud_group_uri: description: | The subcloud group reference, name or id. @@ -317,6 +323,12 @@ region_name: in: body required: true type: string +release: + description: | + The subcloud software version. + in: body + required: false + type: string restore_values: description: | The content of a file containing restore parameters (e.g. diff --git a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-get-response.json b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-get-response.json index d9f7ba4ed..38e773d00 100644 --- a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-get-response.json +++ b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-get-response.json @@ -3,6 +3,7 @@ { "deploy_chart": "deployment-manager.tgz", "deploy_playbook": "deployment-manager-playbook.yaml", - "deploy_overrides": "deployment-manager-overrides-subcloud.yaml" + "deploy_overrides": "deployment-manager-overrides-subcloud.yaml", + "software_version": "22.12" } } diff --git a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-request.json b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-request.json index 320800c41..151c9edae 100644 --- a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-request.json +++ b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-request.json @@ -1,5 +1,6 @@ { "deploy_chart": "deployment manager contents", "deploy_playbook": "deployment manager playbook contents", - "deploy_overrides": "deployment manager overrides contents" + "deploy_overrides": "deployment manager overrides contents", + "release": "22.12" } diff --git a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-response.json b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-response.json index 44ec8c980..01f1be103 100644 --- a/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-response.json +++ b/api-ref/source/samples/subcloud-deploy/subcloud-deploy-post-response.json @@ -1,5 +1,6 @@ { "deploy_chart": "deployment-manager.tgz", "deploy_playbook": "deployment-manager-playbook.yaml", - "deploy_overrides": "deployment-manager-overrides-subcloud.yaml" + "deploy_overrides": "deployment-manager-overrides-subcloud.yaml", + "software_version": "22.12" } diff --git a/distributedcloud/dccommon/install_consts.py b/distributedcloud/dccommon/install_consts.py index 6ba60492e..e05bbe634 100644 --- a/distributedcloud/dccommon/install_consts.py +++ b/distributedcloud/dccommon/install_consts.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021 Wind River Systems, Inc. +# Copyright (c) 2020-2023 Wind River Systems, Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -16,8 +16,6 @@ SUPPORTED_INSTALL_TYPES = 6 MANDATORY_INSTALL_VALUES = [ - 'image', - 'software_version', 'bootstrap_interface', 'bootstrap_address', 'bootstrap_address_prefix', diff --git a/distributedcloud/dcmanager/api/controllers/v1/subcloud_deploy.py b/distributedcloud/dcmanager/api/controllers/v1/subcloud_deploy.py index 2e3789f19..2fc96e7fc 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subcloud_deploy.py @@ -105,12 +105,12 @@ class SubcloudDeployController(object): error_msg = "error: argument %s is required" % missing_str.rstrip() pecan.abort(httpclient.BAD_REQUEST, error_msg) - release = tsc.SW_VERSION - if request.POST.get('release_version'): - release = request.POST.get('release_version') - deploy_dicts['release_version'] = release + software_version = tsc.SW_VERSION + if request.POST.get('release'): + software_version = request.POST.get('release') + deploy_dicts['software_version'] = software_version - dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, release) + dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, software_version) for f in consts.DEPLOY_COMMON_FILE_OPTIONS: if f not in request.POST: continue @@ -145,7 +145,7 @@ class SubcloudDeployController(object): deploy_dicts = dict() if not release: release = tsc.SW_VERSION - deploy_dicts['release_version'] = release + deploy_dicts['software_version'] = release dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, release) for f in consts.DEPLOY_COMMON_FILE_OPTIONS: filename = None diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index 6164be547..e32ab23ac 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -96,6 +96,13 @@ INSTALL_VALUES_ADDRESSES = [ 'network_address' ] +ANSIBLE_BOOTSTRAP_VALIDATE_CONFIG_VARS = \ + consts.ANSIBLE_CURRENT_VERSION_BASE_PATH + \ + '/roles/bootstrap/validate-config/vars/main.yml' + +FRESH_INSTALL_K8S_VERSION = 'fresh_install_k8s_version' +KUBERNETES_VERSION = 'kubernetes_version' + def _get_multipart_field_name(part): content = part.headers[b"Content-Disposition"].decode("utf8") @@ -132,14 +139,14 @@ class SubcloudsController(object): pecan.abort(400, _("Invalid group_id")) @staticmethod - def _get_common_deploy_files(payload): + def _get_common_deploy_files(payload, software_version): for f in consts.DEPLOY_COMMON_FILE_OPTIONS: # Skip the prestage_images option as it is not relevant in this # context if f == consts.DEPLOY_PRESTAGE: continue filename = None - dir_path = tsc.DEPLOY_PATH + dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, software_version) if os.path.isdir(dir_path): filename = utils.get_filename_by_prefix(dir_path, f + '_') if filename is None: @@ -159,7 +166,7 @@ class SubcloudsController(object): fn = self._get_config_file_path(payload['name'], consts.DEPLOY_CONFIG) self._upload_config_file(contents, fn, consts.DEPLOY_CONFIG) payload.update({consts.DEPLOY_CONFIG: fn}) - self._get_common_deploy_files(payload) + self._get_common_deploy_files(payload, payload['software_version']) @staticmethod def _get_request_data(request): @@ -242,7 +249,7 @@ class SubcloudsController(object): LOG.exception(msg) pecan.abort(400, msg) - def _get_reconfig_payload(self, request, subcloud_name): + def _get_reconfig_payload(self, request, subcloud_name, software_version): payload = dict() multipart_data = decoder.MultipartDecoder( request.body, pecan.request.headers.get('Content-Type')) @@ -260,7 +267,7 @@ class SubcloudsController(object): payload.update({consts.DEPLOY_CONFIG: fn}) elif "sysadmin_password" in hv: payload.update({'sysadmin_password': part.content}) - self._get_common_deploy_files(payload) + self._get_common_deploy_files(payload, software_version) return payload def _get_config_file_path(self, subcloud_name, config_file_type=None): @@ -615,7 +622,7 @@ class SubcloudsController(object): """Validate install values if 'install_values' is present in payload. The image in payload install values is optional, and if not provided, - the image is set to the available active load image. + the image is set to the available active/inactive load image. :return boolean: True if bmc install requested, otherwise False """ @@ -640,15 +647,18 @@ class SubcloudsController(object): pecan.abort(400, msg) payload['install_values'].update({'bmc_password': bmc_password}) + software_version = payload.get('software_version') + if not software_version and subcloud: + software_version = subcloud.software_version if 'software_version' in install_values: - software_version = str(install_values.get('software_version')) - else: - if original_install_values: - pecan.abort(400, _("Mandatory install value software_version not present, " - "existing software_version in DB: %s") % - original_install_values.get("software_version")) - else: - pecan.abort(400, _("Mandatory install value software_version not present")) + install_software_version = str(install_values.get('software_version')) + if software_version and software_version != install_software_version: + pecan.abort(400, + _("The software_version value %s in the install values " + "yaml file does not match with the specified/current " + "software version of %s. Please correct or remove " + "this parameter from the yaml file and try again.") % + (install_software_version, software_version)) if 'persistent_size' in install_values: persistent_size = install_values.get('persistent_size') if not isinstance(persistent_size, int): @@ -671,27 +681,21 @@ class SubcloudsController(object): for k in install_consts.MANDATORY_INSTALL_VALUES: if k not in install_values: - if k == 'image': - if software_version == tsc.SW_VERSION: - # check for the image at load vault load location - matching_iso, err_msg = utils.get_matching_iso() - if err_msg: - LOG.exception(err_msg) - pecan.abort(400, _(err_msg)) - LOG.info("image was not in install_values: will reference %s" % - matching_iso) - else: - pecan.abort(400, _("Image was not in install_values, and " - "software version %s in install values " - "did not match the active load %s") % - (software_version, tsc.SW_VERSION)) + if original_install_values: + pecan.abort(400, _("Mandatory install value %s not present, " + "existing %s in DB: %s") % + (k, k, original_install_values.get(k))) else: - if original_install_values: - pecan.abort(400, _("Mandatory install value %s not present, " - "existing %s in DB: %s") % - (k, k, original_install_values.get(k))) - else: - pecan.abort(400, _("Mandatory install value %s not present") % k) + pecan.abort(400, + _("Mandatory install value %s not present") % k) + + # check for the image at load vault load location + matching_iso, err_msg = utils.get_matching_iso(software_version) + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) + LOG.info("Image in install_values is set to %s" % matching_iso) + payload['install_values'].update({'image': matching_iso}) if (install_values['install_type'] not in list(range(install_consts.SUPPORTED_INSTALL_TYPES))): @@ -759,6 +763,45 @@ class SubcloudsController(object): return True + @staticmethod + def _validate_k8s_version(payload): + """Validate k8s version. + + If the specified release in the payload is not the active release, + the kubernetes_version value if specified in the subcloud bootstrap + yaml file must be of the same value as fresh_install_k8s_version of + the specified release. + """ + if payload['software_version'] == tsc.SW_VERSION: + return + + kubernetes_version = payload.get(KUBERNETES_VERSION) + if kubernetes_version: + try: + bootstrap_var_file = utils.get_playbook_for_software_version( + ANSIBLE_BOOTSTRAP_VALIDATE_CONFIG_VARS, + payload['software_version']) + fresh_install_k8s_version = utils.get_value_from_yaml_file( + bootstrap_var_file, + FRESH_INSTALL_K8S_VERSION) + if not fresh_install_k8s_version: + pecan.abort(400, _("%s not found in %s") + % (FRESH_INSTALL_K8S_VERSION, + bootstrap_var_file)) + if kubernetes_version != fresh_install_k8s_version: + pecan.abort(400, _("The kubernetes_version value (%s) " + "specified in the subcloud bootstrap " + "yaml file doesn't match " + "fresh_install_k8s_version value (%s) " + "of the specified release %s") + % (kubernetes_version, + fresh_install_k8s_version, + payload['software_version'])) + except exceptions.PlaybookNotFound: + pecan.abort(400, _("The bootstrap playbook validate-config vars " + "not found for %s software version") + % payload['software_version']) + def _get_subcloud_users(self): """Get the subcloud users and passwords from keyring""" DEFAULT_SERVICE_PROJECT_NAME = 'services' @@ -855,15 +898,11 @@ class SubcloudsController(object): resource='subcloud', msg='Subcloud with that name already exists') - # Subcloud is added with software version that matches system - # controller. - software_version = tsc.SW_VERSION # if group_id has been omitted from payload, use 'Default'. group_id = payload.get('group_id', consts.DEFAULT_SUBCLOUD_GROUP_ID) data_install = None if 'install_values' in payload: - software_version = payload['install_values']['software_version'] data_install = json.dumps(payload['install_values']) subcloud = db_api.subcloud_create( @@ -871,7 +910,7 @@ class SubcloudsController(object): payload['name'], payload.get('description'), payload.get('location'), - software_version, + payload.get('software_version'), utils.get_management_subnet(payload), utils.get_management_gateway_address(payload), utils.get_management_start_address(payload), @@ -1140,6 +1179,10 @@ class SubcloudsController(object): group_id = payload.get('group_id', consts.DEFAULT_SUBCLOUD_GROUP_ID) + # If a subcloud release is not passed, use the current + # system controller software_version + payload['software_version'] = payload.get('release', tsc.SW_VERSION) + self._validate_system_controller_patch_status() self._validate_subcloud_config(context, @@ -1160,6 +1203,8 @@ class SubcloudsController(object): self._validate_install_values(payload) + self._validate_k8s_version(payload) + self._format_ip_address(payload) # Upload the deploy config files if it is included in the request @@ -1219,6 +1264,7 @@ class SubcloudsController(object): subcloud_id = subcloud.id if verb is None: + # subcloud update payload = self._get_patch_data(request) if not payload: pecan.abort(400, _('Body required')) @@ -1317,7 +1363,8 @@ class SubcloudsController(object): LOG.exception(e) pecan.abort(500, _('Unable to update subcloud')) elif verb == 'reconfigure': - payload = self._get_reconfig_payload(request, subcloud.name) + payload = self._get_reconfig_payload( + request, subcloud.name, subcloud.software_version) if not payload: pecan.abort(400, _('Body required')) @@ -1454,25 +1501,31 @@ class SubcloudsController(object): external_oam_floating_ip, subcloud_subnets) + # If a subcloud release is not passed, use the current + # system controller software_version + payload['software_version'] = payload.get('release', tsc.SW_VERSION) + + self._validate_k8s_version(payload) + # If the software version of the subcloud is different from the - # central cloud, update the software version in install valuse and - # delete the image path in install values, then the subcloud will - # be reinstalled using the image in dc_vault. - if install_values.get('software_version') != tsc.SW_VERSION: - install_values['software_version'] = tsc.SW_VERSION + # specified or active load, update the software version in install + # value and delete the image path in install values, then the subcloud + # will be reinstalled using the image in dc_vault. + if install_values.get('software_version') != \ + payload['software_version']: + install_values['software_version'] = payload['software_version'] install_values.pop('image', None) - # Confirm the active system controller load is still in dc-vault if + # Confirm the specified or active load is still in dc-vault if # image not in install values, add the matching image into the # install values. - if 'image' not in install_values: - matching_iso, err_msg = utils.get_matching_iso() - if err_msg: - LOG.exception(err_msg) - pecan.abort(400, _(err_msg)) - LOG.info("image was not in install_values: will reference %s" % - matching_iso) - install_values['image'] = matching_iso + matching_iso, err_msg = utils.get_matching_iso( + payload['software_version']) + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) + LOG.info("Image in install_values is set to %s" % matching_iso) + install_values['image'] = matching_iso # Update the install values in payload payload.update({ @@ -1489,15 +1542,15 @@ class SubcloudsController(object): self._upload_deploy_config_file(request, payload) try: - # Align the software version of the subcloud with the central - # cloud. Update description, location and group id if offered, + # Align the software version of the subcloud with reinstall + # version. Update description, location and group id if offered, # update the deploy status as pre-install. - db_api.subcloud_update( + subcloud = db_api.subcloud_update( context, subcloud_id, description=payload.get('description', subcloud.description), location=payload.get('location', subcloud.location), - software_version=tsc.SW_VERSION, + software_version=payload['software_version'], management_state=dccommon_consts.MANAGEMENT_UNMANAGED, deploy_status=consts.DEPLOY_STATE_PRE_INSTALL, data_install=data_install) diff --git a/distributedcloud/dcmanager/common/consts.py b/distributedcloud/dcmanager/common/consts.py index a92dced42..9b613f400 100644 --- a/distributedcloud/dcmanager/common/consts.py +++ b/distributedcloud/dcmanager/common/consts.py @@ -360,3 +360,7 @@ STATES_FOR_ONGOING_BACKUP = [BACKUP_STATE_INITIAL, OPENLDAP_CA_CERT_SECRET_NAME = "system-local-ca" CERT_NAMESPACE_PLATFORM_CA_CERTS = 'cert-manager' + +# The ansible playbook base directories +ANSIBLE_CURRENT_VERSION_BASE_PATH = '/usr/share/ansible/stx-ansible/playbooks' +ANSIBLE_PREVIOUS_VERSION_BASE_PATH = '/opt/dc-vault/playbooks' diff --git a/distributedcloud/dcmanager/common/exceptions.py b/distributedcloud/dcmanager/common/exceptions.py index fa1686f91..880d6706b 100644 --- a/distributedcloud/dcmanager/common/exceptions.py +++ b/distributedcloud/dcmanager/common/exceptions.py @@ -1,6 +1,6 @@ # Copyright 2015 Huawei Technologies Co., Ltd. # Copyright 2015 Ericsson AB. -# Copyright (c) 2017-2022 Wind River Systems, Inc. +# Copyright (c) 2017-2023 Wind River Systems, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -243,3 +243,7 @@ class StrategySkippedException(DCManagerException): class StrategyStoppedException(DCManagerException): message = _("Strategy has been stopped") + + +class PlaybookNotFound(NotFound): + message = _("Playbook %(playbook_name)s not found") diff --git a/distributedcloud/dcmanager/common/utils.py b/distributedcloud/dcmanager/common/utils.py index b3f117b09..c1c2c50ab 100644 --- a/distributedcloud/dcmanager/common/utils.py +++ b/distributedcloud/dcmanager/common/utils.py @@ -26,6 +26,7 @@ import resource as sys_resource import six.moves import subprocess import tsconfig.tsconfig as tsc +import yaml from keystoneauth1 import exceptions as keystone_exceptions from oslo_concurrency import lockutils @@ -749,14 +750,16 @@ def _is_valid_for_backup_restore(subcloud): return True -def get_matching_iso(): +def get_matching_iso(software_version=None): try: - matching_iso, _ = get_vault_load_files(tsc.SW_VERSION) + if not software_version: + software_version = tsc.SW_VERSION + matching_iso, _ = get_vault_load_files(software_version) if not matching_iso: - error_msg = ('Failed to get active load image. Provide ' - 'active load image via ' + error_msg = ('Failed to get %s load image. Provide ' + 'active/inactive load image via ' '"system --os-region-name SystemController ' - 'load-import --active"') + 'load-import --active/--inactive"' % software_version) LOG.exception(error_msg) return None, error_msg return matching_iso, None @@ -900,3 +903,42 @@ def set_open_file_limit(new_soft_limit: int): (new_soft_limit, current_hard)) except Exception as ex: LOG.exception(f'Failed to set NOFILE resource limit: {ex}') + + +def get_playbook_for_software_version(playbook_filename, software_version=None): + """Get the ansible playbook filename in corresponding software version. + + :param playbook_filename: ansible playbook filename + :param software_version: software version + :raises PlaybookNotFound: If the playbook is not found + + Returns the unchanged ansible playbook filename if the software version + parameter is not provided or the same as active release, otherwise, returns + the filename in corresponding software version. + """ + if software_version and software_version != tsc.SW_VERSION: + software_version_path = os.path.join( + consts.ANSIBLE_PREVIOUS_VERSION_BASE_PATH, software_version) + playbook_filename = playbook_filename.replace( + consts.ANSIBLE_CURRENT_VERSION_BASE_PATH, + software_version_path) + if not os.path.isfile(playbook_filename): + raise exceptions.PlaybookNotFound(playbook_name=playbook_filename) + return playbook_filename + + +def get_value_from_yaml_file(filename, key): + """Get corresponding value for a key in the given yaml file. + + :param filename: the yaml filename + :param key: the path for the value + + Returns the value or None if not found. + """ + value = None + if os.path.isfile(filename): + with open(os.path.abspath(filename), 'r') as f: + data = f.read() + data = yaml.load(data, Loader=yaml.SafeLoader) + value = data.get(key) + return value diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index f0c4406a9..0dcf09bb5 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -218,19 +218,28 @@ class SubcloudManager(manager.Manager): subcloud_name + postfix) return ansible_filename - def compose_install_command(self, subcloud_name, ansible_subcloud_inventory_file): + def compose_install_command(self, subcloud_name, + ansible_subcloud_inventory_file, + software_version=None): install_command = [ "ansible-playbook", ANSIBLE_SUBCLOUD_INSTALL_PLAYBOOK, "-i", ansible_subcloud_inventory_file, "--limit", subcloud_name, "-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" + subcloud_name + '/' + "install_values.yml"] + if software_version and software_version != SW_VERSION: + install_command += [ + "-e", "install_release_version=%s" % software_version] return install_command - def compose_apply_command(self, subcloud_name, ansible_subcloud_inventory_file): + def compose_apply_command(self, subcloud_name, + ansible_subcloud_inventory_file, + software_version=None): apply_command = [ - "ansible-playbook", ANSIBLE_SUBCLOUD_PLAYBOOK, "-i", - ansible_subcloud_inventory_file, + "ansible-playbook", + utils.get_playbook_for_software_version( + ANSIBLE_SUBCLOUD_PLAYBOOK, software_version), + "-i", ansible_subcloud_inventory_file, "--limit", subcloud_name ] # Add the overrides dir and region_name so the playbook knows @@ -291,9 +300,13 @@ class SubcloudManager(manager.Manager): subcloud_name + "_update_values.yml"] return subcloud_update_command - def compose_rehome_command(self, subcloud_name, ansible_subcloud_inventory_file): + def compose_rehome_command(self, subcloud_name, + ansible_subcloud_inventory_file, + software_version): rehome_command = [ - "ansible-playbook", ANSIBLE_SUBCLOUD_REHOME_PLAYBOOK, + "ansible-playbook", + utils.get_playbook_for_software_version( + ANSIBLE_SUBCLOUD_REHOME_PLAYBOOK, software_version), "-i", ansible_subcloud_inventory_file, "--limit", subcloud_name, "--timeout", REHOME_PLAYBOOK_TIMEOUT, @@ -426,10 +439,6 @@ class SubcloudManager(manager.Manager): if "install_values" in payload: payload['install_values']['ansible_ssh_pass'] = \ payload['sysadmin_password'] - if 'image' not in payload['install_values']: - matching_iso, matching_sig = utils.get_vault_load_files( - SW_VERSION) - payload['install_values'].update({'image': matching_iso}) deploy_command = None if "deploy_playbook" in payload: @@ -461,7 +470,8 @@ class SubcloudManager(manager.Manager): if migrate_flag: rehome_command = self.compose_rehome_command( subcloud.name, - ansible_subcloud_inventory_file) + ansible_subcloud_inventory_file, + subcloud.software_version) apply_thread = threading.Thread( target=self.run_deploy, args=(subcloud, payload, context, @@ -471,10 +481,12 @@ class SubcloudManager(manager.Manager): if "install_values" in payload: install_command = self.compose_install_command( subcloud.name, - ansible_subcloud_inventory_file) + ansible_subcloud_inventory_file, + subcloud.software_version) apply_command = self.compose_apply_command( subcloud.name, - ansible_subcloud_inventory_file) + ansible_subcloud_inventory_file, + subcloud.software_version) apply_thread = threading.Thread( target=self.run_deploy, args=(subcloud, payload, context, @@ -590,10 +602,12 @@ class SubcloudManager(manager.Manager): install_command = self.compose_install_command( subcloud.name, - ansible_subcloud_inventory_file) + ansible_subcloud_inventory_file, + payload['software_version']) apply_command = self.compose_apply_command( subcloud.name, - ansible_subcloud_inventory_file) + ansible_subcloud_inventory_file, + payload['software_version']) apply_thread = threading.Thread( target=self.run_deploy, args=(subcloud, payload, context, @@ -958,11 +972,11 @@ class SubcloudManager(manager.Manager): if payload.get('with_install'): install_command = self.compose_install_command( - subcloud.name, subcloud_inventory_file) + subcloud.name, subcloud_inventory_file, subcloud.software_version) # Update data_install with missing data - matching_iso, _ = utils.get_vault_load_files(SW_VERSION) + matching_iso, _ = utils.get_vault_load_files(subcloud.software_version) data_install['image'] = matching_iso - data_install['software_version'] = SW_VERSION + data_install['software_version'] = subcloud.software_version data_install['ansible_ssh_pass'] = payload['sysadmin_password'] data_install['ansible_become_pass'] = payload['sysadmin_password'] install_success = self._run_subcloud_install( diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py index 411332082..af1d8f671 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py @@ -27,7 +27,7 @@ from dcmanager.tests import utils from tsconfig.tsconfig import SW_VERSION -FAKE_RELEASE = '21.12' +FAKE_SOFTWARE_VERSION = '21.12' FAKE_TENANT = utils.UUID1 FAKE_ID = '1' FAKE_URL = '/v1.0/subcloud-deploy' @@ -55,7 +55,7 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): @mock.patch.object(subcloud_deploy.SubcloudDeployController, '_upload_files') def test_post_subcloud_deploy(self, mock_upload_files): - params = [('release_version', FAKE_RELEASE)] + params = [('release', FAKE_SOFTWARE_VERSION)] fields = list() for opt in consts.DEPLOY_COMMON_FILE_OPTIONS: fake_name = opt + "_fake" @@ -67,7 +67,7 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): headers=FAKE_HEADERS, params=params) self.assertEqual(response.status_code, http_client.OK) - self.assertEqual(FAKE_RELEASE, response.json['release_version']) + self.assertEqual(FAKE_SOFTWARE_VERSION, response.json['software_version']) @mock.patch.object(subcloud_deploy.SubcloudDeployController, '_upload_files') @@ -83,7 +83,7 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): upload_files=fields) self.assertEqual(response.status_code, http_client.OK) # Verify the active release will be returned if release doesn't present - self.assertEqual(SW_VERSION, response.json['release_version']) + self.assertEqual(SW_VERSION, response.json['software_version']) @mock.patch.object(subcloud_deploy.SubcloudDeployController, '_upload_files') @@ -206,11 +206,11 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): os.path.isdir = mock.Mock(return_value=True) mock_get_filename_by_prefix.side_effect = \ get_filename_by_prefix_side_effect - url = FAKE_URL + '/' + FAKE_RELEASE + url = FAKE_URL + '/' + FAKE_SOFTWARE_VERSION response = self.app.get(url, headers=FAKE_HEADERS) self.assertEqual(response.status_code, http_client.OK) - self.assertEqual(FAKE_RELEASE, - response.json['subcloud_deploy']['release_version']) + self.assertEqual(FAKE_SOFTWARE_VERSION, + response.json['subcloud_deploy']['software_version']) self.assertEqual(FAKE_DEPLOY_PLAYBOOK_FILE, response.json['subcloud_deploy'][consts.DEPLOY_PLAYBOOK]) self.assertEqual(FAKE_DEPLOY_OVERRIDES_FILE, @@ -236,7 +236,7 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): response = self.app.get(FAKE_URL, headers=FAKE_HEADERS) self.assertEqual(response.status_code, http_client.OK) self.assertEqual(SW_VERSION, - response.json['subcloud_deploy']['release_version']) + response.json['subcloud_deploy']['software_version']) self.assertEqual(FAKE_DEPLOY_PLAYBOOK_FILE, response.json['subcloud_deploy'][consts.DEPLOY_PLAYBOOK]) self.assertEqual(FAKE_DEPLOY_OVERRIDES_FILE, diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index 732210921..5d3c61002 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -40,6 +40,8 @@ from dcmanager.tests.unit.api.v1.controllers.mixins import PostMixin from dcmanager.tests.unit.common import fake_subcloud from dcmanager.tests import utils +from tsconfig.tsconfig import SW_VERSION + SAMPLE_SUBCLOUD_NAME = 'SubcloudX' SAMPLE_SUBCLOUD_DESCRIPTION = 'A Subcloud of mystery' @@ -186,8 +188,6 @@ class SubcloudAPIMixin(APIMixin): # based off MANDATORY_INSTALL_VALUES # bmc_password must be passed as a param FAKE_INSTALL_DATA = { - "image": "fake image", - "software_version": "123.456", "bootstrap_interface": "fake interface", "bootstrap_address": "10.10.10.12", "bootstrap_address_prefix": "10.10.10.12", @@ -494,9 +494,12 @@ class TestSubcloudPost(testroot.DCManagerApiTest, bad_values, good_value) - def test_post_subcloud_install_values(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_values(self, mock_vault_files): """Test POST operation with install values is supported by the API.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') + # pass a different "install" list of files for this POST self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) upload_files = self.get_post_upload_files() @@ -513,6 +516,86 @@ class TestSubcloudPost(testroot.DCManagerApiTest, headers=self.get_api_headers()) self._verify_post_success(response) + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_without_release_parameter(self, mock_vault_files): + """Test POST operation without release parameter.""" + + mock_vault_files.return_value = ('fake_iso', 'fake_sig') + + self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) + upload_files = self.get_post_upload_files() + + params = self.get_post_params() + # add bmc_password to params + params.update( + {'bmc_password': + base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8")}) + + response = self.app.post(self.get_api_prefix(), + params=params, + upload_files=upload_files, + headers=self.get_api_headers()) + self._verify_post_success(response) + # Verify that the subcloud installed with the active release + # when no release parameter provided. + self.assertEqual(SW_VERSION, response.json['software-version']) + + def test_post_subcloud_release_not_match_install_values_sw(self): + """Release parameter not match software_version in the install_values.""" + + self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) + upload_files = self.get_post_upload_files() + + params = self.get_post_params() + # add bmc_password and release to params + params.update( + {'bmc_password': + base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"), + 'release': '21.12'}) + + response = self.app.post(self.get_api_prefix(), + params=params, + upload_files=upload_files, + headers=self.get_api_headers(), + expect_errors=True) + + # Verify the request was rejected + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + + @mock.patch.object(subclouds.SubcloudsController, '_validate_k8s_version') + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_with_release_parameter(self, mock_vault_files, + mock_validate_k8s_version): + """Test POST operation with release parameter.""" + + mock_vault_files.return_value = ('fake_iso', 'fake_sig') + software_version = '21.12' + # Update the software_version value to match the release parameter value, + # otherwise, the request will be rejected + self.install_data['software_version'] = software_version + + self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) + upload_files = self.get_post_upload_files() + + params = self.get_post_params() + # add bmc_password and release to params + params.update( + {'bmc_password': + base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"), + 'release': software_version}) + + response = self.app.post(self.get_api_prefix(), + params=params, + upload_files=upload_files, + headers=self.get_api_headers(), + expect_errors=True) + + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(software_version, response.json['software-version']) + + # Revert the software_version value + self.install_data['software_version'] = SW_VERSION + @mock.patch.object(subclouds.PatchingClient, 'query') def test_post_subcloud_when_partial_applied_patch(self, mock_query): """Test POST operation when there is a partial-applied patch.""" @@ -528,9 +611,12 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self.assertEqual(http_client.UNPROCESSABLE_ENTITY, response.status_code) self.assertEqual('text/plain', response.content_type) - def test_post_subcloud_install_values_no_bmc_password(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_values_no_bmc_password(self, mock_vault_files): """Test POST operation with install values is supported by the API.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') + # pass a different "install" list of files for this POST self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) upload_files = self.get_post_upload_files() @@ -555,12 +641,33 @@ class TestSubcloudPost(testroot.DCManagerApiTest, headers=self.get_api_headers()) self._verify_post_success(response) + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_missing_image(self, mock_vault_files): + """Test POST operation without image in install values and vault files.""" + + mock_vault_files.return_value = (None, None) + + params = self.get_post_params() + # add bmc_password to params + params.update( + {'bmc_password': + base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8")}) + + self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) + self.install_data = copy.copy(self.FAKE_INSTALL_DATA) + upload_files = self.get_post_upload_files() + response = self.app.post(self.get_api_prefix(), + params=params, + upload_files=upload_files, + headers=self.get_api_headers(), + expect_errors=True) + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + @mock.patch('dcmanager.common.utils.get_vault_load_files') def test_post_subcloud_install_values_missing(self, mock_vault_files): """Test POST operation with install values fails if data missing.""" - # todo(abailey): add a new unit test with no image and no vault files - mock_vault_files.return_value = (None, None) + mock_vault_files.return_value = ('fake_iso', 'fake_sig') params = self.get_post_params() # add bmc_password to params @@ -581,15 +688,45 @@ class TestSubcloudPost(testroot.DCManagerApiTest, expect_errors=True) self._verify_post_failure(response, key, None) + @mock.patch('dcmanager.common.utils.get_vault_load_files') + @mock.patch.object(cutils, 'get_playbook_for_software_version') + @mock.patch.object(cutils, 'get_value_from_yaml_file') + def test_post_subcloud_bad_kubernetes_version(self, + mock_get_value_from_yaml_file, + mock_get_playbook_for_software_version, + mock_vault_files): + """Test POST operation with bad kubernetes_version.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') - # try with nothing removed and verify it works + + software_version = '21.12' + # Update the software_version value to match the release parameter value, + # otherwise, the request will be rejected + self.install_data['software_version'] = software_version + + params = self.get_post_params() + # add bmc_password to params + params.update( + {'bmc_password': + base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"), + 'release': software_version}) + + # Add kubernetes version to bootstrap_data + self.bootstrap_data['kubernetes_version'] = '1.21.8' + mock_get_value_from_yaml_file.return_value = '1.23.1' + + self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) self.install_data = copy.copy(self.FAKE_INSTALL_DATA) upload_files = self.get_post_upload_files() response = self.app.post(self.get_api_prefix(), params=params, upload_files=upload_files, - headers=self.get_api_headers()) - self._verify_post_success(response) + headers=self.get_api_headers(), + expect_errors=True) + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + + # Revert the change of bootstrap_data + del self.bootstrap_data['kubernetes_version'] def _test_post_input_value_inputs(self, setup_overrides, @@ -614,6 +751,7 @@ class TestSubcloudPost(testroot.DCManagerApiTest, starting_data = copy.copy(self.FAKE_INSTALL_DATA) for key, val in setup_overrides.items(): starting_data[key] = val + starting_data['image'] = 'fake image' # Test all the bad param values for bad_value in bad_values: @@ -661,9 +799,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, headers=self.get_api_headers()) self._verify_post_success(response) - def test_post_subcloud_install_values_invalid_type(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_values_invalid_type(self, mock_vault_files): """Test POST with an invalid type specified in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} required_overrides = {} # the install_type must a number 0 <= X <=5 @@ -679,9 +819,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_bootstrap_ip(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_bootstrap_ip(self, mock_vault_files): """Test POST with invalid boostrap ip specified in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} required_overrides = {} install_key = "bootstrap_address" @@ -694,9 +836,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_bmc_ip(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_bmc_ip(self, mock_vault_files): """Test POST with invalid bmc ip specified in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} required_overrides = {} install_key = "bmc_address" @@ -708,9 +852,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_persistent_size(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_persistent_size(self, mock_vault_files): """Test POST with invalid persistent_size specified in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} required_overrides = {} install_key = "persistent_size" @@ -723,9 +869,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_nexthop_gateway(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_nexthop_gateway(self, mock_vault_files): """Test POST with invalid nexthop_gateway in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} required_overrides = {} # nexthop_gateway is not required. but if provided, it must be valid @@ -738,9 +886,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_network_address(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_network_address(self, mock_vault_files): """Test POST with invalid network_address in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = {} # The nexthop_gateway is required when network_address is present # The network mask is required when network address is present @@ -757,9 +907,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_bad_network_mask(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_bad_network_mask(self, mock_vault_files): """Test POST with invalid network_mask in install values.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') # network_address is not required. but if provided a valid network_mask # is needed setup_overrides = { @@ -778,9 +930,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_diff_bmc_ip_version(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_diff_bmc_ip_version(self, mock_vault_files): """Test POST install values with mismatched(ipv4/ipv6) bmc ip.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') setup_overrides = { "bootstrap_address": "192.168.1.2" } @@ -795,9 +949,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_diff_bmc_ip_version_ipv6(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_diff_bmc_ip_version_ipv6(self, mock_vault_files): """Test POST install values with mismatched(ipv6/ipv4) bmc ip.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') # version of bootstrap address must be same as bmc_address setup_overrides = { "bootstrap_address": "fd01:6::7" @@ -812,9 +968,11 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_diff_nexthop_ip_version(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_diff_nexthop_ip_version(self, mock_vault_files): """Test POST install values mismatched(ipv4/ipv6) nexthop_gateway.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') # ip version of bootstrap address must be same as nexthop_gateway # All required addresses (like bmc address) much match bootstrap # default bmc address is ipv4 @@ -828,9 +986,12 @@ class TestSubcloudPost(testroot.DCManagerApiTest, self._test_post_input_value_inputs(setup_overrides, required_overrides, install_key, bad_values, good_value) - def test_post_subcloud_install_diff_nexthop_ip_version_ipv6(self): + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_post_subcloud_install_diff_nexthop_ip_version_ipv6(self, + mock_vault_files): """Test POST install values with mismatched(ipv6/ipv4) bmc ip.""" + mock_vault_files.return_value = ('fake_iso', 'fake_sig') # version of bootstrap address must be same as nexthop_gateway # All required addresses must also be setup ipv6 such as bmc_address # default bmc address is ipv4 @@ -1037,9 +1198,11 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_patch_data') - def test_update_subcloud_install_values_persistent_size(self, + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_update_subcloud_install_values_persistent_size(self, mock_vault_files, mock_get_patch_data, mock_rpc_client): + mock_vault_files.return_value = ('fake_iso', 'fake_sig') subcloud = fake_subcloud.create_fake_subcloud(self.ctx, data_install=None) payload = {} install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES_WITH_PERSISTENT_SIZE) @@ -1102,8 +1265,11 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_patch_data') - def test_patch_subcloud_install_values(self, mock_get_patch_data, + @mock.patch('dcmanager.common.utils.get_vault_load_files') + def test_patch_subcloud_install_values(self, mock_vault_files, + mock_get_patch_data, mock_rpc_client): + mock_vault_files.return_value = ('fake_iso', 'fake_sig') subcloud = fake_subcloud.create_fake_subcloud(self.ctx, data_install=None) payload = {} install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) @@ -1136,12 +1302,14 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_patch_data') + @mock.patch('dcmanager.common.utils.get_vault_load_files') def test_patch_subcloud_install_values_with_existing_data_install( - self, mock_get_patch_data, mock_rpc_client): + self, mock_vault_files, mock_get_patch_data, mock_rpc_client): + mock_vault_files.return_value = ('fake_iso', 'fake_sig') install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) subcloud = fake_subcloud.create_fake_subcloud( self.ctx, data_install=json.dumps(install_data)) - install_data.update({"software_version": "18.04"}) + install_data.update({"install_type": 2}) payload = {} encoded_password = base64.b64encode( 'bmc_password'.encode("utf-8")).decode('utf-8') @@ -1455,8 +1623,10 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + @mock.patch.object(subclouds.SubcloudsController, '_upload_deploy_config_file') def test_reinstall_subcloud( - self, mock_get_request_data, mock_validate_oam_network_config, + self, mock_upload_deploy_config_file, + mock_get_request_data, mock_validate_oam_network_config, mock_get_subcloud_db_install_values, mock_rpc_client, mock_get_vault_load_files): @@ -1485,6 +1655,56 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): mock.ANY) self.assertEqual(response.status_int, 200) + mock_upload_deploy_config_file.assert_called_once() + self.assertEqual(SW_VERSION, response.json['software-version']) + + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, + '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + @mock.patch.object(subclouds.SubcloudsController, '_upload_deploy_config_file') + @mock.patch.object(subclouds.SubcloudsController, '_validate_k8s_version') + def test_reinstall_subcloud_with_release_parameter( + self, mock_validate_k8s_version, mock_upload_deploy_config_file, + mock_get_request_data, mock_validate_oam_network_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): + + software_version = '21.12' + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + reinstall_data['release'] = software_version + mock_get_request_data.return_value = reinstall_data + + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + mock_get_subcloud_db_install_values.return_value = install_data + + mock_rpc_client().reinstall_subcloud.return_value = True + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + response = self.app.patch_json( + FAKE_URL + '/' + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params=reinstall_data) + + mock_validate_oam_network_config.assert_called_once() + mock_rpc_client().reinstall_subcloud.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + + mock_validate_k8s_version.assert_called_once() + mock_upload_deploy_config_file.assert_called_once() + self.assertEqual(software_version, response.json['software-version']) + self.assertIn(software_version, + json.loads(response.json['data_install'])['software_version']) + @mock.patch.object(cutils, 'get_vault_load_files') @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') diff --git a/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py b/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py index c9df341b5..6792354a9 100644 --- a/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py +++ b/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2020-2022 Wind River Systems, Inc. +# Copyright (c) 2020-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -16,6 +16,8 @@ FAKE_ID = '1' FAKE_URL = '/v1.0/subclouds' WRONG_URL = '/v1.0/wrong' +FAKE_SOFTWARE_VERSION = '18.03' + FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin,member,reader', 'X-Identity-Status': 'Confirmed', 'X-Project-Name': 'admin'} @@ -61,7 +63,7 @@ FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD = { FAKE_SUBCLOUD_INSTALL_VALUES = { "image": "http://192.168.101.2:8080/iso/bootimage.iso", - "software_version": "18.03", + "software_version": FAKE_SOFTWARE_VERSION, "bootstrap_interface": "eno1", "bootstrap_address": "128.224.151.183", "bootstrap_address_prefix": 23, @@ -81,7 +83,7 @@ FAKE_SUBCLOUD_INSTALL_VALUES = { FAKE_SUBCLOUD_INSTALL_VALUES_WITH_PERSISTENT_SIZE = { "image": "http://192.168.101.2:8080/iso/bootimage.iso", - "software_version": "18.03", + "software_version": FAKE_SOFTWARE_VERSION, "bootstrap_interface": "eno1", "bootstrap_address": "128.224.151.183", "bootstrap_address_prefix": 23, @@ -105,7 +107,7 @@ def create_fake_subcloud(ctxt, **kwargs): "name": "subcloud1", "description": "subcloud1 description", "location": "subcloud1 location", - 'software_version': "18.03", + 'software_version': FAKE_SOFTWARE_VERSION, "management_subnet": "192.168.101.0/24", "management_gateway_ip": "192.168.101.1", "management_start_ip": "192.168.101.2", diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 0b75d698c..90e925837 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -14,6 +14,7 @@ import copy import datetime + import mock from os import path as os_path @@ -40,6 +41,7 @@ from dcmanager.tests import utils from tsconfig.tsconfig import SW_VERSION LAST_SW_VERSION_IN_CENTOS = "22.06" +FAKE_PREVIOUS_SW_VERSION = '21.12' FAKE_ADMIN_USER_ID = 1 @@ -408,6 +410,8 @@ class TestSubcloudManager(base.DCManagerTestCase): self.assertEqual('localhost', sm.host) self.assertEqual(self.ctx, sm.context) + @mock.patch.object(subcloud_manager.SubcloudManager, + 'compose_apply_command') @mock.patch.object(subcloud_manager.SubcloudManager, 'compose_rehome_command') @mock.patch.object(subcloud_manager.SubcloudManager, @@ -435,12 +439,13 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_keystone_client, mock_delete_subcloud_inventory, mock_create_intermediate_ca_cert, - mock_compose_rehome_command): + mock_compose_rehome_command, + mock_compose_apply_command): values = utils.create_subcloud_dict(base.SUBCLOUD_SAMPLE_DATA_0) values['deploy_status'] = consts.DEPLOY_STATE_NONE # dcmanager add_subcloud queries the data from the db - self.create_subcloud_static(self.ctx, name=values['name']) + subcloud = self.create_subcloud_static(self.ctx, name=values['name']) mock_keystone_client().keystone_client = FakeKeystoneClient() mock_keyring.get_password.return_value = "testpassword" @@ -458,6 +463,10 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_thread_start.assert_called_once() mock_create_intermediate_ca_cert.assert_called_once() mock_compose_rehome_command.assert_not_called() + mock_compose_apply_command.assert_called_once_with( + values['name'], + sm._get_ansible_filename(values['name'], consts.INVENTORY_FILE_POSTFIX), + subcloud['software_version']) # Verify subcloud was updated with correct values self.assertEqual(consts.DEPLOY_STATE_PRE_DEPLOY, @@ -502,7 +511,7 @@ class TestSubcloudManager(base.DCManagerTestCase): values['migrate'] = 'true' # dcmanager add_subcloud queries the data from the db - self.create_subcloud_static(self.ctx, name=values['name']) + subcloud = self.create_subcloud_static(self.ctx, name=values['name']) mock_keystone_client().keystone_client = FakeKeystoneClient() mock_keyring.get_password.return_value = "testpassword" @@ -518,7 +527,10 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_write_subcloud_ansible_config.assert_called_once() mock_thread_start.assert_called_once() mock_create_intermediate_ca_cert.assert_called_once() - mock_compose_rehome_command.assert_called_once() + mock_compose_rehome_command.assert_called_once_with( + values['name'], + sm._get_ansible_filename(values['name'], consts.INVENTORY_FILE_POSTFIX), + subcloud['software_version']) # Verify subcloud was updated with correct values self.assertEqual(consts.DEPLOY_STATE_PRE_REHOME, @@ -1379,7 +1391,9 @@ class TestSubcloudManager(base.DCManagerTestCase): def test_compose_install_command(self): sm = subcloud_manager.SubcloudManager() install_command = sm.compose_install_command( - 'subcloud1', '/var/opt/dc/ansible/subcloud1_inventory.yml') + 'subcloud1', + '/var/opt/dc/ansible/subcloud1_inventory.yml', + FAKE_PREVIOUS_SW_VERSION) self.assertEqual( install_command, [ @@ -1387,20 +1401,27 @@ class TestSubcloudManager(base.DCManagerTestCase): subcloud_manager.ANSIBLE_SUBCLOUD_INSTALL_PLAYBOOK, '-i', '/var/opt/dc/ansible/subcloud1_inventory.yml', '--limit', 'subcloud1', - '-e', "@/var/opt/dc/ansible/subcloud1/install_values.yml" + '-e', "@/var/opt/dc/ansible/subcloud1/install_values.yml", + '-e', "install_release_version=%s" % FAKE_PREVIOUS_SW_VERSION ] ) - def test_compose_apply_command(self): + @mock.patch('os.path.isfile') + def test_compose_apply_command(self, mock_isfile): + mock_isfile.return_value = True sm = subcloud_manager.SubcloudManager() apply_command = sm.compose_apply_command( - 'subcloud1', '/var/opt/dc/ansible/subcloud1_inventory.yml') + 'subcloud1', + '/var/opt/dc/ansible/subcloud1_inventory.yml', + FAKE_PREVIOUS_SW_VERSION) self.assertEqual( apply_command, [ 'ansible-playbook', - subcloud_manager.ANSIBLE_SUBCLOUD_PLAYBOOK, '-i', - '/var/opt/dc/ansible/subcloud1_inventory.yml', + cutils.get_playbook_for_software_version( + subcloud_manager.ANSIBLE_SUBCLOUD_PLAYBOOK, + FAKE_PREVIOUS_SW_VERSION), + '-i', '/var/opt/dc/ansible/subcloud1_inventory.yml', '--limit', 'subcloud1', '-e', "override_files_dir='/var/opt/dc/ansible' region_name=subcloud1" ] @@ -1427,16 +1448,22 @@ class TestSubcloudManager(base.DCManagerTestCase): ] ) - def test_compose_rehome_command(self): + @mock.patch('os.path.isfile') + def test_compose_rehome_command(self, mock_isfile): + mock_isfile.return_value = True sm = subcloud_manager.SubcloudManager() rehome_command = sm.compose_rehome_command( - 'subcloud1', '/var/opt/dc/ansible/subcloud1_inventory.yml') + 'subcloud1', + '/var/opt/dc/ansible/subcloud1_inventory.yml', + FAKE_PREVIOUS_SW_VERSION) self.assertEqual( rehome_command, [ 'ansible-playbook', - subcloud_manager.ANSIBLE_SUBCLOUD_REHOME_PLAYBOOK, '-i', - '/var/opt/dc/ansible/subcloud1_inventory.yml', + cutils.get_playbook_for_software_version( + subcloud_manager.ANSIBLE_SUBCLOUD_REHOME_PLAYBOOK, + FAKE_PREVIOUS_SW_VERSION), + '-i', '/var/opt/dc/ansible/subcloud1_inventory.yml', '--limit', 'subcloud1', '--timeout', subcloud_manager.REHOME_PLAYBOOK_TIMEOUT, '-e', @@ -1463,9 +1490,10 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_compose_apply_command, mock_compose_install_command, mock_create_intermediate_ca_cert, mock_write_subcloud_ansible_config): + subcloud_name = 'subcloud1' subcloud = self.create_subcloud_static( self.ctx, - name='subcloud1', + name=subcloud_name, deploy_status=consts.DEPLOY_STATE_PRE_INSTALL) fake_install_values = \ @@ -1474,7 +1502,7 @@ class TestSubcloudManager(base.DCManagerTestCase): fake_payload = copy.copy(fake_subcloud.FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) fake_payload.update({ 'bmc_password': 'bmc_pass', - 'software_version': SW_VERSION, + 'software_version': FAKE_PREVIOUS_SW_VERSION, 'install_values': fake_install_values}) sm = subcloud_manager.SubcloudManager() @@ -1487,8 +1515,14 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_create_subcloud_inventory.assert_called_once() mock_create_intermediate_ca_cert.assert_called_once() mock_write_subcloud_ansible_config.assert_called_once() - mock_compose_install_command.assert_called_once() - mock_compose_apply_command.assert_called_once() + mock_compose_install_command.assert_called_once_with( + subcloud_name, + sm._get_ansible_filename(subcloud_name, consts.INVENTORY_FILE_POSTFIX), + FAKE_PREVIOUS_SW_VERSION) + mock_compose_apply_command.assert_called_once_with( + subcloud_name, + sm._get_ansible_filename(subcloud_name, consts.INVENTORY_FILE_POSTFIX), + FAKE_PREVIOUS_SW_VERSION) mock_thread_start.assert_called_once() def test_handle_subcloud_operations_in_progress(self):