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 b327ef4..3e0130f 100644 --- a/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py +++ b/distributedcloud-client/dcmanagerclient/api/v1/phased_subcloud_deploy_manager.py @@ -79,3 +79,9 @@ class PhasedSubcloudDeployManager(base.ResourceManager): files = kwargs.get("files") url = BASE_URL + f"{subcloud_ref}/resume" return self._deploy_operation(url, files, data, method="patch") + + def subcloud_deploy_enroll(self, subcloud_ref, **kwargs): + data = kwargs.get("data") + files = kwargs.get("files") + url = BASE_URL + f"{subcloud_ref}/enroll" + 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 91f89b2..d803c9b 100644 --- a/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py +++ b/distributedcloud-client/dcmanagerclient/commands/v1/phased_subcloud_deploy_manager.py @@ -147,13 +147,7 @@ class PhasedSubcloudDeployResume(base.DCManagerShowOne): 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")) + utils.set_sysadmin_password(parsed_args, data) if parsed_args.install_values: if parsed_args.bmc_password: @@ -336,13 +330,7 @@ class InstallPhasedSubcloudDeploy(base.DCManagerShowOne): data = {} # 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")) + utils.set_sysadmin_password(parsed_args, data) if parsed_args.install_values is not None: if not os.path.isfile(parsed_args.install_values): @@ -428,13 +416,7 @@ class BootstrapPhasedSubcloudDeploy(base.DCManagerShowOne): files["bootstrap_values"] = parsed_args.bootstrap_values # 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")) + utils.set_sysadmin_password(parsed_args, data) subcloud_ref = parsed_args.subcloud @@ -488,13 +470,7 @@ class ConfigPhasedSubcloudDeploy(base.DCManagerShowOne): 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")) + utils.set_sysadmin_password(parsed_args, data) try: return phased_subcloud_deploy_manager.subcloud_deploy_config( @@ -537,3 +513,89 @@ class CompletePhasedSubcloudDeploy(base.DCManagerShowOne): f"Unable to complete the deployment of subcloud {subcloud_ref}" ) raise exceptions.DCManagerClientException(error_msg) + + +class EnrollPhasedSubcloudDeploy(base.DCManagerShowOne): + """Enrolls a subcloud.""" + + 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 enroll." + ) + + parser.add_argument( + "--install-values", + required=False, + help="YAML file containing parameters required for the remote " + "install 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( + "--bootstrap-address", + required=False, + help="IP address for initial subcloud controller.", + ) + + parser.add_argument( + "--bootstrap-values", + required=False, + help="YAML file containing the parameters required for the " + "subcloud enrollment.", + ) + + 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.", + ) + + return parser + + def _get_resources(self, parsed_args): + phased_subcloud_deploy_manager = ( + self.app.client_manager.phased_subcloud_deploy_manager + ) + files = {} + data = {} + + 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: " + f"{parsed_args.bootstrap_values}" + ) + raise exceptions.DCManagerClientException(error_msg) + files["bootstrap_values"] = parsed_args.bootstrap_values + + utils.set_sysadmin_password(parsed_args, data) + + subcloud_ref = parsed_args.subcloud + + return phased_subcloud_deploy_manager.subcloud_deploy_enroll( + subcloud_ref, files=files, data=data + ) diff --git a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py index e23287d..9d1a5c9 100644 --- a/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/commands/v1/subcloud_manager.py @@ -264,6 +264,13 @@ class AddSubcloud(base.DCManagerShowOne): "release of the system controller will be used.", ) + parser.add_argument( + "--enroll", + required=False, + action="store_true", + help="Enroll a subcloud", + ) + return parser def _get_resources(self, parsed_args): @@ -303,6 +310,10 @@ class AddSubcloud(base.DCManagerShowOne): raise exceptions.DCManagerClientException(error_msg) files["deploy_config"] = parsed_args.deploy_config + if parsed_args.migrate and parsed_args.enroll: + error_msg = "cannot run migrate and enroll commands together" + raise exceptions.DCManagerClientException(error_msg) + # 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( @@ -327,6 +338,9 @@ class AddSubcloud(base.DCManagerShowOne): if parsed_args.migrate: data["migrate"] = "true" + if parsed_args.enroll: + data["enroll"] = "true" + if parsed_args.release is not None: data["release"] = parsed_args.release diff --git a/distributedcloud-client/dcmanagerclient/shell.py b/distributedcloud-client/dcmanagerclient/shell.py index ffe2299..0b2ffdc 100644 --- a/distributedcloud-client/dcmanagerclient/shell.py +++ b/distributedcloud-client/dcmanagerclient/shell.py @@ -601,6 +601,7 @@ class DCManagerShell(app.App): "subcloud deploy delete": sdm.SubcloudDeployDelete, "subcloud deploy install": psdm.InstallPhasedSubcloudDeploy, "subcloud deploy resume": psdm.PhasedSubcloudDeployResume, + "subcloud deploy enroll": psdm.EnrollPhasedSubcloudDeploy, "subcloud deploy show": sdm.SubcloudDeployShow, "subcloud deploy upload": sdm.SubcloudDeployUpload, "subcloud errors": sm.ShowSubcloudError, 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 c004e11..e68b315 100644 --- a/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py +++ b/distributedcloud-client/dcmanagerclient/tests/v1/test_phased_subcloud_deploy.py @@ -271,3 +271,21 @@ class TestCLIPhasedSubcloudDeployManagerV1(base.BaseCommandTest): ], ) self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST_WITH_PEERID, actual_call[1]) + + def test_subcloud_deploy_enroll(self): + self.client.subcloud_deploy_enroll.return_value = [base.SUBCLOUD_RESOURCE] + + with tempfile.NamedTemporaryFile(mode="w") as bootstrap_file: + bootstrap_file_path = os.path.abspath(bootstrap_file.name) + + actual_call = self.call( + cmd.EnrollPhasedSubcloudDeploy, + app_args=[ + base.ID, + "--bootstrap-address", + base.BOOTSTRAP_ADDRESS, + "--bootstrap-values", + bootstrap_file_path, + ], + ) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST_WITH_PEERID, actual_call[1]) diff --git a/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py b/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py index 87ccc29..d402b25 100644 --- a/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py +++ b/distributedcloud-client/dcmanagerclient/tests/v1/test_subcloud_manager.py @@ -240,6 +240,27 @@ class TestCLISubcloudManagerV1(base.BaseCommandTest): result_list[1] = base.NAME_SC2 self.assertEqual(tuple(result_list), actual_call[1]) + @mock.patch("getpass.getpass", return_value="testpassword") + def test_add_enroll_subcloud(self, _mock_getpass): + self.client.subcloud_manager.add_subcloud.return_value = [ + self.subcloud_resource + ] + + with tempfile.NamedTemporaryFile(mode="w") as f: + yaml.dump(base.FAKE_BOOTSTRAP_VALUES, f) + file_path = os.path.abspath(f.name) + actual_call = self.call( + subcloud_cmd.AddSubcloud, + app_args=[ + "--bootstrap-address", + base.BOOTSTRAP_ADDRESS, + "--bootstrap-values", + file_path, + "--enroll", + ], + ) + self.assertEqual(base.SUBCLOUD_FIELD_RESULT_LIST_WITH_PEERID, actual_call[1]) + def test_rename_subcloud(self): subcloud_renamed = copy.copy(base.SUBCLOUD_RESOURCE_WITH_PEERID) subcloud_renamed.name = base.NAME_SC2 diff --git a/distributedcloud-client/dcmanagerclient/utils.py b/distributedcloud-client/dcmanagerclient/utils.py index 97d3fd2..bd61124 100644 --- a/distributedcloud-client/dcmanagerclient/utils.py +++ b/distributedcloud-client/dcmanagerclient/utils.py @@ -19,6 +19,7 @@ import getpass import json import os +import base64 from urllib import parse, request import yaml @@ -166,3 +167,14 @@ def subcloud_detail_format(subcloud=None): data = (("",) * len(columns),) return columns, data + + +def set_sysadmin_password(parsed_args, data): + + if parsed_args.sysadmin_password: + data["sysadmin_password"] = base64.b64encode( + parsed_args.sysadmin_password.encode("utf-8") + ) + else: + password = prompt_for_password() + data["sysadmin_password"] = base64.b64encode(password.encode("utf-8"))