diff --git a/src/ssh/azext_ssh/_params.py b/src/ssh/azext_ssh/_params.py index ce3ad0b4dd5..b709be17489 100644 --- a/src/ssh/azext_ssh/_params.py +++ b/src/ssh/azext_ssh/_params.py @@ -22,7 +22,7 @@ def load_arguments(self, _): c.argument('port', options_list=['--port'], help='SSH port') c.argument('ssh_client_path', options_list=['--ssh-client-path'], help='Path to ssh executable. Default to ssh pre-installed if not provided.') - c.argument('delete_privkey', options_list=['--delete-private-key'], + c.argument('delete_credentials', options_list=['--delete-private-key'], help=('This is an internal argument. This argument is used by Azure Portal to provide a one click ' 'SSH login experience in Cloud shell.'), deprecate_info=c.deprecate(hide=True), action='store_true') @@ -41,13 +41,17 @@ def load_arguments(self, _): help='The username for a local user') c.argument('overwrite', action='store_true', options_list=['--overwrite'], help='Overwrites the config file if this flag is set') + c.argument('credentials_folder', options_list=['--keys-destination-folder', '--keys-dest-folder'], + help='Folder where new generated keys will be stored.') c.argument('port', options_list=['--port'], help='Port to connect to on the remote host.') c.argument('cert_file', options_list=['--certificate-file', '-c'], help='Path to certificate file') with self.argument_context('ssh cert') as c: c.argument('cert_path', options_list=['--file', '-f'], help='The file path to write the SSH cert to, defaults to public key path with -aadcert.pub appened') - c.argument('public_key_file', options_list=['--public-key-file', '-p'], help='The RSA public key file path') + c.argument('public_key_file', options_list=['--public-key-file', '-p'], + help='The RSA public key file path. If not provided, ' + 'generated key pair is stored in the same directory as --file.') with self.argument_context('ssh arc') as c: c.argument('vm_name', options_list=['--vm-name', '--name', '-n'], help='The name of the Arc Server') @@ -60,7 +64,7 @@ def load_arguments(self, _): c.argument('port', options_list=['--port'], help='Port to connect to on the remote host.') c.argument('ssh_client_path', options_list=['--ssh-client-path'], help='Path to ssh executable. Default to ssh pre-installed if not provided.') - c.argument('delete_privkey', options_list=['--delete-private-key'], + c.argument('delete_credentials', options_list=['--delete-private-key'], help=('This is an internal argument. This argument is used by Azure Portal to provide a one click ' 'SSH login experience in Cloud shell.'), deprecate_info=c.deprecate(hide=True), action='store_true') diff --git a/src/ssh/azext_ssh/constants.py b/src/ssh/azext_ssh/constants.py index a52b8019402..a7e30605597 100644 --- a/src/ssh/azext_ssh/constants.py +++ b/src/ssh/azext_ssh/constants.py @@ -8,3 +8,4 @@ CLIENT_PROXY_STORAGE_URL = "https://sshproxysa.blob.core.windows.net" CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS = 120 CLEANUP_TIME_INTERVAL_IN_SECONDS = 10 +CLEANUP_AWAIT_TERMINATION_IN_SECONDS = 30 \ No newline at end of file diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index b89adb3a79e..75d10ed2a04 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -13,6 +13,7 @@ import stat from glob import glob +from knack import log from azure.cli.core import azclierror from msrestazure import tools @@ -22,38 +23,75 @@ from . import constants as consts from . import file_utils +logger = log.get_logger(__name__) def ssh_vm(cmd, resource_group_name=None, vm_name=None, resource_id=None, ssh_ip=None, public_key_file=None, private_key_file=None, use_private_ip=False, local_user=None, cert_file=None, port=None, - ssh_client_path=None, delete_privkey=False, ssh_args=None): + ssh_client_path=None, delete_credentials=False, ssh_args=None): + + if delete_credentials and os.environ.get("AZUREPS_HOST_ENVIRONMENT") != "cloud-shell/1.0": + raise azclierror.ArgumentUsageError("Can't use --delete-private-key outside an Azure Cloud Shell session.") _assert_args(resource_group_name, vm_name, ssh_ip, resource_id, cert_file, local_user) + credentials_folder = None do_ssh_op = _decide_op_call(cmd, resource_group_name, vm_name, resource_id, ssh_ip, None, None, - ssh_client_path, ssh_args, delete_privkey) + ssh_client_path, ssh_args, delete_credentials, credentials_folder) do_ssh_op(cmd, ssh_ip, public_key_file, private_key_file, local_user, - cert_file, port, use_private_ip) + cert_file, port, use_private_ip, credentials_folder) def ssh_config(cmd, config_path, resource_group_name=None, vm_name=None, ssh_ip=None, resource_id=None, public_key_file=None, private_key_file=None, overwrite=False, use_private_ip=False, - local_user=None, cert_file=None, port=None): + local_user=None, cert_file=None, port=None, credentials_folder=None): _assert_args(resource_group_name, vm_name, ssh_ip, resource_id, cert_file, local_user) + + if (public_key_file or private_key_file) and credentials_folder: + raise azclierror.ArgumentUsageError("--keys-destination-folder can't be used in conjunction with " + "--public-key-file/-p or --private-key-file/-i.") + + # Default credential location + if not credentials_folder: + config_folder = os.path.dirname(config_path) + if not os.path.isdir(config_folder): + raise azclierror.InvalidArgumentValueError(f"Config file destination folder {config_folder} " + "does not exist.") + folder_name = ssh_ip + if resource_group_name and vm_name: + folder_name = resource_group_name + "-" + vm_name + elif resource_id: + resource_info = tools.parse_resource_id(resource_id) + folder_name = resource_info['resource_group'] + "-" + resource_info['resource_name'] + + credentials_folder = os.path.join(config_folder, os.path.join("az_ssh_config", folder_name)) + do_ssh_op = _decide_op_call(cmd, resource_group_name, vm_name, resource_id, ssh_ip, config_path, overwrite, - None, None, None) + None, None, False, credentials_folder) do_ssh_op(cmd, ssh_ip, public_key_file, private_key_file, local_user, - cert_file, port, use_private_ip) + cert_file, port, use_private_ip, credentials_folder) def ssh_cert(cmd, cert_path=None, public_key_file=None): - public_key_file, _ = _check_or_create_public_private_files(public_key_file, None) + if not cert_path and not public_key_file: + raise azclierror.RequiredArgumentMissingError("--file or --public-key-file must be provided.") + if cert_path and not os.path.isdir(os.path.dirname(cert_path)): + raise azclierror.InvalidArgumentValueError(f"{os.path.dirname(cert_path)} folder doesn't exist") + # If user doesn't provide a public key, save generated key pair to the same folder as --file + keys_folder = None + if not public_key_file: + keys_folder = os.path.dirname(cert_path) + logger.warning("The generated SSH keys are stored at %s. Please delete SSH keys when the certificate " + "is no longer being used.", keys_folder) + public_key_file, _, _ = _check_or_create_public_private_files(public_key_file, None, keys_folder) cert_file, _ = _get_and_write_certificate(cmd, public_key_file, cert_path) print(cert_file + "\n") def ssh_arc(cmd, resource_group_name=None, vm_name=None, resource_id=None, public_key_file=None, private_key_file=None, - local_user=None, cert_file=None, port=None, ssh_client_path=None, delete_privkey=False, ssh_args=None): - + local_user=None, cert_file=None, port=None, ssh_client_path=None, delete_credentials=False, ssh_args=None): + + if delete_credentials and os.environ.get("AZUREPS_HOST_ENVIRONMENT") != "cloud-shell/1.0": + raise azclierror.ArgumentUsageError("Can't use --delete-private-key outside an Azure Cloud Shell session.") _assert_args(resource_group_name, vm_name, None, resource_id, cert_file, local_user) if resource_id: @@ -66,14 +104,16 @@ def ssh_arc(cmd, resource_group_name=None, vm_name=None, resource_id=None, publi resource_group_name = resource_info['resource_group'] vm_name = resource_info['resource_name'] + credentials_folder = None + op_call = functools.partial(ssh_utils.start_ssh_connection, ssh_client_path=ssh_client_path, ssh_args=ssh_args, - delete_privkey=delete_privkey) + delete_credentials=delete_credentials) _do_ssh_op(cmd, None, public_key_file, private_key_file, local_user, cert_file, port, - False, resource_group_name, vm_name, op_call, True) + False, credentials_folder, resource_group_name, vm_name, op_call, True) def _do_ssh_op(cmd, ssh_ip, public_key_file, private_key_file, username, - cert_file, port, use_private_ip, resource_group_name, vm_name, op_call, is_arc): + cert_file, port, use_private_ip, credentials_folder, resource_group_name, vm_name, op_call, is_arc): proxy_path = None relay_info = None @@ -87,12 +127,18 @@ def _do_ssh_op(cmd, ssh_ip, public_key_file, private_key_file, username, raise azclierror.ResourceNotFoundError(f"VM '{vm_name}' does not have a public IP address to SSH to") raise azclierror.ResourceNotFoundError(f"VM '{vm_name}' does not have a public or private IP address to" "SSH to") - + + # If user provides local user, no credentials should be deleted. + delete_keys = False + delete_cert = False if not username: - public_key_file, private_key_file = _check_or_create_public_private_files(public_key_file, private_key_file) + delete_cert = True + public_key_file, private_key_file, delete_keys = _check_or_create_public_private_files(public_key_file, + private_key_file, + credentials_folder) cert_file, username = _get_and_write_certificate(cmd, public_key_file, None) - op_call(relay_info, proxy_path, vm_name, ssh_ip, username, cert_file, private_key_file, port, is_arc) + op_call(relay_info, proxy_path, vm_name, ssh_ip, username, cert_file, private_key_file, port, is_arc, delete_keys, delete_cert, public_key_file) def _get_and_write_certificate(cmd, public_key_file, cert_file): @@ -181,12 +227,23 @@ def _assert_args(resource_group, vm_name, ssh_ip, resource_id, cert_file, userna raise azclierror.FileOperationError(f"Certificate file {cert_file} not found") -def _check_or_create_public_private_files(public_key_file, private_key_file): +def _check_or_create_public_private_files(public_key_file, private_key_file, credentials_folder): + delete_keys = False # If nothing is passed in create a temporary directory with a ephemeral keypair if not public_key_file and not private_key_file: - temp_dir = tempfile.mkdtemp(prefix="aadsshcert") - public_key_file = os.path.join(temp_dir, "id_rsa.pub") - private_key_file = os.path.join(temp_dir, "id_rsa") + # We only want to delete the keys if the user hasn't provided their own keys + # Only ssh vm deletes generated keys. + delete_keys = True + if not credentials_folder: + # az ssh vm: Create keys on temp folder and delete folder once connection succeeds/fails. + credentials_folder = tempfile.mkdtemp(prefix="aadsshcert") + else: + # az ssh config: Keys saved to the same folder as --file or to --keys-destination-folder. + # az ssh cert: Keys saved to the same folder as --file. + if not os.path.isdir(credentials_folder): + os.makedirs(credentials_folder) + public_key_file = os.path.join(credentials_folder, "id_rsa.pub") + private_key_file = os.path.join(credentials_folder, "id_rsa") ssh_utils.create_ssh_keyfile(private_key_file) if not public_key_file: @@ -204,7 +261,7 @@ def _check_or_create_public_private_files(public_key_file, private_key_file): if not os.path.isfile(private_key_file): raise azclierror.FileOperationError(f"Private key file {private_key_file} not found") - return public_key_file, private_key_file + return public_key_file, private_key_file, delete_keys def _write_cert_file(certificate_contents, cert_file): @@ -319,7 +376,7 @@ def _arc_list_access_details(cmd, resource_group, vm_name): def _decide_op_call(cmd, resource_group_name, vm_name, resource_id, ssh_ip, config_path, overwrite, - ssh_client_path, ssh_args, delete_privkey): + ssh_client_path, ssh_args, delete_credentials, credentials_folder): # If the user provides an IP address the target will be treated as an Azure VM even if it is an # Arc Server. Which just means that the Connectivity Proxy won't be used to establish connection. @@ -353,17 +410,17 @@ def _decide_op_call(cmd, resource_group_name, vm_name, resource_id, ssh_ip, conf from azure.core.exceptions import ResourceNotFoundError if isinstance(arc_error, ResourceNotFoundError) and isinstance(vm_error, ResourceNotFoundError): raise azclierror.ResourceNotFoundError(f"The resource {vm_name} in the resource group " - "{resource_group_name} was not found. Erros:\n" + f"{resource_group_name} was not found. Erros:\n" f"{str(arc_error)}\n{str(vm_error)}") raise azclierror.BadRequestError("Unable to determine the target machine type as Azure VM or " f"Arc Server. Errors:\n{str(arc_error)}\n{str(vm_error)}") if config_path: op_call = functools.partial(ssh_utils.write_ssh_config, config_path=config_path, overwrite=overwrite, - resource_group=resource_group_name) + resource_group=resource_group_name, credentials_folder=credentials_folder) else: op_call = functools.partial(ssh_utils.start_ssh_connection, ssh_client_path=ssh_client_path, ssh_args=ssh_args, - delete_privkey=delete_privkey) + delete_credentials=delete_credentials) do_ssh_op = functools.partial(_do_ssh_op, resource_group_name=resource_group_name, vm_name=vm_name, is_arc=is_arc_server, op_call=op_call) diff --git a/src/ssh/azext_ssh/file_utils.py b/src/ssh/azext_ssh/file_utils.py index f3ecebf8b7f..1e320606b9d 100644 --- a/src/ssh/azext_ssh/file_utils.py +++ b/src/ssh/azext_ssh/file_utils.py @@ -27,13 +27,26 @@ def mkdir_p(path): def delete_file(file_path, message, warning=False): - try: - os.remove(file_path) - except Exception as e: - if warning: - logger.warning(message) - else: - raise azclierror.FileOperationError(message + "Error: " + str(e)) from e + if os.path.isfile(file_path): + try: + os.remove(file_path) + except Exception as e: + if warning: + logger.warning(message) + else: + raise azclierror.FileOperationError(message + "Error: " + str(e)) from e + + + +def delete_folder(dir_path, message, warning=False): + if os.path.isdir(dir_path): + try: + os.rmdir(dir_path) + except Exception as e: + if warning: + logger.warning(message) + else: + raise azclierror.FileOperationError(message + "Error: " + str(e)) from e def create_directory(file_path, error_message): diff --git a/src/ssh/azext_ssh/ssh_utils.py b/src/ssh/azext_ssh/ssh_utils.py index f1f221aa172..629690df8b8 100644 --- a/src/ssh/azext_ssh/ssh_utils.py +++ b/src/ssh/azext_ssh/ssh_utils.py @@ -22,34 +22,17 @@ def start_ssh_connection(relay_info, proxy_path, vm_name, ip, username, cert_file, private_key_file, port, - is_arc, ssh_client_path, ssh_args, delete_privkey): + is_arc, delete_keys, delete_cert, public_key_file, ssh_client_path, ssh_args, delete_credentials): if not ssh_client_path: ssh_client_path = _get_ssh_path() + ssh_arg_list = [] if ssh_args: ssh_arg_list = ssh_args + env = os.environ.copy() - ssh_client_log_file_arg = [] - # delete_privkey is only true for injected commands in the portal one click ssh experience - if delete_privkey and (cert_file or private_key_file): - if '-E' in ssh_arg_list: - # This condition should rarely be true - index = ssh_arg_list.index('-E') - log_file = ssh_arg_list[index + 1] - else: - if cert_file: - log_dir = os.path.dirname(cert_file) - elif private_key_file: - log_dir = os.path.dirname(private_key_file) - log_file_name = 'ssh_client_log_' + str(os.getpid()) - log_file = os.path.join(log_dir, log_file_name) - ssh_client_log_file_arg = ['-E', log_file] - - if '-v' not in ssh_arg_list and '-vv' not in ssh_arg_list and '-vvv' not in ssh_arg_list: - ssh_client_log_file_arg = ssh_client_log_file_arg + ['-v'] - if is_arc: env['SSHPROXY_RELAY_INFO'] = relay_info if port: @@ -62,28 +45,50 @@ def start_ssh_connection(relay_info, proxy_path, vm_name, ip, username, cert_fil host = _get_host(username, ip) args = _build_args(cert_file, private_key_file, port) - command = [ssh_client_path, host] - command = command + args + ssh_client_log_file_arg + ssh_arg_list - - # If delete_privkey flag is true, we will try to clean the private key file and the certificate file - # once the connection has been established. If it's not possible to open the log file, we default to - # waiting for about 2 minutes once the ssh process starts before cleaning up the files. - if delete_privkey and (cert_file or private_key_file): - if os.path.isfile(log_file): - file_utils.delete_file(log_file, f"Couldn't delete existing log file {log_file}", True) - cleanup_process = mp.Process(target=_do_cleanup, args=(private_key_file, cert_file, log_file)) + if not cert_file and not private_key_file: + # In this case, even if delete_credentials is true, there is nothing to clean-up. + delete_credentials = False + + log_file = None + if delete_keys or delete_cert or delete_credentials: + if '-E' not in ssh_arg_list and set(['-v', '-vv', '-vvv']).isdisjoint(ssh_arg_list): + # If the user either provides his own client log file (-E) or + # wants the client log messages to be printed to the console (-vvv/-vv/-v), + # we should not use the log files to check for connection success. + if cert_file: + log_dir = os.path.dirname(cert_file) + elif private_key_file: + log_dir = os.path.dirname(private_key_file) + log_file_name = 'ssh_client_log_' + str(os.getpid()) + log_file = os.path.join(log_dir, log_file_name) + ssh_arg_list = ssh_arg_list + ['-E', log_file, '-v'] + # Create a new process that will wait until the connection is established and then delete keys. + cleanup_process = mp.Process(target=_do_cleanup, args=(delete_keys or delete_credentials, delete_cert or delete_credentials, + cert_file, private_key_file, public_key_file, log_file, True)) cleanup_process.start() + command = [ssh_client_path, host] + command = command + args + ssh_arg_list + logger.debug("Running ssh command %s", ' '.join(command)) subprocess.call(command, shell=platform.system() == 'Windows', env=env) - # If the cleanup process is still alive once the ssh process is terminated, we terminate it and make - # sure the private key and certificate are deleted. - if delete_privkey and (cert_file or private_key_file): + if delete_keys or delete_cert or delete_credentials: if cleanup_process.is_alive(): cleanup_process.terminate() - time.sleep(1) - _do_cleanup(private_key_file, cert_file) + # wait for process to terminate + t0 = time.time() + while cleanup_process.is_alive() and (time.time() - t0) < const.CLEANUP_AWAIT_TERMINATION_IN_SECONDS: + time.sleep(1) + + # Make sure all files have been properly removed. + _do_cleanup(delete_keys or delete_credentials, delete_cert or delete_credentials, cert_file, private_key_file, public_key_file) + if log_file: + file_utils.delete_file(log_file, f"Couldn't delete temporary log file {log_file}. ", True) + if delete_keys: + # This is only true if keys were generated, so they must be in a temp folder. + temp_dir = os.path.dirname(cert_file) + file_utils.delete_folder(temp_dir, f"Couldn't delete temporary folder {temp_dir}", True) def create_ssh_keyfile(private_key_file): @@ -114,8 +119,18 @@ def get_ssh_cert_principals(cert_file): return principals +def get_ssh_cert_validity(cert_file): + if cert_file: + info = get_ssh_cert_info(cert_file) + for line in info: + if "Valid:" in line: + return line.strip() + return None + + def write_ssh_config(relay_info, proxy_path, vm_name, ip, username, - cert_file, private_key_file, port, is_arc, config_path, overwrite, resource_group): + cert_file, private_key_file, port, is_arc, delete_keys, delete_cert, _, + config_path, overwrite, resource_group, credentials_folder): common_lines = [] common_lines.append("\tUser " + username) @@ -125,16 +140,29 @@ def write_ssh_config(relay_info, proxy_path, vm_name, ip, username, common_lines.append("\tIdentityFile " + private_key_file) lines = [""] + relay_info_path = None + relay_info_filename = None if is_arc: if cert_file: relay_info_dir = os.path.dirname(cert_file) elif private_key_file: relay_info_dir = os.path.dirname(private_key_file) else: - relay_info_dir = tempfile.mkdtemp(prefix="ssharcrelayinfo") - relay_info_path = os.path.join(relay_info_dir, "relay_info") + # create the custom folder + relay_info_dir = credentials_folder + if not os.path.isdir(relay_info_dir): + os.makedirs(credentials_folder) + + if ip: + relay_info_filename = ip + "-relay_info" + if vm_name and resource_group: + relay_info_filename = resource_group + "-" + vm_name + "-relay_info" + + relay_info_path = os.path.join(relay_info_dir, relay_info_filename) + # Overwrite relay_info if it already exists in that folder. + file_utils.delete_file(relay_info_path, f"{relay_info_path} already exists, and couldn't be overwritten.") file_utils.write_to_file(relay_info_path, 'w', relay_info, - f"Couldn't write relay information to file {relay_info_path}", 'utf-8') + f"Couldn't write relay information to file {relay_info_path}.", 'utf-8') oschmod.set_mode(relay_info_path, stat.S_IRUSR) lines.append("Host " + resource_group + "-" + vm_name) @@ -168,6 +196,36 @@ def write_ssh_config(relay_info, proxy_path, vm_name, ip, username, with open(config_path, mode) as f: f.write('\n'.join(lines)) + + if delete_keys or delete_cert or is_arc: + # Warn users to delete credentials once config file is no longer being used. + # If user provided keys, only ask them to delete the certificate. + if is_arc: + if delete_keys and delete_cert: + path_to_delete = os.path.dirname(cert_file) + items_to_delete = f" (id_rsa, id_rsa.pub, id_rsa.pub-aadcert.pub, {relay_info_filename})" + elif delete_cert: + path_to_delete = os.path.dirname(cert_file) + items_to_delete = f" (id_rsa.pub-aadcert.pub, {relay_info_filename})" + else: + path_to_delete = relay_info_path + items_to_delete = "" + else: + path_to_delete = os.path.dirname(cert_file) + items_to_delete = " (id_rsa, id_rsa.pub, id_rsa.pub-aadcert.pub)" + if not delete_keys: + path_to_delete = cert_file + items_to_delete = "" + + validity_warning = "" + if delete_cert: + validity = get_ssh_cert_validity(cert_file) + if validity: + validity_warning = f" {validity.lower()}" + + logger.warning("%s contains sensitive information%s%s\n" + "Please delete it once you no longer need this config file. ", + path_to_delete, items_to_delete, validity_warning) def _get_ssh_path(ssh_command="ssh"): @@ -209,11 +267,7 @@ def _build_args(cert_file, private_key_file, port): return private_key + certificate + port_arg -def _do_cleanup(private_key_file, cert_file, log_file=None): - if os.environ.get("AZUREPS_HOST_ENVIRONMENT") != "cloud-shell/1.0": - raise azclierror.BadRequestError("Can't delete private key file. " - "The --delete-private-key flag set to True, " - "but this is not an Azure Cloud Shell session.") +def _do_cleanup(delete_keys, delete_cert, cert_file, private_key, public_key, log_file=None, wait=False): if log_file: t0 = time.time() match = False @@ -221,17 +275,19 @@ def _do_cleanup(private_key_file, cert_file, log_file=None): time.sleep(const.CLEANUP_TIME_INTERVAL_IN_SECONDS) try: with open(log_file, 'r') as ssh_client_log: - for line in ssh_client_log: - if re.search("debug1: Authentication succeeded", line): - match = True - ssh_client_log.close() + match = "debug1: Authentication succeeded" in ssh_client_log.read() + ssh_client_log.close() except: - t1 = time.time() - t0 - if t1 < const.CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS: - time.sleep(const.CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS - t1) - - if private_key_file and os.path.isfile(private_key_file): - file_utils.delete_file(private_key_file, f"Failed to delete private key file '{private_key_file}'. ") - - if cert_file and os.path.isfile(cert_file): - file_utils.delete_file(cert_file, f"Failed to delete certificate file '{cert_file}'. ") + # If there is an exception, wait for a little bit and try again + time.sleep(const.CLEANUP_TIME_INTERVAL_IN_SECONDS) + + elif wait: + # if we are not checking the logs, but still want to wait for connection before deleting files + time.sleep(const.CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS) + + if delete_keys and private_key: + file_utils.delete_file(private_key, f"Couldn't delete private key {private_key}. ", True) + if delete_keys and public_key: + file_utils.delete_file(public_key, f"Couldn't delete public key {public_key}. ", True) + if delete_cert and cert_file: + file_utils.delete_file(cert_file, f"Couldn't delete certificate {cert_file}. ", True) diff --git a/src/ssh/azext_ssh/vendored_sdks/hybridconnectivity/operations/_endpoints_operations.py b/src/ssh/azext_ssh/vendored_sdks/hybridconnectivity/operations/_endpoints_operations.py index d4ee531d53e..b29bf835c13 100644 --- a/src/ssh/azext_ssh/vendored_sdks/hybridconnectivity/operations/_endpoints_operations.py +++ b/src/ssh/azext_ssh/vendored_sdks/hybridconnectivity/operations/_endpoints_operations.py @@ -74,7 +74,8 @@ def list_credentials( } error_map.update(kwargs.pop('error_map', {})) # api_version = "2021-10-06-preview" - api_version = "2021-10-01-privatepreview" + # api_version = "2021-10-01-privatepreview" + api_version = "2021-07-08-privatepreview" accept = "application/json" # Construct URL diff --git a/src/ssh/setup.py b/src/ssh/setup.py index 1615ce2dde6..860e04cb9b8 100644 --- a/src/ssh/setup.py +++ b/src/ssh/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages -VERSION = "0.1.9" +VERSION = "0.2.1" CLASSIFIERS = [ 'Development Status :: 4 - Beta',