Skip to content

Commit

Permalink
[Compute] az vm/vmss create: Add --ssh-key-type parameter to supp…
Browse files Browse the repository at this point in the history
…ort generating Ed25519 SSH keys (#29926)
  • Loading branch information
yanzhudd authored Sep 23, 2024
1 parent 45ab8d3 commit 414043d
Show file tree
Hide file tree
Showing 6 changed files with 2,831 additions and 349 deletions.
3 changes: 2 additions & 1 deletion src/azure-cli/azure/cli/command_modules/vm/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,8 @@ def load_arguments(self, _):
c.argument('disable_integrity_monitoring_autoupgrade', action='store_true', min_api='2020-12-01', help='Disable auto upgrade of guest attestation extension for Trusted Launch enabled VMs and VMSS.')

with self.argument_context(scope, arg_group='Authentication') as c:
c.argument('generate_ssh_keys', action='store_true', help='Generate SSH public and private RSA key files if missing. The keys will be stored in the ~/.ssh directory')
c.argument('generate_ssh_keys', action='store_true', help='Generate SSH public and private key files if missing. The keys will be stored in the ~/.ssh directory')
c.argument('ssh_key_type', arg_type=get_enum_type(['RSA', 'Ed25519']), default='RSA', min_api='2023-09-01', help='Specify the type of SSH public and private key files to be generated if missing.')
c.argument('admin_username', help='Username for the VM. Default value is current username of OS. If the default value is system reserved, then default value will be set to azureuser. Please refer to https://docs.microsoft.com/rest/api/compute/virtualmachines/createorupdate#osprofile to get a full list of reserved values.')
c.argument('admin_password', help="Password for the VM if authentication type is 'Password'.")
c.argument('ssh_key_value', options_list=['--ssh-key-values'], completer=FilesCompleter(), type=file_type, nargs='+')
Expand Down
24 changes: 18 additions & 6 deletions src/azure-cli/azure/cli/command_modules/vm/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,7 @@ def _validate_admin_password(password, os_type):

def validate_ssh_key(namespace, cmd=None):
from azure.core.exceptions import HttpResponseError
ssh_key_type = namespace.ssh_key_type if hasattr(namespace, 'ssh_key_type') else 'RSA'
if hasattr(namespace, 'ssh_key_name') and namespace.ssh_key_name:
client = _compute_client_factory(cmd.cli_ctx)
# --ssh-key-name
Expand All @@ -1248,7 +1249,7 @@ def validate_ssh_key(namespace, cmd=None):
elif namespace.generate_ssh_keys:
parameters = {}
parameters['location'] = namespace.location
public_key = _validate_ssh_key_helper("", namespace.generate_ssh_keys)
public_key = _validate_ssh_key_helper("", namespace.generate_ssh_keys, ssh_key_type)
parameters['public_key'] = public_key
client.ssh_public_keys.create(resource_group_name=namespace.resource_group_name,
ssh_public_key_name=namespace.ssh_key_name,
Expand All @@ -1261,16 +1262,22 @@ def validate_ssh_key(namespace, cmd=None):

processed_ssh_key_values = []
for ssh_key_value in namespace.ssh_key_value:
processed_ssh_key_values.append(_validate_ssh_key_helper(ssh_key_value, namespace.generate_ssh_keys))
processed_ssh_key_values.append(_validate_ssh_key_helper(ssh_key_value,
namespace.generate_ssh_keys,
ssh_key_type))
namespace.ssh_key_value = processed_ssh_key_values
# if no ssh keys processed, try to generate new key / use existing at root.
else:
namespace.ssh_key_value = [_validate_ssh_key_helper("", namespace.generate_ssh_keys)]
namespace.ssh_key_value = [_validate_ssh_key_helper("",
namespace.generate_ssh_keys,
ssh_key_type)]


def _validate_ssh_key_helper(ssh_key_value, should_generate_ssh_keys):
def _validate_ssh_key_helper(ssh_key_value, should_generate_ssh_keys, ssh_key_type=None):
file_name = 'id_rsa.pub' if ssh_key_type is None or ssh_key_type == 'RSA' else 'id_ed25519.pub'
string_or_file = (ssh_key_value or
os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa.pub'))
os.path.join(os.path.expanduser('~'), '.ssh', file_name))

content = string_or_file
if os.path.exists(string_or_file):
logger.info('Use existing SSH public key file: %s', string_or_file)
Expand All @@ -1285,7 +1292,12 @@ def _validate_ssh_key_helper(ssh_key_value, should_generate_ssh_keys):
private_key_filepath = public_key_filepath[:-4]
else:
private_key_filepath = public_key_filepath + '.private'
content = keys.generate_ssh_keys(private_key_filepath, public_key_filepath)

if ssh_key_type == "Ed25519":
from azure.cli.command_modules.vm._vm_utils import generate_ssh_keys_ed25519
content = generate_ssh_keys_ed25519(private_key_filepath, public_key_filepath)
else:
content = keys.generate_ssh_keys(private_key_filepath, public_key_filepath)
logger.warning("SSH key files '%s' and '%s' have been generated under ~/.ssh to "
"allow SSH access to the VM. If using machines without "
"permanent storage, back up your keys to a safe location.",
Expand Down
34 changes: 34 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_vm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,3 +708,37 @@ def import_aaz_by_profile(profile, module_name):
from azure.cli.core.aaz.utils import get_aaz_profile_module_name
profile_module_name = get_aaz_profile_module_name(profile_name=profile)
return importlib.import_module(f"azure.cli.command_modules.vm.aaz.{profile_module_name}.{module_name}")


def generate_ssh_keys_ed25519(private_key_filepath, public_key_filepath):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

ssh_dir = os.path.dirname(private_key_filepath)
if not os.path.exists(ssh_dir):
os.makedirs(ssh_dir)
os.chmod(ssh_dir, 0o700)

private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.OpenSSH,
encryption_algorithm=serialization.NoEncryption()
)

with os.fdopen(os.open(private_key_filepath, flags=os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode=384, ), "w", ) as f:
f.write(
private_bytes.decode()
)
os.chmod(private_key_filepath, 0o600)

with open(public_key_filepath, 'w') as public_key_file:
s = public_key.public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH)
public_key = s.decode(encoding="utf8").replace("\n", "")
public_key_file.write(public_key)
os.chmod(public_key_filepath, 0o644)

return public_key
4 changes: 2 additions & 2 deletions src/azure-cli/azure/cli/command_modules/vm/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
os_disk_security_encryption_type=None, os_disk_secure_vm_disk_encryption_set=None,
disk_controller_type=None, disable_integrity_monitoring_autoupgrade=False, enable_proxy_agent=None,
proxy_agent_mode=None, source_snapshots_or_disks=None, source_snapshots_or_disks_size_gb=None,
source_disk_restore_point=None, source_disk_restore_point_size_gb=None):
source_disk_restore_point=None, source_disk_restore_point_size_gb=None, ssh_key_type=None):

from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
Expand Down Expand Up @@ -3138,7 +3138,7 @@ def create_vmss(cmd, vmss_name, resource_group_name, image=None,
location=None, tags=None, upgrade_policy_mode='manual', validate=False,
admin_username=None, admin_password=None, authentication_type=None,
vm_sku=None, no_wait=False,
ssh_dest_key_path=None, ssh_key_value=None, generate_ssh_keys=False,
ssh_dest_key_path=None, ssh_key_value=None, generate_ssh_keys=False, ssh_key_type=None,
load_balancer=None, load_balancer_sku=None, application_gateway=None,
app_gateway_subnet_address_prefix=None,
app_gateway_sku='Standard_Large', app_gateway_capacity=10,
Expand Down
Loading

0 comments on commit 414043d

Please sign in to comment.