From 587adfe0108a2b32bc1ec2f6f2175a13aa1221a3 Mon Sep 17 00:00:00 2001 From: Guilherme Schons Date: Fri, 24 Feb 2023 12:26:46 -0300 Subject: [PATCH] Add inactive param for import-load on cgts-client This change added a parameter called 'inactive' to load-import workflow on cgts-client to allow import a previous release (ISO). Test Plan: PASS: (AIO-SX) failed to import the current version PASS: (AIO-SX) failed to import the current version with active param PASS: (AIO-SX) import the new version PASS: (AIO-SX) import new version with local param PASS: (AIO-SX) failed to import the previous release PASS: (AIO-SX) import the previous release with inactive param PASS: DC (--os-region-name SystemController) success to import currently version with active param PASS: DC (--os-region-name SystemController) failed to import currently version PASS: DC (--os-region-name SystemController) import new version PASS: DC (--os-region-name SystemController) import new version with local param PASS: DC (--os-region-name SystemController) import previous release with inactive param PASS: DC (--os-region-name SystemController) failed to import previous release PASS: DC (--os-region-name SystemController) extracted ISO files to the controller (/var/www/pages/feed/rel-version) Story: 2010611 Task: 47509 Depends-On: https://review.opendev.org/c/starlingx/config/+/875186 Signed-off-by: Guilherme Schons Change-Id: I053b0f29ffb347e109143181c6f60988706e6d29 --- .../cgts-client/cgtsclient/tests/utils.py | 16 ++ .../cgtsclient/tests/v1/test_load.py | 132 +++++++++++ .../cgtsclient/tests/v1/test_load_shell.py | 215 ++++++++++++++++++ .../cgts-client/cgtsclient/v1/load.py | 33 +-- .../cgts-client/cgtsclient/v1/load_shell.py | 23 +- 5 files changed, 399 insertions(+), 20 deletions(-) create mode 100644 sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load.py create mode 100644 sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load_shell.py diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/tests/utils.py b/sysinv/cgts-client/cgts-client/cgtsclient/tests/utils.py index 603243559b..9b160aca00 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/tests/utils.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/tests/utils.py @@ -47,6 +47,22 @@ class FakeAPI(object): fixture = self._request(*args, **kwargs) return FakeResponse(fixture[0]), fixture[1] + def upload_request_with_multipart(self, *args, **kwargs): + # TODO(gdossant): add 'data' parameter to _request method. + # It will impact more than 40 tests and must be done in + # a specific commit. + + kwargs.pop('check_exceptions') + data = kwargs.pop('data') + + fixture = self._request(*args, **kwargs) + + call = list(self.calls[0]) + call.append(data) + self.calls[0] = tuple(call) + + return fixture[1] + class FakeResponse(object): def __init__(self, headers, body=None, version=None): diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load.py b/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load.py new file mode 100644 index 0000000000..d15ef6a195 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load.py @@ -0,0 +1,132 @@ +# +# Copyright (c) 2023 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import testtools + +from cgtsclient.exc import InvalidAttribute +from cgtsclient.tests import utils +from cgtsclient.v1.load import Load +from cgtsclient.v1.load import LoadManager + + +class LoadManagerTest(testtools.TestCase): + def setUp(self): + super(LoadManagerTest, self).setUp() + + self.load = { + 'id': '1', + 'uuid': 'c0d71e4c-f327-45a7-8349-11821a9d44df', + 'state': 'IMPORTED', + 'software_version': '6.0', + 'compatible_version': '6.0', + 'required_patches': '', + } + fixtures = { + '/v1/loads/import_load': + { + 'POST': ( + {}, + self.load, + ), + }, + } + self.api = utils.FakeAPI(fixtures) + self.mgr = LoadManager(self.api) + + +class LoadImportTest(LoadManagerTest): + def setUp(self): + super(LoadImportTest, self).setUp() + + self.load_patch = { + 'path_to_iso': '/home/bootimage.iso', + 'path_to_sig': '/home/bootimage.sig', + 'inactive': False, + 'active': False, + 'local': False, + } + self.load_patch_request_body = { + 'path_to_iso': '/home/bootimage.iso', + 'path_to_sig': '/home/bootimage.sig', + } + + def test_load_import(self): + expected = [ + ( + 'POST', '/v1/loads/import_load', + {}, + self.load_patch_request_body, + {'active': 'false', 'inactive': 'false'}, + ) + ] + + load = self.mgr.import_load(**self.load_patch) + + self.assertEqual(self.api.calls, expected) + self.assertIsInstance(load, Load) + + def test_load_import_active(self): + self.load_patch['active'] = True + + expected = [ + ( + 'POST', '/v1/loads/import_load', + {}, + self.load_patch_request_body, + {'active': 'true', 'inactive': 'false'}, + ) + ] + + load = self.mgr.import_load(**self.load_patch) + + self.assertEqual(self.api.calls, expected) + self.assertIsInstance(load, Load) + + def test_load_import_local(self): + self.load_patch['local'] = True + self.load_patch_request_body['active'] = 'false' + self.load_patch_request_body['inactive'] = 'false' + + expected = [ + ( + 'POST', '/v1/loads/import_load', + {}, + self.load_patch_request_body, + ) + ] + + load = self.mgr.import_load(**self.load_patch) + + self.assertEqual(self.api.calls, expected) + self.assertIsInstance(load, Load) + + def test_load_import_inactive(self): + self.load_patch['inactive'] = True + + expected = [ + ( + 'POST', '/v1/loads/import_load', + {}, + self.load_patch_request_body, + {'active': 'false', 'inactive': 'true'} + ) + ] + + load = self.mgr.import_load(**self.load_patch) + + self.assertEqual(self.api.calls, expected) + self.assertIsInstance(load, Load) + + def test_load_import_invalid_attribute(self): + self.load_patch['foo'] = 'bar' + + self.assertRaises( + InvalidAttribute, + self.mgr.import_load, + **self.load_patch + ) + + self.assertEqual(self.api.calls, []) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load_shell.py new file mode 100644 index 0000000000..5d79421093 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/tests/v1/test_load_shell.py @@ -0,0 +1,215 @@ +# +# Copyright (c) 2023 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from mock import patch + +from cgtsclient.exc import CommandError +from cgtsclient.tests import test_shell +from cgtsclient.v1.load import Load + + +class LoadImportShellTest(test_shell.ShellTest): + def setUp(self): + super(LoadImportShellTest, self).setUp() + + load_import = patch('cgtsclient.v1.load.LoadManager.import_load') + self.mock_load_import = load_import.start() + self.addCleanup(load_import.stop) + + load_show = patch('cgtsclient.v1.load_shell._print_load_show') + self.mock_load_show = load_show.start() + self.addCleanup(load_show.stop) + + load_list = patch('cgtsclient.v1.load.LoadManager.list') + self.mock_load_list = load_list.start() + self.addCleanup(load_list.stop) + + load_resource = { + 'software_version': '6.0', + 'compatible_version': '5.0', + 'required_patches': '', + } + self.load_resouce = Load( + manager=None, + info=load_resource, + loaded=True, + ) + + self.mock_load_import.return_value = self.load_resouce + self.mock_load_list.return_value = [] + self.mock_load_show.return_value = {} + + self.patch_expected = { + 'path_to_iso': '/home/bootimage.iso', + 'path_to_sig': '/home/bootimage.sig', + 'active': False, + 'local': False, + 'inactive': False, + } + + @patch('os.path.isfile', lambda x: True) + def test_load_import(self): + self.make_env() + + cmd = 'load-import /home/bootimage.iso /home/bootimage.sig' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_list.assert_called_once() + self.mock_load_show.assert_called_once() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + @patch('os.path.abspath') + @patch('os.path.isfile', lambda x: True) + def test_load_import_relative_path(self, mock_abspath): + self.make_env() + + mock_abspath.side_effect = [ + '/home/bootimage.iso', + '/home/bootimage.sig', + ] + + cmd = 'load-import bootimage.iso bootimage.sig' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_list.assert_called_once() + self.mock_load_show.assert_called_once() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + @patch('os.path.isfile', lambda x: True) + def test_load_import_active(self): + self.make_env() + + self.patch_expected['active'] = True + + cmd = ''' + load-import --active + /home/bootimage.iso + /home/bootimage.sig + ''' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_show.assert_called_once() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + self.mock_load_list.assert_not_called() + + @patch('os.path.isfile', lambda x: True) + def test_load_import_active_short_form(self): + self.make_env() + + self.patch_expected['active'] = True + + cmd = ''' + load-import -a + /home/bootimage.iso + /home/bootimage.sig + ''' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_show.assert_called_once() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + self.mock_load_list.assert_not_called() + + @patch('os.path.isfile', lambda x: True) + def test_load_import_local(self): + self.make_env() + + self.patch_expected['local'] = True + + cmd = ''' + load-import --local + /home/bootimage.iso + /home/bootimage.sig + ''' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_list.assert_called_once() + self.mock_load_show.assert_called_once() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + @patch('os.path.isfile', lambda x: True) + def test_load_import_inactive(self): + self.make_env() + + self.patch_expected['inactive'] = True + + cmd = ''' + load-import --inactive + /home/bootimage.iso + /home/bootimage.sig + ''' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_show.assert_called_once() + self.mock_load_list.assert_not_called() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + @patch('os.path.isfile', lambda x: True) + def test_load_import_inactive_short_form(self): + self.make_env() + + self.patch_expected['inactive'] = True + + cmd = ''' + load-import -i + /home/bootimage.iso + /home/bootimage.sig + ''' + self.shell(cmd) + + self.mock_load_import.assert_called_once() + self.mock_load_show.assert_called_once() + self.mock_load_list.assert_not_called() + + self.mock_load_import.assert_called_with(**self.patch_expected) + + @patch('os.path.isfile', lambda x: True) + def test_load_import_max_imported(self): + self.make_env() + + self.mock_load_list.return_value = [ + { + 'id': 1, + 'state': 'ACTIVE', + 'software_version': '5', + }, + { + 'id': 2, + 'state': 'IMPORTED', + 'software_version': '6', + }, + ] + + cmd = 'load-import bootimage.iso bootimage.sig' + self.assertRaises(CommandError, self.shell, cmd) + + self.mock_load_list.assert_called_once() + + self.mock_load_import.assert_not_called() + self.mock_load_show.assert_not_called() + + def test_load_import_invalid_path(self): + self.make_env() + + cmd = 'load-import bootimage.iso bootimage.sig' + self.assertRaises(CommandError, self.shell, cmd) + + self.mock_load_import.assert_not_called() + self.mock_load_list.assert_not_called() + self.mock_load_show.assert_not_called() diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/load.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/load.py index ed973faf0e..e22fed6a25 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/load.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/load.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2022 Wind River Systems, Inc. +# Copyright (c) 2015-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -11,7 +11,8 @@ from cgtsclient import exc CREATION_ATTRIBUTES = ['software_version', 'compatible_version', 'required_patches'] -IMPORT_ATTRIBUTES = ['path_to_iso', 'path_to_sig', 'active', 'local'] +IMPORT_ATTRIBUTES = ['path_to_iso', 'path_to_sig', 'active', 'local', + 'inactive'] class Load(base.Resource): @@ -48,27 +49,33 @@ class LoadManager(base.Manager): def import_load(self, **kwargs): path = '/v1/loads/import_load' - - active = None - local = False + local = kwargs.pop('local') load_info = {} - for (key, value) in kwargs.items(): + + for key, value in kwargs.items(): if key in IMPORT_ATTRIBUTES: - if key == 'active': - active = value - elif key == 'local': - local = value + if isinstance(value, bool): + load_info[key] = str(value).lower() else: load_info[key] = value else: raise exc.InvalidAttribute(key) - if local is True: - load_info['active'] = active + if local: return self._create(path, body=load_info) + data = { + 'active': load_info.pop('active', 'false'), + 'inactive': load_info.pop('inactive', 'false'), + } + json_data = self._upload_multipart( - path, body=load_info, data={'active': active}, check_exceptions=True) + path, + body=load_info, + data=data, + check_exceptions=True, + ) + return self.resource_class(self, json_data) def delete(self, load_id): diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/load_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/load_shell.py index ec9c44ddd1..3c81681f34 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/load_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/load_shell.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015-2022 Wind River Systems, Inc. +# Copyright (c) 2015-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -73,6 +73,11 @@ def do_load_delete(cc, args): help=("Perform an active load import operation. " "Applicable only for SystemController to allow import of " "an active load for subcloud install")) +@utils.arg('-i', '--inactive', + action='store_true', + default=False, + help=("Perform an inactive load import operation. " + "Import a previous release load for subcloud install")) @utils.arg('--local', action='store_true', default=False, @@ -84,6 +89,8 @@ def do_load_import(cc, args): """Import a load.""" local = args.local + active = args.active + inactive = args.inactive # If absolute path is not specified, we assume it is the relative path. # args.isopath will then be set to the absolute path @@ -99,10 +106,7 @@ def do_load_import(cc, args): if not os.path.isfile(args.sigpath): raise exc.CommandError(_("File %s does not exist." % args.sigpath)) - active = None - if args.active is True: - active = 'true' - else: + if not active and not inactive: # The following logic is taken from sysinv api as it takes a while for # this large POST request to reach the server. # @@ -114,8 +118,13 @@ def do_load_import(cc, args): "Max number of loads (2) reached. Please remove the " "old or unused load before importing a new one.")) - patch = {'path_to_iso': args.isopath, 'path_to_sig': args.sigpath, - 'active': active, 'local': local} + patch = { + 'path_to_iso': args.isopath, + 'path_to_sig': args.sigpath, + 'inactive': inactive, + 'active': active, + 'local': local, + } try: print("This operation will take a while. Please wait.")