def _assert_args(resource_group, vm_name, ssh_ip, resource_type, cert_file, username): if resource_type and resource_type.lower() != "microsoft.compute" \ and resource_type.lower() != "microsoft.hybridcompute": raise azclierror.InvalidArgumentValueError("--resource-type must be either \"Microsoft.Compute\" " "for Azure VMs or \"Microsoft.HybridCompute\" for Arc Servers.") if not (resource_group or vm_name or ssh_ip): raise azclierror.RequiredArgumentMissingError( "The VM must be specified by --ip or --resource-group and " "--vm-name/--name") if resource_group and not vm_name or vm_name and not resource_group: raise azclierror.MutuallyExclusiveArgumentError( "--resource-group and --vm-name/--name must be provided together") if ssh_ip and (vm_name or resource_group): raise azclierror.MutuallyExclusiveArgumentError( "--ip cannot be used with --resource-group or --vm-name/--name") if cert_file and not username: raise azclierror.MutuallyExclusiveArgumentError( "To authenticate with a certificate you need to provide a --local-user") if cert_file and not os.path.isfile(cert_file): raise azclierror.FileOperationError(f"Certificate file {cert_file} not found")
def _check_or_create_public_private_files(public_key_file, private_key_file): # 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") ssh_utils.create_ssh_keyfile(private_key_file) if not public_key_file: if private_key_file: public_key_file = private_key_file + ".pub" else: raise azclierror.RequiredArgumentMissingError( "Public key file not specified") if not os.path.isfile(public_key_file): raise azclierror.FileOperationError( f"Public key file {public_key_file} not found") # The private key is not required as the user may be using a keypair # stored in ssh-agent (and possibly in a hardware token) if 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
def ssh_cert(cmd, cert_path=None, public_key_file=None, ssh_client_folder=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 public_key_file: public_key_file = os.path.abspath(public_key_file) if cert_path: cert_path = os.path.abspath(cert_path) if ssh_client_folder: ssh_client_folder = os.path.abspath(ssh_client_folder) # 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) public_key_file, _, _ = _check_or_create_public_private_files(public_key_file, None, keys_folder, ssh_client_folder) cert_file, _ = _get_and_write_certificate(cmd, public_key_file, cert_path, ssh_client_folder) if keys_folder: logger.warning("%s contains sensitive information (id_rsa, id_rsa.pub). " "Please delete once this certificate is no longer being used.", keys_folder) # pylint: disable=broad-except try: cert_expiration = ssh_utils.get_certificate_start_and_end_times(cert_file, ssh_client_folder)[1] print_styled_text((Style.SUCCESS, f"Generated SSH certificate {cert_file} is valid until {cert_expiration} in local time.")) except Exception as e: logger.warning("Couldn't determine certificate validity. Error: %s", str(e)) print_styled_text((Style.SUCCESS, f"Generated SSH certificate {cert_file}."))
def _check_or_create_public_private_files(public_key_file, private_key_file, credentials_folder): delete_keys = False # If nothing is passed, then create a directory with a ephemeral keypair if not public_key_file and not private_key_file: # 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: if private_key_file: public_key_file = private_key_file + ".pub" else: raise azclierror.RequiredArgumentMissingError("Public key file not specified") if not os.path.isfile(public_key_file): raise azclierror.FileOperationError(f"Public key file {public_key_file} not found") # The private key is not required as the user may be using a keypair # stored in ssh-agent (and possibly in a hardware token) if 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, delete_keys
def _assert_args(resource_group, vm_name, ssh_ip): if not (resource_group or vm_name or ssh_ip): raise azclierror.RequiredArgumentMissingError( "The VM must be specified by --ip or --resource-group and --vm-name/--name" ) if resource_group and not vm_name or vm_name and not resource_group: raise azclierror.MutuallyExclusiveArgumentError( "--resource-group and --vm-name/--name must be provided together") if ssh_ip and (vm_name or resource_group): raise azclierror.MutuallyExclusiveArgumentError( "--ip cannot be used with --resource-group or --vm-name/--name")
def ssh_cert(cmd, cert_path=None, 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 _assert_args(resource_group, vm_name, ssh_ip, cert_file, username): if not (resource_group or vm_name or ssh_ip): raise azclierror.RequiredArgumentMissingError( "The VM must be specified by --ip or --resource-group and --vm-name/--name") if resource_group and not vm_name or vm_name and not resource_group: raise azclierror.MutuallyExclusiveArgumentError( "--resource-group and --vm-name/--name must be provided together") if ssh_ip and (vm_name or resource_group): raise azclierror.MutuallyExclusiveArgumentError( "--ip cannot be used with --resource-group or --vm-name/--name") if cert_file and not username: raise azclierror.MutuallyExclusiveArgumentError( "To authenticate with a certificate you need to provide a --local-user") if cert_file and not os.path.isfile(cert_file): raise azclierror.FileOperationError(f"Certificate file {cert_file} not found")
def ssh_config(cmd, config_path, resource_group_name=None, vm_name=None, ssh_ip=None, public_key_file=None, private_key_file=None, overwrite=False, use_private_ip=False, local_user=None, cert_file=None, port=None, resource_type=None, credentials_folder=None, ssh_proxy_folder=None, ssh_client_folder=None): # If user provides their own key pair, certificate will be written in the same folder as public key. 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.") _assert_args(resource_group_name, vm_name, ssh_ip, resource_type, cert_file, local_user) config_session = ssh_info.ConfigSession(config_path, resource_group_name, vm_name, ssh_ip, public_key_file, private_key_file, overwrite, use_private_ip, local_user, cert_file, port, resource_type, credentials_folder, ssh_proxy_folder, ssh_client_folder) op_call = ssh_utils.write_ssh_config config_session.resource_type = _decide_resource_type(cmd, config_session) # if the folder doesn't exist, this extension won't create a new one. config_folder = os.path.dirname(config_session.config_path) if not os.path.isdir(config_folder): raise azclierror.InvalidArgumentValueError(f"Config file destination folder {config_folder} " "does not exist.") if not credentials_folder: # * is not a valid name for a folder in Windows. Treat this as a special case. folder_name = config_session.ip if config_session.ip != "*" else "all_ips" if config_session.resource_group_name and config_session.vm_name: folder_name = config_session.resource_group_name + "-" + config_session.vm_name if not set(folder_name).isdisjoint(set(const.WINDOWS_INVALID_FOLDERNAME_CHARS)) and \ platform.system() == "Windows" and (not config_session.local_user or config_session.is_arc()): folder_name = file_utils.remove_invalid_characters_foldername(folder_name) if folder_name == "": raise azclierror.RequiredArgumentMissingError("Can't create default folder for generated keys. " "Please provide --keys-destination-folder.") config_session.credentials_folder = os.path.join(config_folder, os.path.join("az_ssh_config", folder_name)) _do_ssh_op(cmd, config_session, op_call)
def _decide_resource_type(cmd, op_info): # 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. is_arc_server = False is_azure_vm = False if op_info.ip: is_azure_vm = True vm = None elif op_info.resource_type: if op_info.resource_type.lower() == "microsoft.hybridcompute": arc, arc_error, is_arc_server = _check_if_arc_server(cmd, op_info.resource_group_name, op_info.vm_name) if not is_arc_server: colorama.init() if isinstance(arc_error, ResourceNotFoundError): raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group " f"{op_info.resource_group_name} was not found.", const.RECOMMENDATION_RESOURCE_NOT_FOUND) raise azclierror.BadRequestError("Unable to determine that the target machine is an Arc Server. " f"Error:\n{str(arc_error)}", const.RECOMMENDATION_RESOURCE_NOT_FOUND) elif op_info.resource_type.lower() == "microsoft.compute": vm, vm_error, is_azure_vm = _check_if_azure_vm(cmd, op_info.resource_group_name, op_info.vm_name) if not is_azure_vm: colorama.init() if isinstance(vm_error, ResourceNotFoundError): raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group " f"{op_info.resource_group_name} was not found.", const.RECOMMENDATION_RESOURCE_NOT_FOUND) raise azclierror.BadRequestError("Unable to determine that the target machine is an Azure VM. " f"Error:\n{str(vm_error)}", const.RECOMMENDATION_RESOURCE_NOT_FOUND) else: vm, vm_error, is_azure_vm = _check_if_azure_vm(cmd, op_info.resource_group_name, op_info.vm_name) arc, arc_error, is_arc_server = _check_if_arc_server(cmd, op_info.resource_group_name, op_info.vm_name) if is_azure_vm and is_arc_server: colorama.init() raise azclierror.BadRequestError(f"{op_info.resource_group_name} has Azure VM and Arc Server with the " f"same name: {op_info.vm_name}.", colorama.Fore.YELLOW + "Please provide a --resource-type." + colorama.Style.RESET_ALL) if not is_azure_vm and not is_arc_server: colorama.init() if isinstance(arc_error, ResourceNotFoundError) and isinstance(vm_error, ResourceNotFoundError): raise azclierror.ResourceNotFoundError(f"The resource {op_info.vm_name} in the resource group " f"{op_info.resource_group_name} was not found. ", const.RECOMMENDATION_RESOURCE_NOT_FOUND) 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)}", const.RECOMMENDATION_RESOURCE_NOT_FOUND) # Note: We are not able to determine the os of the target if the user only provides an IP address. os_type = None if is_azure_vm and vm and vm.storage_profile and vm.storage_profile.os_disk and vm.storage_profile.os_disk.os_type: os_type = vm.storage_profile.os_disk.os_type if is_arc_server and arc and arc.properties and arc.properties and arc.properties.os_name: os_type = arc.properties.os_name if os_type: telemetry.add_extension_event('ssh', {'Context.Default.AzureCLI.TargetOSType': os_type}) # Note 2: This is a temporary check while AAD login is not enabled for Windows. if os_type and os_type.lower() == 'windows' and not op_info.local_user: colorama.init() raise azclierror.RequiredArgumentMissingError("SSH Login using AAD credentials is not currently supported " "for Windows.", colorama.Fore.YELLOW + "Please provide --local-user." + colorama.Style.RESET_ALL) target_resource_type = "Microsoft.Compute" if is_arc_server: target_resource_type = "Microsoft.HybridCompute" telemetry.add_extension_event('ssh', {'Context.Default.AzureCLI.TargetResourceType': target_resource_type}) return target_resource_type