From 92e506dd72db095f27676652656bf787b2fbe1ca Mon Sep 17 00:00:00 2001 From: Victor Romano Date: Fri, 21 Jul 2023 09:33:03 -0300 Subject: [PATCH] Add subcloud redeploy option to dcmanager This commit adds the command "subcloud redeploy" to dcmanager client. Usage: dcmanager subcloud redeploy --sysadmin-password [--install-values ] [--bmc-password ] [--bootstrap-values ] [--deploy-config ] [--release ] Test Plan: - PASS: Verify the command works with or without --install-values, --bmc-password, --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/+/889975 Story: 2010756 Task: 48497 Change-Id: I16857f6779205c4dfcc6d255e0aabdd83496f4c7 Signed-off-by: Victor Romano --- .../api/v1/subcloud_manager.py | 21 +++ .../commands/v1/subcloud_manager.py | 128 ++++++++++++++++++ .../dcmanagerclient/shell.py | 1 + .../tests/v1/test_subcloud_manager.py | 65 +++++++++ 4 files changed, 215 insertions(+) diff --git a/distributedcloud-client/dcmanagerclient/api/v1/subcloud_manager.py b/distributedcloud-client/dcmanagerclient/api/v1/subcloud_manager.py index 9e3f9f6..114faaa 100644 --- a/distributedcloud-client/dcmanagerclient/api/v1/subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/api/v1/subcloud_manager.py @@ -90,6 +90,21 @@ class subcloud_manager(base.ResourceManager): resource.append(self.json_to_resource(json_object)) return resource + def subcloud_redeploy(self, url, body, data): + fields = dict() + for k, v in body.items(): + fields.update({k: (v, open(v, 'rb'),)}) + fields.update(data) + enc = MultipartEncoder(fields=fields) + headers = {'content-type': enc.content_type} + resp = self.http_client.patch(url, enc, headers=headers) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append(self.json_to_resource(json_object)) + return resource + def _subcloud_prestage(self, url, data): data = json.dumps(data) resp = self.http_client.patch(url, data) @@ -169,3 +184,9 @@ class subcloud_manager(base.ResourceManager): data = kwargs.get('data') url = '/subclouds/%s/reinstall' % subcloud_ref return self.subcloud_reinstall(url, files, data) + + def redeploy_subcloud(self, subcloud_ref, **kwargs): + files = kwargs.get('files') + data = kwargs.get('data') + url = '/subclouds/%s/redeploy' % subcloud_ref + return self.subcloud_redeploy(url, files, data) diff --git a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py index 31fee10..e244401 100644 --- a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py @@ -739,6 +739,134 @@ class ReinstallSubcloud(base.DCManagerShowOne): raise exceptions.DCManagerClientException(msg) +class RedeploySubcloud(base.DCManagerShowOne): + """Redeploy a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(RedeploySubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to redeploy.' + ) + + parser.add_argument( + '--install-values', + required=False, + help='YAML file containing parameters required for the ' + 'remote install of the subcloud.' + ) + + parser.add_argument( + '--bootstrap-values', + required=False, + help='YAML file containing subcloud configuration settings. ' + 'Can be either a local file path or a URL.' + ) + + parser.add_argument( + '--deploy-config', + required=False, + help='YAML file containing subcloud variables to be passed to the ' + 'deploy playbook.' + ) + + 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.subcloud_manager + files = dict() + data = dict() + + # Get the install values yaml file + if parsed_args.install_values is not None: + 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 bootstrap values yaml file + if parsed_args.bootstrap_values is not None: + 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 deploy config yaml file + if parsed_args.deploy_config is not None: + 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 is not None: + 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 is not None: + data['release'] = parsed_args.release + + # Require user to type redeploy to confirm + print("WARNING: This will redeploy the subcloud. " + "All applications and data on the subcloud will be lost.") + confirm = six.moves.input( + "Please type \"redeploy\" to confirm: ").strip().lower() + if confirm == 'redeploy': + try: + return dcmanager_client.subcloud_manager.redeploy_subcloud( + subcloud_ref=subcloud_ref, files=files, data=data) + except Exception as e: + print(e) + error_msg = "Unable to redeploy subcloud %s" % (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + else: + msg = "Subcloud %s will not be redeployed" % (subcloud_ref) + raise exceptions.DCManagerClientException(msg) + + class RestoreSubcloud(base.DCManagerShowOne): """Restore a subcloud.""" diff --git a/distributedcloud-client/dcmanagerclient/shell.py b/distributedcloud-client/dcmanagerclient/shell.py index eac6e80..a7b9f2c 100644 --- a/distributedcloud-client/dcmanagerclient/shell.py +++ b/distributedcloud-client/dcmanagerclient/shell.py @@ -533,6 +533,7 @@ class DCManagerShell(app.App): 'subcloud update': sm.UpdateSubcloud, 'subcloud reconfig': sm.ReconfigSubcloud, 'subcloud reinstall': sm.ReinstallSubcloud, + 'subcloud redeploy': sm.RedeploySubcloud, 'subcloud restore': sm.RestoreSubcloud, 'subcloud prestage': sm.PrestageSubcloud, 'subcloud-backup create': sbm.CreateSubcloudBackup, diff --git a/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py b/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py index 90c601e..9253f2a 100644 --- a/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py @@ -266,6 +266,71 @@ class TestCLISubcloudManagerV1(base.BaseCommandTest): self.assertTrue('bootstrap-values does not exist' in str(e)) + @mock.patch('getpass.getpass', return_value='testpassword') + @mock.patch('six.moves.input', return_value='redeploy') + def test_redeploy_subcloud(self, mock_input, getpass): + self.client.subcloud_manager.redeploy_subcloud. \ + 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( + subcloud_cmd.RedeploySubcloud, app_args=[ + base.NAME, + '--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]) + + @mock.patch('getpass.getpass', return_value='testpassword') + @mock.patch('six.moves.input', return_value='redeploy') + def test_redeploy_subcloud_no_parameters(self, mock_input, getpass): + self.client.subcloud_manager.redeploy_subcloud.\ + return_value = [base.SUBCLOUD_RESOURCE] + actual_call = self.call( + subcloud_cmd.RedeploySubcloud, + app_args=[base.ID]) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST, actual_call[1]) + + @mock.patch('getpass.getpass', return_value='testpassword') + @mock.patch('six.moves.input', return_value='redeploy') + def test_redeploy_bootstrap_files_does_not_exists( + self, mock_input, getpass): + self.client.subcloud_manager.redeploy_subcloud.\ + 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) + + app_args_install = [base.NAME, + '--install-values', install_file_path] + app_args_bootstrap = [base.NAME, + '--bootstrap-values', bootstrap_file_path] + app_args_config = [base.NAME, '--deploy-config', config_file_path] + args_dict = {'install-values': app_args_install, + 'bootstrap-values': app_args_bootstrap, + 'deploy-config': app_args_config} + + for file in ['install-values', 'bootstrap-values', + 'deploy-config']: + e = self.assertRaises(DCManagerClientException, + self.call, + subcloud_cmd.RedeploySubcloud, + app_args=args_dict[file]) + self.assertTrue(f'{file} does not exist' in str(e)) + @mock.patch('getpass.getpass', return_value='testpassword') def test_restore_subcloud(self, getpass): with tempfile.NamedTemporaryFile() as f: