From 671db8b5619a84396d786236c1ad968c1b64480b Mon Sep 17 00:00:00 2001 From: Victor Romano Date: Fri, 9 Jun 2023 11:57:54 -0300 Subject: [PATCH] Add subcloud deploy resume option to dcmanager This commit adds the command "subcloud deploy resume" to dcmanager client. Usage: dcmanager subcloud deploy resume --sysadmin-password [--install-values ] [--bmc-password ] [--bootstrap-address ] [--bootstrap-values ] [--deploy-config ] [--release ] Test Plan: - PASS: Verify the command works with or without --install-values, --bmc-password, --bootstrap-address, --bootstrap-values, --deploy-config and --release. - PASS: Verify that all provided parameters are successfully passed to the backend. - PASS: Verify that the CLI asks for the sysadmin-password and bmc-password if they are not provided in the command (bmc-password is only prompt if --install-values is also provided). - PASS: Verify that the dcmanager help subcloud deploy resume shows the correct help message containing all options. Depends-on: https://review.opendev.org/c/starlingx/distcloud/+/886104 Story: 2010756 Task: 48317 Change-Id: I8a984626007b43e685f326dd4dd05294cd50fcce Signed-off-by: Victor Romano --- .../api/v1/phased_subcloud_deploy_manager.py | 6 + .../v1/phased_subcloud_deploy_manager.py | 148 ++++++++++++++++-- .../commands/v1/subcloud_manager.py | 20 +-- .../dcmanagerclient/shell.py | 1 + .../tests/v1/test_phased_subcloud_deploy.py | 84 ++++++++++ 5 files changed, 237 insertions(+), 22 deletions(-) diff --git a/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py b/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py index fe081f4..a131bcb 100644 --- a/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py +++ b/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py @@ -68,3 +68,9 @@ class phased_subcloud_deploy_manager(base.ResourceManager): files = kwargs.get('files', {}) url = BASE_URL + "%s/abort" % subcloud_ref return self._deploy_operation(url, files, data, method='patch') + + def subcloud_deploy_resume(self, subcloud_ref, **kwargs): + data = kwargs.get('data') + files = kwargs.get('files') + url = BASE_URL + "%s/resume" % subcloud_ref + return self._deploy_operation(url, files, data, method='patch') diff --git a/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py b/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py index 3aec06c..effb161 100644 --- a/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py +++ b/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py @@ -42,6 +42,130 @@ class AbortPhasedSubcloudDeploy(base.DCManagerShowOne): raise exceptions.DCManagerClientException(error_msg) +class PhasedSubcloudDeployResume(base.DCManagerShowOne): + """Resume the subcloud deployment.""" + + def _get_format_function(self): + return utils.subcloud_detail_format + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to resume deployment.' + ) + + parser.add_argument( + '--bootstrap-address', + required=False, + help='IP address for initial subcloud controller.' + ) + + parser.add_argument( + '--bootstrap-values', + required=False, + help='YAML file containing parameters required for the bootstrap ' + 'of the subcloud.' + ) + + parser.add_argument( + '--deploy-config', + required=False, + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' + ) + + parser.add_argument( + '--install-values', + required=False, + help='YAML file containing parameters required for the remote ' + 'install of the subcloud.' + ) + + parser.add_argument( + '--sysadmin-password', + required=False, + help='sysadmin password of the subcloud to be configured, ' + 'if not provided you will be prompted.' + ) + + parser.add_argument( + '--bmc-password', + required=False, + help='bmc password of the subcloud to be configured, ' + 'if not provided you will be prompted. This parameter is only' + ' valid if the --install-values are specified.' + ) + + parser.add_argument( + '--release', + required=False, + help='software release used to install, bootstrap and/or deploy ' + 'the subcloud with. If not specified, the current software ' + 'release of the system controller will be used.' + ) + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.\ + phased_subcloud_deploy_manager.phased_subcloud_deploy_manager + files = dict() + data = dict() + + if parsed_args.bootstrap_address: + data['bootstrap-address'] = parsed_args.bootstrap_address + + # Get the bootstrap values yaml file + if parsed_args.bootstrap_values: + if not os.path.isfile(parsed_args.bootstrap_values): + error_msg = "bootstrap-values does not exist: %s" % \ + parsed_args.bootstrap_values + raise exceptions.DCManagerClientException(error_msg) + files['bootstrap_values'] = parsed_args.bootstrap_values + + # Get the install values yaml file + if parsed_args.install_values: + if not os.path.isfile(parsed_args.install_values): + error_msg = "install-values does not exist: %s" % \ + parsed_args.install_values + raise exceptions.DCManagerClientException(error_msg) + files['install_values'] = parsed_args.install_values + + # Get the deploy config yaml file + if parsed_args.deploy_config: + if not os.path.isfile(parsed_args.deploy_config): + error_msg = "deploy-config does not exist: %s" % \ + parsed_args.deploy_config + raise exceptions.DCManagerClientException(error_msg) + files['deploy_config'] = parsed_args.deploy_config + + # Prompt the user for the subcloud's password if it isn't provided + if parsed_args.sysadmin_password: + data['sysadmin_password'] = base64.b64encode( + parsed_args.sysadmin_password.encode("utf-8")) + else: + password = utils.prompt_for_password() + data["sysadmin_password"] = base64.b64encode( + password.encode("utf-8")) + + if parsed_args.install_values: + if parsed_args.bmc_password: + data['bmc_password'] = base64.b64encode( + parsed_args.bmc_password.encode("utf-8")) + else: + password = utils.prompt_for_password('bmc') + data["bmc_password"] = base64.b64encode( + password.encode("utf-8")) + + if parsed_args.release: + data['release'] = parsed_args.release + + return dcmanager_client.subcloud_deploy_resume( + subcloud_ref=subcloud_ref, files=files, data=data) + + class CreatePhasedSubcloudDeploy(base.DCManagerShowOne): """Creates a new subcloud.""" @@ -60,22 +184,22 @@ class CreatePhasedSubcloudDeploy(base.DCManagerShowOne): parser.add_argument( '--bootstrap-values', required=True, - help='YAML file containing subcloud configuration settings. ' - 'Can be either a local file path or a URL.' + help='YAML file containing parameters required for the bootstrap ' + 'of the subcloud.' ) parser.add_argument( '--deploy-config', required=False, - help='YAML file containing subcloud variables to be passed to the ' - 'deploy playbook.' + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' ) parser.add_argument( '--install-values', required=False, - help='YAML file containing parameters required for the ' - 'remote install of the subcloud.' + help='YAML file containing parameters required for the remote ' + 'install of the subcloud.' ) parser.add_argument( @@ -167,8 +291,8 @@ class InstallPhasedSubcloudDeploy(base.DCManagerShowOne): parser.add_argument( '--install-values', required=False, - help='YAML file containing parameters required for the ' - 'remote install of the subcloud.' + help='YAML file containing parameters required for the remote ' + 'install of the subcloud.' ) parser.add_argument( @@ -260,8 +384,8 @@ class BootstrapPhasedSubcloudDeploy(base.DCManagerShowOne): parser.add_argument( '--bootstrap-values', required=False, - help='YAML file containing subcloud configuration settings. ' - 'Can be either a local file path or a URL.' + help='YAML file containing parameters required for the bootstrap ' + 'of the subcloud.' ) parser.add_argument( @@ -322,8 +446,8 @@ class ConfigPhasedSubcloudDeploy(base.DCManagerShowOne): parser.add_argument( '--deploy-config', required=False, - help='YAML file containing subcloud variables to be passed to the ' - 'deploy playbook.' + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' ) parser.add_argument( diff --git a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py index 62d9f37..31fee10 100644 --- a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py @@ -141,15 +141,15 @@ class AddSubcloud(base.DCManagerShowOne): parser.add_argument( '--bootstrap-values', required=True, - help='YAML file containing subcloud configuration settings. ' - 'Can be either a local file path or a URL.' + help='YAML file containing parameters required for the bootstrap ' + 'of the subcloud.' ) parser.add_argument( '--deploy-config', required=False, - help='YAML file containing subcloud variables to be passed to the ' - 'deploy playbook.' + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' ) parser.add_argument( @@ -599,8 +599,8 @@ class ReconfigSubcloud(base.DCManagerShowOne): parser.add_argument( '--deploy-config', required=True, - help='YAML file containing subcloud variables to be passed to the ' - 'deploy playbook.' + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' ) parser.add_argument( @@ -661,15 +661,15 @@ class ReinstallSubcloud(base.DCManagerShowOne): parser.add_argument( '--bootstrap-values', required=True, - help='YAML file containing subcloud configuration settings. ' - 'Can be either a local file path or a URL.' + help='YAML file containing parameters required for the bootstrap ' + 'of the subcloud.' ) parser.add_argument( '--deploy-config', required=False, - help='YAML file containing subcloud variables to be passed to the ' - 'deploy playbook.' + help='YAML file containing parameters required for the initial ' + 'configuration and unlock of the subcloud.' ) parser.add_argument( diff --git a/distributedcloud-client/dcmanagerclient/shell.py b/distributedcloud-client/dcmanagerclient/shell.py index 4258a37..eac6e80 100644 --- a/distributedcloud-client/dcmanagerclient/shell.py +++ b/distributedcloud-client/dcmanagerclient/shell.py @@ -549,6 +549,7 @@ class DCManagerShell(app.App): 'subcloud deploy bootstrap': psdm.BootstrapPhasedSubcloudDeploy, 'subcloud deploy config': psdm.ConfigPhasedSubcloudDeploy, 'subcloud deploy install': psdm.InstallPhasedSubcloudDeploy, + 'subcloud deploy resume': psdm.PhasedSubcloudDeployResume, 'subcloud deploy upload': sdm.SubcloudDeployUpload, 'subcloud deploy show': sdm.SubcloudDeployShow, 'subcloud-deploy upload': sdm.DeprecatedSubcloudDeployUpload, diff --git a/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py b/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py index 8bc3858..394e686 100644 --- a/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py +++ b/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py @@ -145,3 +145,87 @@ class TestCLIPhasedSubcloudDeployManagerV1(base.BaseCommandTest): cmd.AbortPhasedSubcloudDeploy, app_args=[base.NAME]) self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST, actual_call[1]) + + def test_subcloud_deploy_resume_all_parameters(self): + self.client.subcloud_deploy_resume.return_value = [ + base.SUBCLOUD_RESOURCE] + + with tempfile.NamedTemporaryFile(mode='w') as bootstrap_file,\ + tempfile.NamedTemporaryFile(mode='w') as config_file,\ + tempfile.NamedTemporaryFile(mode='w') as install_file: + + bootstrap_file_path = os.path.abspath(bootstrap_file.name) + config_file_path = os.path.abspath(config_file.name) + install_file_path = os.path.abspath(install_file.name) + + actual_call = self.call( + cmd.PhasedSubcloudDeployResume, app_args=[ + base.NAME, + '--bootstrap-address', base.BOOTSTRAP_ADDRESS, + '--bootstrap-values', bootstrap_file_path, + '--install-values', install_file_path, + '--deploy-config', config_file_path, + '--release', base.SOFTWARE_VERSION, + ]) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST, actual_call[1]) + + def test_subcloud_deploy_resume_missing_files(self): + self.client.subcloud_deploy_resume.return_value = [ + base.SUBCLOUD_RESOURCE] + + with tempfile.NamedTemporaryFile(mode='w') as bootstrap_file,\ + tempfile.NamedTemporaryFile(mode='w') as config_file,\ + tempfile.NamedTemporaryFile(mode='w') as install_file: + + bootstrap_file_path = os.path.abspath(bootstrap_file.name) + config_file_path = os.path.abspath(config_file.name) + install_file_path = os.path.abspath(install_file.name) + + # Missing bootstrap values + app_args_bootstrap = [base.NAME, + '--bootstrap-address', base.BOOTSTRAP_ADDRESS, + '--bootstrap-values', bootstrap_file_path] + error_msg_bootstrap = 'bootstrap-values does not exist' + call_bootstrap = self.assertRaises(DCManagerClientException, + self.call, + cmd.PhasedSubcloudDeployResume, + app_args=app_args_bootstrap) + self.assertTrue(error_msg_bootstrap in str(call_bootstrap)) + + # Missing install values + app_args_install = [base.NAME, '--install-values', install_file_path] + error_msg_install = 'install-values does not exist' + call_install = self.assertRaises(DCManagerClientException, + self.call, + cmd.PhasedSubcloudDeployResume, + app_args=app_args_install) + self.assertTrue(error_msg_install in str(call_install)) + + # Missing deploy config values + app_args_config = [base.NAME, '--deploy-config', config_file_path] + error_msg_config = 'deploy-config does not exist' + call_config = self.assertRaises(DCManagerClientException, + self.call, + cmd.PhasedSubcloudDeployResume, + app_args=app_args_config) + self.assertTrue(error_msg_config in str(call_config)) + + def test_subcloud_deploy_resume_no_parameters(self): + self.client.subcloud_deploy_resume.return_value = [ + base.SUBCLOUD_RESOURCE] + + actual_call = self.call( + cmd.PhasedSubcloudDeployResume, + app_args=[base.NAME]) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST, actual_call[1]) + + def test_subcloud_deploy_resume_no_files_only_release(self): + self.client.subcloud_deploy_resume.return_value = [ + base.SUBCLOUD_RESOURCE] + + actual_call = self.call( + cmd.PhasedSubcloudDeployResume, app_args=[ + base.NAME, + '--release', base.SOFTWARE_VERSION, + ]) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST, actual_call[1])