Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --secrets for VM and VMSS #2212

Merged
merged 9 commits into from Mar 6, 2017

move format-secret to vm module

  • Loading branch information...
devigned committed Feb 28, 2017
commit bb66387a7662f389fe73aa29bd5e441f23b076a3
@@ -85,26 +85,7 @@
secrets=$(az keyvault secret list-versions --vault-name vaultname \\
-n cert1 --query "[?attributes.enabled]")
vm_secrets=$(az keyvault secret vm-format -s "$secrets") \n
az vm create -g group-name -n vm-name --admin-username deploy \\
--image debian --secrets "$vm_secrets"
"""

helps['keyvault secret vm-format'] = """
type: command
long-summary: >
Transform secrets into a form consumed by VMs via --secrets.
examples:
- name: Create a self-signed certificate with a the default policy and add to a virtual machine
text: >
az keyvault certificate create --vault-name vaultname -n cert1 \\
-p "$(az keyvault certificate get-default-policy)"
secrets=$(az keyvault secret list-versions --vault-name vaultname \\
-n cert1 --query "[?attributes.enabled]")
vm_secrets=$(az keyvault secret vm-format -s "$secrets") \n
vm_secrets=$(az vm format-secret -s "$secrets") \n
az vm create -g group-name -n vm-name --admin-username deploy \\
--image debian --secrets "$vm_secrets"
@@ -182,5 +182,3 @@ def register_attributes_argument(scope, name, attr_class, create=False):
register_cli_argument('keyvault certificate issuer', 'admin_last_name', arg_group='Organization Detail')
register_cli_argument('keyvault certificate issuer', 'admin_email', arg_group='Organization Detail')
register_cli_argument('keyvault certificate issuer', 'admin_phone', arg_group='Organization Detail')

register_cli_argument('keyvault secret vm-format', 'secrets', options_list=('--secrets', '-s'), type=get_json_object, help='JSON encoded secrets. Use @{file} to load from a file.')
@@ -111,6 +111,3 @@ def keyvault_update_setter(client, resource_group_name, vault_name, parameters):

# default policy document
cli_keyvault_data_plane_command('keyvault certificate get-default-policy', custom_path.format('get_default_policy'))

# secret vm formatter
cli_command(__name__, 'keyvault secret vm-format', custom_path.format('get_vm_format_secret'), factory)
@@ -22,7 +22,6 @@
Trigger,
Action)
from azure.keyvault.generated.models.key_vault_client_enums import ActionType, KeyUsageType
from azure.keyvault.key_vault_id import parse_secret_id
from azure.mgmt.keyvault.models import (VaultCreateOrUpdateParameters,
VaultProperties,
AccessPolicyEntry,
@@ -37,8 +36,7 @@
from azure.cli.core._util import CLIError
import azure.cli.core.azlogging as azlogging
from azure.keyvault import KeyVaultClient
from azure.cli.command_modules.keyvault._validators import secret_text_encoding_values, \
_get_resource_group_from_vault_name
from azure.cli.command_modules.keyvault._validators import secret_text_encoding_values

logger = azlogging.get_az_logger(__name__)

@@ -204,50 +202,6 @@ def _get_object_id(graph_client, subscription=None, spn=None, upn=None):
return _get_object_id_from_subscription(graph_client, subscription)


def _get_vault_id_from_name(client, vault_name):
group_name = _get_resource_group_from_vault_name(vault_name)
vault = client.get(group_name, vault_name)
return vault.id


def get_vm_format_secret(client, secrets, certificate_store=None):
"""
Format secrets to be used in `az vm create --secrets`
:param client: management api client
:param dict secrets: array of secrets to be formatted
:param str certificate_store: certificate store the secret will be applied (Windows only)
:return: formatted secrets as an array
:rtype: list
"""
grouped_secrets = {}
if isinstance(secrets, dict):
secrets = [secrets]

# group secrets by source vault
for secret in secrets:
parsed = parse_secret_id(secret['id'])
match = re.search('://(.+?)\\.', parsed.vault)
vault_name = match.group(1)
if vault_name not in grouped_secrets:
grouped_secrets[vault_name] = {
'vaultCertificates': [],
'id': _get_vault_id_from_name(client, vault_name)
}

vault_cert = {'certificateUrl': secret['id']}
if certificate_store:
vault_cert['certificateStore'] = certificate_store

grouped_secrets[vault_name]['vaultCertificates'].append(vault_cert)

# transform the reduced map to vm format
formatted = [{'sourceVault': {'id': value['id']},
'vaultCertificates': value['vaultCertificates']}
for _, value in list(grouped_secrets.items())]

return formatted


def get_default_policy(client, scaffold=False): #pylint: disable=unused-argument
"""
Get a default certificate policy to be used with `az keyvault certificate create`
@@ -22,6 +22,25 @@
text: az {1} -n name -g MyResourceGroup
"""

helps['vm format-secret'] = """
type: command
long-summary: >
Transform secrets into a form consumed by VMs and VMSS create via --secrets.

This comment has been minimized.

Copy link
@tjprescott

tjprescott Mar 1, 2017

Member

Should we specify that this is only applicable to Windows VMs?

examples:
- name: Create a self-signed certificate with a the default policy and add to a virtual machine
text: >
az keyvault certificate create --vault-name vaultname -n cert1 \\
-p "$(az keyvault certificate get-default-policy)"
secrets=$(az keyvault secret list-versions --vault-name vaultname \\
-n cert1 --query "[?attributes.enabled]")
vm_secrets=$(az vm format-secret -s "$secrets") \n
az vm create -g group-name -n vm-name --admin-username deploy \\
--image debian --secrets "$vm_secrets"

This comment has been minimized.

Copy link
@tjprescott

tjprescott Mar 1, 2017

Member

Why do I need to run this command to use secrets for Debian if this command only exists because of the Windows cert store?

This comment has been minimized.

Copy link
@devigned

devigned Mar 1, 2017

Author Member

There is non-trivial work to be done to format a key for use in vm secrets with or without cert store. Yes, we could accept only secret ids and fetch the vault resource id within az vm create, but that would change the input format for --secret when we need to input certificate store with a secret in az vm create.

I opt'd for a single format for input for --secret which matches the API paired with a transformation command which takes as input a list of secrets and optionally a certificate store. I think balances the tradeoffs between ease of use and flexibility to change the output of az vm format-secret if need arises due to API changes.

Do you have a different design in mind?

This comment has been minimized.

Copy link
@tjprescott

tjprescott Mar 1, 2017

Member

To recap our f2f:

  • Make --secrets on VM create support nargs='+', type=json.loads and have VM create do the work of merging the different outputs from az vm format-secrets. Keep the output of format-secrets as that required by VM create (not a simplified intermediate one).
  • Have the vm format-secrets --secrets arg take a list of secret URIs rather than the JSON output of the list command (or, have it accept both). You'd have to use query and TSV output to feed this in, but that's consistent with our current --ids usage elsewhere in the CLI.
"""

helps['vm create'] = """
type: command
short-summary: Create an Azure Virtual Machine.
@@ -76,7 +95,7 @@
secrets=$(az keyvault secret list-versions --vault-name vaultname \\
-n cert1 --query "[?attributes.enabled]")
vm_secrets=$(az keyvault secret vm-format -s "$secrets") \n
vm_secrets=$(az vm format-secret -s "$secrets") \n
az vm create -g group-name -n vm-name --admin-username deploy \\
--image debian --secrets "$vm_secrets"
@@ -115,7 +134,7 @@
secrets=$(az keyvault secret list-versions --vault-name vaultname \\
-n cert1 --query "[?attributes.enabled]")
vm_secrets=$(az keyvault secret vm-format -s "$secrets") \n
vm_secrets=$(az vm format-secret -s "$secrets") \n
az vmss create -g group-name -n vm-name --admin-username deploy \\
--image debian --secrets "$vm_secrets"
@@ -9,6 +9,8 @@
from azure.mgmt.compute.models import (CachingTypes,
UpgradeMode)
from azure.mgmt.storage.models import SkuName

from azure.cli.core._util import get_json_object
from azure.cli.core.commands import register_cli_argument, CliArgumentType, register_extra_cli_argument
from azure.cli.core.commands.parameters import \
(location_type, get_one_of_subscription_locations,
@@ -271,3 +273,5 @@ def get_vm_size_completion_list(prefix, action, parsed_args, **kwargs): # pylin
register_cli_argument(scope, 'source_snapshot', ignore_type)
register_cli_argument(scope, 'size_gb', options_list=('--size-gb', '-z'), help='size in GB.')
register_cli_argument(scope, 'duration_in_seconds', help='Time duration in seconds until the SAS access expires')

register_cli_argument('vm format-secret', 'secrets', options_list=('--secrets', '-s'), type=get_json_object, help='JSON encoded secrets as an array of secrets [], or a single secret {}. Use @{file} to load from a file.')
@@ -8,8 +8,8 @@

from msrestazure.azure_exceptions import CloudError

from azure.mgmt.keyvault import KeyVaultManagementClient
from azure.cli.core.commands.arm import resource_id, parse_resource_id, is_valid_resource_id
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core._util import CLIError, random_string
from ._client_factory import _compute_client_factory
from azure.cli.command_modules.vm._vm_utils import check_existence, load_json
@@ -24,7 +24,24 @@ def validate_nsg_name(namespace):
or '{}_NSG_{}'.format(namespace.vm_name, random_string(8))


def _get_resource_group_from_vault_name(vault_name):
"""
Fetch resource group from vault name
:param str vault_name: name of the key vault
:return: resource group name or None
:rtype: str
"""
from azure.cli.core.commands.client_factory import get_mgmt_service_client
client = get_mgmt_service_client(KeyVaultManagementClient).vaults
for vault in client.list():
id_comps = parse_resource_id(vault.id)
if id_comps['name'] == vault_name:
return id_comps['resource_group']
return None


def _get_resource_id(val, resource_group, resource_type, resource_namespace):
from azure.cli.core.commands.client_factory import get_subscription_id
if is_valid_resource_id(val):
return val
else:
@@ -347,7 +364,7 @@ def _validate_vm_create_storage_account(namespace):


def _validate_vm_create_availability_set(namespace):

from azure.cli.core.commands.client_factory import get_subscription_id
if namespace.availability_set:
as_id = parse_resource_id(namespace.availability_set)
name = as_id['name']
@@ -476,6 +493,7 @@ def _validate_vmss_create_public_ip(namespace):


def _validate_vm_create_nics(namespace):
from azure.cli.core.commands.client_factory import get_subscription_id
nics_value = namespace.nics
nics = []

@@ -100,6 +100,7 @@ def transform_av_set_collection_output(av_sets):
cli_command(__name__, 'vm resize', custom_path.format('resize_vm'))
cli_command(__name__, 'vm capture', custom_path.format('capture_vm'))
cli_command(__name__, 'vm open-port', custom_path.format('vm_open_port'))
cli_command(__name__, 'vm format-secret', custom_path.format('get_vm_format_secret'))
cli_generic_update_command(__name__, 'vm update',
mgmt_path.format(op_var, op_class, 'get'),
mgmt_path.format(op_var, op_class, 'create_or_update'),
@@ -16,7 +16,8 @@
from urlparse import urlparse # pylint: disable=import-error

from six.moves.urllib.request import urlopen # noqa, pylint: disable=import-error,unused-import

from azure.cli.command_modules.vm._validators import _get_resource_group_from_vault_name
from azure.keyvault.key_vault_id import parse_secret_id
from azure.mgmt.compute.models import (VirtualHardDisk,
VirtualMachineScaleSet,
VirtualMachineCaptureParameters,
@@ -1757,3 +1758,48 @@ def create_av_set(availability_set_name, resource_group_name,
resource_group_name, deployment_name, properties, raw=no_wait))
compute_client = _compute_client_factory()
return compute_client.availability_sets.get(resource_group_name, availability_set_name)


def _get_vault_id_from_name(client, vault_name):
group_name = _get_resource_group_from_vault_name(vault_name)
vault = client.get(group_name, vault_name)
return vault.id


def get_vm_format_secret(secrets, certificate_store=None):
"""
Format secrets to be used in `az vm create --secrets`
:param dict secrets: array of secrets to be formatted
:param str certificate_store: certificate store the secret will be applied (Windows only)
:return: formatted secrets as an array
:rtype: list
"""
from azure.mgmt.keyvault import KeyVaultManagementClient
client = get_mgmt_service_client(KeyVaultManagementClient).vaults
grouped_secrets = {}
if isinstance(secrets, dict):
secrets = [secrets]

# group secrets by source vault
for secret in secrets:
parsed = parse_secret_id(secret['id'])
match = re.search('://(.+?)\\.', parsed.vault)
vault_name = match.group(1)
if vault_name not in grouped_secrets:
grouped_secrets[vault_name] = {
'vaultCertificates': [],
'id': _get_vault_id_from_name(client, vault_name)
}

vault_cert = {'certificateUrl': secret['id']}
if certificate_store:
vault_cert['certificateStore'] = certificate_store

grouped_secrets[vault_name]['vaultCertificates'].append(vault_cert)

# transform the reduced map to vm format
formatted = [{'sourceVault': {'id': value['id']},
'vaultCertificates': value['vaultCertificates']}
for _, value in list(grouped_secrets.items())]

return formatted
@@ -1267,7 +1267,7 @@ def body(self):
self.vault_name,
policy_path))
secret_out = self.cmd('keyvault secret list-versions --vault-name {} -n cert1'.format(self.vault_name))[-1]
vm_format = self.cmd('keyvault secret vm-format -s \'{0}\''.format(json.dumps(secret_out)))
vm_format = self.cmd('vm format-secret -s \'{0}\''.format(json.dumps(secret_out)))

self.cmd('vm create -g {rg} -n {vm_name} --admin-username {admin} --authentication-type {auth_type} --image {image} --ssh-key-value \'{ssh_key}\' -l {location} --secrets \'{secrets}\''.format(
rg=self.resource_group,
@@ -1332,7 +1332,7 @@ def body(self):
policy_path))

secret_out = self.cmd('keyvault secret list-versions --vault-name {} -n cert1'.format(self.vault_name))[-1]
vm_format = self.cmd('keyvault secret vm-format -s \'{0}\' --certificate-store "My"'.format(json.dumps(secret_out)))
vm_format = self.cmd('vm format-secret -s \'{0}\' --certificate-store "My"'.format(json.dumps(secret_out)))

self.cmd('vm create -g {rg} -n {vm_name} --admin-username {admin} --admin-password VerySecret!12 --image {image} -l {location} --secrets \'{secrets}\''.format(
rg=self.resource_group,
@@ -1570,7 +1570,7 @@ def body(self):
policy_path))

secret_out = self.cmd('keyvault secret list-versions --vault-name {} -n cert1'.format(self.vault_name))[-1]
vm_format = self.cmd('keyvault secret vm-format -s \'{0}\''.format(json.dumps(secret_out)))
vm_format = self.cmd('vm format-secret -s \'{0}\''.format(json.dumps(secret_out)))

self.cmd('vmss create -n {name} -g {rg} --image Debian --admin-username deploy --ssh-key-value \'{ssh}\' --secrets \'{secrets}\''.format(
name=self.vmss_name,
@@ -26,6 +26,7 @@

DEPENDENCIES = [
'azure-mgmt-compute==0.33.1rc1',
'azure-mgmt-keyvault==0.30.0',
'azure-keyvault==0.1.0',
'azure-mgmt-network==0.30.0',
'azure-mgmt-resource==0.30.2',
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.