Add sw version validation to all release param endpoints

Incorporate the existing release version validation with
all the dcmanager operations/commands endpoints that allow
the user to specify a release version:
 - subcloud add
 - subcloud prestage
 - subcloud redeploy
 - subcloud deploy show
 - subcloud deploy resume
 - subcloud deploy create
 - subcloud deploy install
 - prestage-strategy create
 - subcloud-backup restore

Please note that the "subcloud deploy upload" release
validation was addressed in a previous change [1].

These changes add the validate_release_version_supported check
(previously introduced [1]) to all the endpoints that
consume a release parameter. This is done to check
whether a specified release version is supported by the
current active version.

[1] https://review.opendev.org/c/starlingx/distcloud/+/891911

Test Plan:
For each command listed above, test the
release parameter (--release):
    1. PASS: Verify that the current active version is
       accepted as a valid parameter.
    2. PASS: Verify that all the supported upgrade versions
       in /usr/rootdirs/opt/upgrades/metadata.xml are accepted
       as a valid release parameter.
    3. PASS: Verify that any upgrade version that's not included
       in metadata.xml is rejected with an error
       "<release> is not a supported release version". The only
       exception to this is the current active version, it must
       always be valid.
    4. PASS: Delete all the "supported_upgrades" elements in
       metadata.xml and verify the error
       "Unable to validate the release version" is printed.
    5. PASS: Delete metadata.xml and verify that
       "Unable to validate the release version" error is printed.
    6. PASS: Verify that the current active version is valid
       regardless of metadata.xml file and its contents.
    7. PASS: Exclude the release parameter and verify that the
       command is successful (completed
       with the current active version).

Closes-Bug: 2036479

Change-Id: I1608a68ce6863f51dc0b90e0a6f6b9b588e85689
Signed-off-by: Salman Rana <salman.rana@windriver.com>
This commit is contained in:
Salman Rana 2023-09-19 08:07:18 -04:00
parent 4ed1f39731
commit 628d3f276d
12 changed files with 107 additions and 70 deletions

View File

@ -206,7 +206,11 @@ class PhasedSubcloudDeployController(object):
pecan.abort(400, _('The deploy install command can only be used ' pecan.abort(400, _('The deploy install command can only be used '
'during initial deployment.')) 'during initial deployment.'))
payload['software_version'] = payload.get('release', subcloud.software_version) unvalidated_sw_version = payload.get('release', subcloud.software_version)
# get_sw_version will simply return back
# the passed unvalidated_sw_version after validating it.
payload['software_version'] = utils.get_sw_version(unvalidated_sw_version)
psd_common.populate_payload_with_pre_existing_data( psd_common.populate_payload_with_pre_existing_data(
payload, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS) payload, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS)
@ -426,11 +430,15 @@ class PhasedSubcloudDeployController(object):
# Consider the incoming release parameter only if install is one # Consider the incoming release parameter only if install is one
# of the pending deploy states # of the pending deploy states
if INSTALL in deploy_states_to_run: if INSTALL in deploy_states_to_run:
payload['software_version'] = payload.get('release', subcloud.software_version) unvalidated_sw_version = payload.get('release', subcloud.software_version)
else: else:
LOG.debug('Disregarding release parameter for %s as installation is complete.' LOG.debug('Disregarding release parameter for %s as installation is complete.'
% subcloud.name) % subcloud.name)
payload['software_version'] = subcloud.software_version unvalidated_sw_version = subcloud.software_version
# get_sw_version will simply return back the passed
# unvalidated_sw_version after validating it.
payload['software_version'] = utils.get_sw_version(unvalidated_sw_version)
# Need to remove bootstrap_values from the list of files to populate # Need to remove bootstrap_values from the list of files to populate
# pre existing data so it does not overwrite newly loaded values # pre existing data so it does not overwrite newly loaded values

View File

@ -15,7 +15,6 @@ import pecan
from pecan import expose from pecan import expose
from pecan import request as pecan_request from pecan import request as pecan_request
from pecan import response from pecan import response
import tsconfig.tsconfig as tsc
import yaml import yaml
from dcmanager.api.controllers import restcomm from dcmanager.api.controllers import restcomm
@ -370,7 +369,8 @@ class SubcloudBackupController(object):
if payload.get('with_install'): if payload.get('with_install'):
# Confirm the requested or active load is still in dc-vault # Confirm the requested or active load is still in dc-vault
payload['software_version'] = payload.get('release', tsc.SW_VERSION) payload['software_version'] = utils.get_sw_version(
payload.get('release'))
matching_iso, err_msg = utils.get_matching_iso(payload['software_version']) matching_iso, err_msg = utils.get_matching_iso(payload['software_version'])
if err_msg: if err_msg:
LOG.exception(err_msg) LOG.exception(err_msg)

View File

@ -32,12 +32,9 @@ from dcmanager.api.controllers import restcomm
from dcmanager.api.policies import subcloud_deploy as subcloud_deploy_policy from dcmanager.api.policies import subcloud_deploy as subcloud_deploy_policy
from dcmanager.api import policy from dcmanager.api import policy
from dcmanager.common import consts from dcmanager.common import consts
from dcmanager.common import exceptions
from dcmanager.common.i18n import _ from dcmanager.common.i18n import _
from dcmanager.common import utils from dcmanager.common import utils
import tsconfig.tsconfig as tsc
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -106,20 +103,9 @@ class SubcloudDeployController(object):
error_msg = "error: argument %s is required" % missing_str.rstrip() error_msg = "error: argument %s is required" % missing_str.rstrip()
pecan.abort(httpclient.BAD_REQUEST, error_msg) pecan.abort(httpclient.BAD_REQUEST, error_msg)
software_version = tsc.SW_VERSION deploy_dicts['software_version'] = utils.get_sw_version(request.POST.get('release'))
if request.POST.get('release'):
try:
utils.validate_release_version_supported(request.POST.get('release'))
software_version = request.POST.get('release')
except exceptions.ValidateFail as e:
pecan.abort(httpclient.BAD_REQUEST,
_("Error: invalid release version parameter. %s" % e))
except Exception:
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
_('Error: unable to validate the release version.'))
deploy_dicts['software_version'] = software_version
dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, software_version) dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, deploy_dicts['software_version'])
for f in consts.DEPLOY_COMMON_FILE_OPTIONS: for f in consts.DEPLOY_COMMON_FILE_OPTIONS:
if f not in request.POST: if f not in request.POST:
continue continue
@ -152,10 +138,8 @@ class SubcloudDeployController(object):
policy.authorize(subcloud_deploy_policy.POLICY_ROOT % "get", {}, policy.authorize(subcloud_deploy_policy.POLICY_ROOT % "get", {},
restcomm.extract_credentials_for_policy()) restcomm.extract_credentials_for_policy())
deploy_dicts = dict() deploy_dicts = dict()
if not release: deploy_dicts['software_version'] = utils.get_sw_version(release)
release = tsc.SW_VERSION dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, deploy_dicts['software_version'])
deploy_dicts['software_version'] = release
dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, release)
for f in consts.DEPLOY_COMMON_FILE_OPTIONS: for f in consts.DEPLOY_COMMON_FILE_OPTIONS:
filename = None filename = None
if os.path.isdir(dir_path): if os.path.isdir(dir_path):

View File

@ -41,8 +41,6 @@ from dccommon import exceptions as dccommon_exceptions
from keystoneauth1 import exceptions as keystone_exceptions from keystoneauth1 import exceptions as keystone_exceptions
import tsconfig.tsconfig as tsc
from dcmanager.api.controllers import restcomm from dcmanager.api.controllers import restcomm
from dcmanager.api.policies import subclouds as subclouds_policy from dcmanager.api.policies import subclouds as subclouds_policy
from dcmanager.api import policy from dcmanager.api import policy
@ -748,9 +746,7 @@ class SubcloudsController(object):
LOG.warning(msg) LOG.warning(msg)
pecan.abort(400, msg) pecan.abort(400, msg)
# If a subcloud release is not passed, use the current payload['software_version'] = utils.get_sw_version(payload.get('release'))
# system controller software_version
payload['software_version'] = payload.get('release', tsc.SW_VERSION)
# Don't load previously stored bootstrap_values if they are present in # Don't load previously stored bootstrap_values if they are present in
# the request, as this would override the already loaded values from it. # the request, as this would override the already loaded values from it.
@ -831,8 +827,8 @@ class SubcloudsController(object):
LOG.exception("validate_prestage failed") LOG.exception("validate_prestage failed")
pecan.abort(400, _(str(exc))) pecan.abort(400, _(str(exc)))
prestage_software_version = payload.get( prestage_software_version = utils.get_sw_version(
consts.PRESTAGE_REQUEST_RELEASE, tsc.SW_VERSION) payload.get(consts.PRESTAGE_REQUEST_RELEASE))
try: try:
self.dcmanager_rpc_client.prestage_subcloud(context, payload) self.dcmanager_rpc_client.prestage_subcloud(context, payload)

View File

@ -1,5 +1,5 @@
# Copyright (c) 2017 Ericsson AB. # Copyright (c) 2017 Ericsson AB.
# Copyright (c) 2017-2022 Wind River Systems, Inc. # Copyright (c) 2017-2023 Wind River Systems, Inc.
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -205,6 +205,10 @@ class SwUpdateStrategyController(object):
if group is None: if group is None:
pecan.abort(400, _('Invalid group_id')) pecan.abort(400, _('Invalid group_id'))
# get_sw_version is used here to validate the
# release parameter if specified.
utils.get_sw_version(payload.get('release'))
# Not adding validation for extra args. Passing them through. # Not adding validation for extra args. Passing them through.
try: try:
# Ask dcmanager-manager to create the strategy. # Ask dcmanager-manager to create the strategy.

View File

@ -972,9 +972,7 @@ def pre_deploy_create(payload: dict, context: RequestContext,
validate_bootstrap_values(payload) validate_bootstrap_values(payload)
# If a subcloud release is not passed, use the current payload['software_version'] = utils.get_sw_version(payload.get('release'))
# system controller software_version
payload['software_version'] = payload.get('release', tsc.SW_VERSION)
validate_subcloud_name_availability(context, payload['name']) validate_subcloud_name_availability(context, payload['name'])

View File

@ -21,6 +21,7 @@ import itertools
import json import json
import netaddr import netaddr
import os import os
import pecan
import pwd import pwd
import re import re
import resource as sys_resource import resource as sys_resource
@ -46,6 +47,7 @@ from dccommon import exceptions as dccommon_exceptions
from dccommon import kubeoperator from dccommon import kubeoperator
from dcmanager.common import consts from dcmanager.common import consts
from dcmanager.common import exceptions from dcmanager.common import exceptions
from dcmanager.common.i18n import _
from dcmanager.db import api as db_api from dcmanager.db import api as db_api
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -1225,6 +1227,28 @@ def create_subcloud_rehome_data_template():
return {'saved_payload': {}} return {'saved_payload': {}}
def get_sw_version(release=None):
"""Get the sw_version to be used.
Return the sw_version by first validating a set release version.
If a release is not specified then use the current system controller
software_version.
"""
if release:
try:
validate_release_version_supported(release)
return release
except exceptions.ValidateFail as e:
pecan.abort(400,
_("Error: invalid release version parameter. %s" % e))
except Exception:
pecan.abort(500,
_('Error: unable to validate the release version.'))
else:
return tsc.SW_VERSION
def validate_release_version_supported(release_version_to_check): def validate_release_version_supported(release_version_to_check):
"""Given a release version, check whether it's supported by the current active version. """Given a release version, check whether it's supported by the current active version.
@ -1242,7 +1266,8 @@ def validate_release_version_supported(release_version_to_check):
supported_versions = get_current_supported_upgrade_versions() supported_versions = get_current_supported_upgrade_versions()
if release_version_to_check not in supported_versions: if release_version_to_check not in supported_versions:
msg = "%s is not a supported release version" % release_version_to_check msg = "%s is not a supported release version (%s)" % \
(release_version_to_check, ",".join(supported_versions))
raise exceptions.ValidateFail(msg) raise exceptions.ValidateFail(msg)
return True return True

View File

@ -404,9 +404,11 @@ class TestSubcloudDeployInstall(testroot.DCManagerApiTest):
self.mock_rpc_client().subcloud_deploy_install.return_value = True self.mock_rpc_client().subcloud_deploy_install.return_value = True
self.mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') self.mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path')
response = self.app.patch_json( with mock.patch('builtins.open',
FAKE_URL + '/' + str(subcloud.id) + '/install', mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
headers=FAKE_HEADERS, params=install_payload) response = self.app.patch_json(
FAKE_URL + '/' + str(subcloud.id) + '/install',
headers=FAKE_HEADERS, params=install_payload)
self.assertEqual(response.status_int, 200) self.assertEqual(response.status_int, 200)
self.assertEqual(consts.DEPLOY_STATE_PRE_INSTALL, self.assertEqual(consts.DEPLOY_STATE_PRE_INSTALL,

View File

@ -1272,9 +1272,11 @@ class TestSubcloudRestore(testroot.DCManagerApiTest):
mock_listdir.return_value = ['test.iso', 'test.sig'] mock_listdir.return_value = ['test.iso', 'test.sig']
mock_rpc_client().restore_subcloud_backups.return_value = True mock_rpc_client().restore_subcloud_backups.return_value = True
response = self.app.patch_json(FAKE_URL_RESTORE, with mock.patch('builtins.open',
headers=FAKE_HEADERS, mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
params=data) response = self.app.patch_json(FAKE_URL_RESTORE,
headers=FAKE_HEADERS,
params=data)
self.assertEqual(response.status_int, 200) self.assertEqual(response.status_int, 200)

View File

@ -25,6 +25,7 @@ from dcmanager.common import consts
from dcmanager.common import phased_subcloud_deploy as psd_common from dcmanager.common import phased_subcloud_deploy as psd_common
from dcmanager.common import utils as dutils from dcmanager.common import utils as dutils
from dcmanager.tests.unit.api import test_root_controller as testroot from dcmanager.tests.unit.api import test_root_controller as testroot
from dcmanager.tests.unit.common import fake_subcloud
from dcmanager.tests import utils from dcmanager.tests import utils
from tsconfig.tsconfig import SW_VERSION from tsconfig.tsconfig import SW_VERSION
@ -48,12 +49,6 @@ FAKE_DEPLOY_FILES = {
FAKE_DEPLOY_CHART_PREFIX: FAKE_DEPLOY_CHART_FILE, FAKE_DEPLOY_CHART_PREFIX: FAKE_DEPLOY_CHART_FILE,
} }
FAKE_UPGRADES_METADATA = '''
<build>\n<version>0.2</version>\n<supported_upgrades>
\n<upgrade>\n<version>%s</version>\n<required_patches>PATCH_0001</required_patches>
\n</upgrade>\n</supported_upgrades>\n</build>
''' % FAKE_SOFTWARE_VERSION
class TestSubcloudDeploy(testroot.DCManagerApiTest): class TestSubcloudDeploy(testroot.DCManagerApiTest):
def setUp(self): def setUp(self):
@ -72,7 +67,8 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest):
mock_upload_files.return_value = True mock_upload_files.return_value = True
params += fields params += fields
with mock.patch('builtins.open', mock.mock_open(read_data=FAKE_UPGRADES_METADATA)): with mock.patch('builtins.open',
mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
response = self.app.post(FAKE_URL, response = self.app.post(FAKE_URL,
headers=FAKE_HEADERS, headers=FAKE_HEADERS,
params=params) params=params)
@ -218,7 +214,11 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest):
mock_get_filename_by_prefix.side_effect = \ mock_get_filename_by_prefix.side_effect = \
get_filename_by_prefix_side_effect get_filename_by_prefix_side_effect
url = FAKE_URL + '/' + FAKE_SOFTWARE_VERSION url = FAKE_URL + '/' + FAKE_SOFTWARE_VERSION
response = self.app.get(url, headers=FAKE_HEADERS)
with mock.patch('builtins.open',
mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
response = self.app.get(url, headers=FAKE_HEADERS)
self.assertEqual(response.status_code, http_client.OK) self.assertEqual(response.status_code, http_client.OK)
self.assertEqual(FAKE_SOFTWARE_VERSION, self.assertEqual(FAKE_SOFTWARE_VERSION,
response.json['subcloud_deploy']['software_version']) response.json['subcloud_deploy']['software_version'])

View File

@ -554,11 +554,13 @@ class TestSubcloudPost(testroot.DCManagerApiTest,
base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"), base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"),
'release': '21.12'}) 'release': '21.12'})
response = self.app.post(self.get_api_prefix(), with mock.patch('builtins.open',
params=params, mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
upload_files=upload_files, response = self.app.post(self.get_api_prefix(),
headers=self.get_api_headers(), params=params,
expect_errors=True) upload_files=upload_files,
headers=self.get_api_headers(),
expect_errors=True)
# Verify the request was rejected # Verify the request was rejected
self.assertEqual(response.status_code, http_client.BAD_REQUEST) self.assertEqual(response.status_code, http_client.BAD_REQUEST)
@ -585,11 +587,13 @@ class TestSubcloudPost(testroot.DCManagerApiTest,
base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"), base64.b64encode('fake pass'.encode("utf-8")).decode("utf-8"),
'release': software_version}) 'release': software_version})
response = self.app.post(self.get_api_prefix(), with mock.patch('builtins.open',
params=params, mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
upload_files=upload_files, response = self.app.post(self.get_api_prefix(),
headers=self.get_api_headers(), params=params,
expect_errors=True) upload_files=upload_files,
headers=self.get_api_headers(),
expect_errors=True)
self.assertEqual(response.status_code, http_client.OK) self.assertEqual(response.status_code, http_client.OK)
self.assertEqual(software_version, response.json['software-version']) self.assertEqual(software_version, response.json['software-version'])
@ -719,11 +723,15 @@ class TestSubcloudPost(testroot.DCManagerApiTest,
self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS) self.set_list_of_post_files(subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS)
self.install_data = copy.copy(self.FAKE_INSTALL_DATA) self.install_data = copy.copy(self.FAKE_INSTALL_DATA)
upload_files = self.get_post_upload_files() upload_files = self.get_post_upload_files()
response = self.app.post(self.get_api_prefix(),
params=params, with mock.patch('builtins.open',
upload_files=upload_files, mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
headers=self.get_api_headers(), response = self.app.post(self.get_api_prefix(),
expect_errors=True) params=params,
upload_files=upload_files,
headers=self.get_api_headers(),
expect_errors=True)
self.assertEqual(response.status_code, http_client.BAD_REQUEST) self.assertEqual(response.status_code, http_client.BAD_REQUEST)
# Revert the change of bootstrap_data # Revert the change of bootstrap_data
@ -1689,10 +1697,12 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest):
("deploy_config", "config_fake_filename", ("deploy_config", "config_fake_filename",
json.dumps(config_data).encode("utf-8"))] json.dumps(config_data).encode("utf-8"))]
response = self.app.patch( with mock.patch('builtins.open',
FAKE_URL + '/' + str(subcloud.id) + '/redeploy', mock.mock_open(read_data=fake_subcloud.FAKE_UPGRADES_METADATA)):
headers=FAKE_HEADERS, params=redeploy_data, response = self.app.patch(
upload_files=upload_files) FAKE_URL + '/' + str(subcloud.id) + '/redeploy',
headers=FAKE_HEADERS, params=redeploy_data,
upload_files=upload_files)
mock_validate_bootstrap_values.assert_called_once() mock_validate_bootstrap_values.assert_called_once()
mock_validate_subcloud_config.assert_called_once() mock_validate_subcloud_config.assert_called_once()

View File

@ -116,6 +116,14 @@ FAKE_SUBCLOUD_INSTALL_VALUES_WITH_PERSISTENT_SIZE = {
"persistent_size": 40000, "persistent_size": 40000,
} }
FAKE_UPGRADES_METADATA = '''
<build>\n<version>0.1</version>\n<supported_upgrades>
\n<upgrade>\n<version>%s</version>\n</upgrade>
\n<upgrade>\n<version>21.12</version>\n</upgrade>
\n<upgrade>\n<version>22.12</version>\n</upgrade>
\n</supported_upgrades>\n</build>
''' % FAKE_SOFTWARE_VERSION
def create_fake_subcloud(ctxt, **kwargs): def create_fake_subcloud(ctxt, **kwargs):
values = { values = {