From ad1f05ac5f8639f1b671297e053ef10445cec959 Mon Sep 17 00:00:00 2001 From: Li Zhu Date: Fri, 31 Mar 2023 20:02:28 -0400 Subject: [PATCH] Add release optionality to subcloud add/reinstall Add an optional --release parameter to subcloud add and reinstall commands to enable release optionality in subcloud add and subcloud reinstall. Test Plan: 1. Verify successful subcloud add which includes remote install with specified (previous/current) release 2. Verify successful subcloud reinstall with the specified (previous/current) release 3. Verify the subcloud is successfully installed with the active release when the release parameter is absent 4. Verify the subcloud is successfully reinstalled with the active release when the release parameter is absent 5. Verify the subcloud install request was rejected when the software_version in the install_values doesn't match the specified release 6. Verify the subcloud install/reinstall request was rejected when the kubernetes_version value specified in the subcloud bootstrap yaml file doesn't match the value of the fresh_install_k8s_version of the specified previous release Depends-On: https://review.opendev.org/c/starlingx/utilities/+/878545 https://review.opendev.org/c/starlingx/ansible-playbooks/+/878922 Story: 2010611 Task: 47684 Signed-off-by: lzhu1 Change-Id: Ic4193c2901d8bfa485eeb683c08422d946802bcb --- api-ref/source/api-ref-dcmanager-v1.rst | 16 +- api-ref/source/parameters.yaml | 12 + .../subcloud-deploy-get-response.json | 3 +- .../subcloud-deploy-post-request.json | 3 +- .../subcloud-deploy-post-response.json | 3 +- distributedcloud/dccommon/install_consts.py | 4 +- .../api/controllers/v1/subcloud_deploy.py | 12 +- .../dcmanager/api/controllers/v1/subclouds.py | 169 +++++++---- distributedcloud/dcmanager/common/consts.py | 4 + .../dcmanager/common/exceptions.py | 6 +- distributedcloud/dcmanager/common/utils.py | 52 +++- .../dcmanager/manager/subcloud_manager.py | 50 ++-- .../v1/controllers/test_subcloud_deploy.py | 16 +- .../unit/api/v1/controllers/test_subclouds.py | 270 ++++++++++++++++-- .../tests/unit/common/fake_subcloud.py | 10 +- .../unit/manager/test_subcloud_manager.py | 70 +++-- 16 files changed, 548 insertions(+), 152 deletions(-) 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):