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

Initial PR for keyvault preview CLI extension #137

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,6 @@ ENV/

# Sphinx
_build/

# Jet Brains
.idea/
17 changes: 17 additions & 0 deletions src/keyvault/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

# Key Vault Azure CLI Extension

The Azure CLI extension for Key Vault is an extension which previews unreleased functionality in the keyvault command module.

__NOTE__: The code for this extension is automatedly pulled from the [azure-sdk-for-python](https://github.com/azure/azure-sdk-for-python) and the [azure-cli](https://github.com/azure/azure-cli) repos using update_extension.py, and updated to run as an Azure CLI extension. Changes may cause incorrect behavior and will be lost if the code is regenerated.

## Manually updating the Extension

Clone the [azure-sdk-for-python](https://github.com/azure/azure-sdk-for-python) and the [azure-cli](https://github.com/azure/azure-cli) repos:

$ git clone https://github.com/azure/azure-sdk-for-python.git
$ git clone https://github.com/azure/azure-cli

Using python 3.* run update_extension.py:

$ python update_extension.py --sdk <azure-sdk-for-python clone root> --cli <azure-cli clone root>
45 changes: 45 additions & 0 deletions src/keyvault/azext_keyvault/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# pylint: disable-all

# ---------------------------------------------------------------------------------
# The code for this extension file is pulled from the azure-cli repo and
# modified to run inside a cli extension. Changes may cause incorrect behavior and
# will be lost if the code is regenerated. Please see the readme.md at the base of
# the keyvault extension for details.
# ---------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core import AzCommandsLoader

import azext_keyvault._help # pylint: disable=unused-import


class KeyVaultCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
from azure.cli.core.commands import CliCommandType
from ._client_factory import keyvault_client_factory
from ._command_type import KeyVaultCommandGroup, KeyVaultArgumentContext
keyvault_custom = CliCommandType(
operations_tmpl='azext_keyvault.custom#{}',
client_factory=keyvault_client_factory
)
super(KeyVaultCommandsLoader, self).__init__(cli_ctx=cli_ctx,
custom_command_type=keyvault_custom,
command_group_cls=KeyVaultCommandGroup,
argument_context_cls=KeyVaultArgumentContext)

def load_command_table(self, args):
from .commands import load_command_table
load_command_table(self, args)
return self.command_table

def load_arguments(self, command):
from ._params import load_arguments
load_arguments(self, command)


COMMAND_LOADER_CLS = KeyVaultCommandsLoader
44 changes: 44 additions & 0 deletions src/keyvault/azext_keyvault/_client_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# pylint: disable-all

# ---------------------------------------------------------------------------------
# The code for this extension file is pulled from the azure-cli repo and
# modified to run inside a cli extension. Changes may cause incorrect behavior and
# will be lost if the code is regenerated. Please see the readme.md at the base of
# the keyvault extension for details.
# ---------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------


def keyvault_client_factory(cli_ctx, **_):
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azext_keyvault.mgmt.keyvault import KeyVaultManagementClient
return get_mgmt_service_client(cli_ctx, KeyVaultManagementClient)


def keyvault_client_vaults_factory(cli_ctx, _):
return keyvault_client_factory(cli_ctx).vaults


def keyvault_data_plane_factory(cli_ctx, _):
from azext_keyvault.keyvault import KeyVaultClient, KeyVaultAuthentication

def get_token(server, resource, scope): # pylint: disable=unused-argument
import adal
from azure.cli.core._profile import Profile
try:
return Profile(cli_ctx=cli_ctx).get_login_credentials(resource)[0]._token_retriever() # pylint: disable=protected-access
except adal.AdalError as err:
from knack.util import CLIError
# pylint: disable=no-member
if (hasattr(err, 'error_response') and
('error_description' in err.error_response) and
('AADSTS70008:' in err.error_response['error_description'])):
raise CLIError(
"Credentials have expired due to inactivity. Please run 'az login'")
raise CLIError(err)

return KeyVaultClient(KeyVaultAuthentication(get_token))
167 changes: 167 additions & 0 deletions src/keyvault/azext_keyvault/_command_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# pylint: disable-all

# ---------------------------------------------------------------------------------
# The code for this extension file is pulled from the azure-cli repo and
# modified to run inside a cli extension. Changes may cause incorrect behavior and
# will be lost if the code is regenerated. Please see the readme.md at the base of
# the keyvault extension for details.
# ---------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import base64

from knack.introspection import extract_full_summary_from_signature, extract_args_from_signature
from knack.util import CLIError

from azure.cli.core.commands import LongRunningOperation, AzCommandGroup, AzArgumentContext


def _encode_hex(item):
""" Recursively crawls the object structure and converts bytes or bytearrays to base64
encoded strings. """
if isinstance(item, list):
return [_encode_hex(x) for x in item]
elif hasattr(item, '__dict__'):
for key, val in item.__dict__.items():
if not key.startswith('_'):
try:
setattr(item, key, _encode_hex(val))
except TypeError:
item.__dict__[key] = _encode_hex(val)
return item
elif isinstance(item, (bytes, bytearray)):
return base64.b64encode(item).decode('utf-8')
return item


def keyvault_exception_handler(ex):
from msrest.exceptions import ValidationError, ClientRequestError
from azext_keyvault.keyvault.models import KeyVaultErrorException
if isinstance(ex, (ValidationError, KeyVaultErrorException)):
try:
raise CLIError(ex.inner_exception.error.message)
except AttributeError:
raise CLIError(ex)
elif isinstance(ex, ClientRequestError):
if 'Failed to establish a new connection' in str(ex.inner_exception):
raise CLIError('Max retries exceeded attempting to connect to vault. '
'The vault may not exist or you may need to flush your DNS cache '
'and try again later.')
raise CLIError(ex)


class KeyVaultCommandGroup(AzCommandGroup):

def __init__(self, command_loader, group_name, **kwargs):
from ._client_factory import keyvault_data_plane_factory
# all regular and custom commands should use the keyvault data plane client
merged_kwargs = self._merge_kwargs(kwargs, base_kwargs=command_loader.module_kwargs)
merged_kwargs['custom_command_type'].settings['client_factory'] = keyvault_data_plane_factory
super(KeyVaultCommandGroup, self).__init__(command_loader, group_name, **kwargs)

def _create_keyvault_command(self, name, method_name=None, command_type_name=None, **kwargs):
self._check_stale()

merged_kwargs = self._flatten_kwargs(kwargs, command_type_name)
operations_tmpl = merged_kwargs['operations_tmpl']
command_name = '{} {}'.format(self.group_name, name) if self.group_name else name

def get_op_handler():
return self.command_loader.get_op_handler(operations_tmpl.format(method_name))

def keyvault_arguments_loader():
op = get_op_handler()
self.command_loader._apply_doc_string(op, merged_kwargs) # pylint: disable=protected-access
cmd_args = list(
extract_args_from_signature(op, excluded_params=self.command_loader.excluded_command_handler_args))
return cmd_args

def keyvault_description_loader():
op = get_op_handler()
self.command_loader._apply_doc_string(op, merged_kwargs) # pylint: disable=protected-access
return extract_full_summary_from_signature(op)

def keyvault_command_handler(command_args):
from azure.cli.core.util import get_arg_list
from azure.cli.core.commands.client_factory import resolve_client_arg_name
from msrest.paging import Paged
from azure.cli.core.util import poller_classes

op = get_op_handler()
op_args = get_arg_list(op)
command_type = merged_kwargs.get('command_type', None)
client_factory = command_type.settings.get('client_factory', None) if command_type \
else merged_kwargs.get('client_factory', None)

client_arg_name = resolve_client_arg_name(operations_tmpl.format(method_name), kwargs)
if client_arg_name in op_args:
client = client_factory(self.command_loader.cli_ctx, command_args)
command_args[client_arg_name] = client
try:
result = op(**command_args)
# apply results transform if specified
transform_result = merged_kwargs.get('transform', None)
if transform_result:
return _encode_hex(transform_result(result))

# otherwise handle based on return type of results
if isinstance(result, poller_classes()):
return _encode_hex(
LongRunningOperation(self.command_loader.cli_ctx, 'Starting {}'.format(name))(result))
elif isinstance(result, Paged):
try:
return _encode_hex(list(result))
except TypeError:
# TODO: Workaround for an issue in either KeyVault server-side or msrest
# See https://github.com/Azure/autorest/issues/1309
return []
else:
return _encode_hex(result)
except Exception as ex: # pylint: disable=broad-except
return keyvault_exception_handler(ex)

self.command_loader._cli_command(command_name, handler=keyvault_command_handler, # pylint: disable=protected-access
argument_loader=keyvault_arguments_loader,
description_loader=keyvault_description_loader,
**merged_kwargs)

def keyvault_command(self, name, method_name=None, command_type=None, **kwargs):
""" Registers an Azure CLI KeyVault Data Plane command. These commands must respond to a
challenge from the service when they make requests. """
command_type_name = 'command_type'
if command_type:
kwargs[command_type_name] = command_type
self._create_keyvault_command(name, method_name, command_type_name, **kwargs)

def keyvault_custom(self, name, method_name=None, command_type=None, **kwargs):
command_type_name = 'custom_command_type'
if command_type:
kwargs[command_type_name] = command_type
self._create_keyvault_command(name, method_name, command_type_name, **kwargs)


class KeyVaultArgumentContext(AzArgumentContext):

def attributes_argument(self, name, attr_class, create=False, ignore=None):
from ._validators import get_attribute_validator, datetime_type
from azure.cli.core.commands.parameters import get_three_state_flag

from knack.arguments import ignore_type

ignore = ignore or []
self.argument('{}_attributes'.format(name), ignore_type,
validator=get_attribute_validator(name, attr_class, create))
if create:
self.extra('disabled', help='Create {} in disabled state.'.format(name), arg_type=get_three_state_flag())
else:
self.extra('enabled', help='Enable the {}.'.format(name), arg_type=get_three_state_flag())
if 'expires' not in ignore:
self.extra('expires', default=None, help='Expiration UTC datetime (Y-m-d\'T\'H:M:S\'Z\').',
type=datetime_type)
if 'not_before' not in ignore:
self.extra('not_before', default=None, type=datetime_type,
help='Key not usable before the provided UTC datetime (Y-m-d\'T\'H:M:S\'Z\').')
55 changes: 55 additions & 0 deletions src/keyvault/azext_keyvault/_completers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# pylint: disable-all

# ---------------------------------------------------------------------------------
# The code for this extension file is pulled from the azure-cli repo and
# modified to run inside a cli extension. Changes may cause incorrect behavior and
# will be lost if the code is regenerated. Please see the readme.md at the base of
# the keyvault extension for details.
# ---------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.decorators import Completer
from azure.cli.core._profile import Profile


def _get_token(cli_ctx, server, resource, scope): # pylint: disable=unused-argument
return Profile(cli_ctx=cli_ctx).get_login_credentials(resource)[0]._token_retriever() # pylint: disable=protected-access


def get_keyvault_name_completion_list(resource_name):

@Completer
def completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument
from azext_keyvault.keyvault import KeyVaultClient, KeyVaultAuthentication
client = KeyVaultClient(KeyVaultAuthentication(_get_token))
func_name = 'get_{}s'.format(resource_name)
vault = namespace.vault_base_url
items = []
for y in list(getattr(client, func_name)(vault)):
id_val = getattr(y, 'id', None) or getattr(y, 'kid', None)
items.append(id_val.rsplit('/', 1)[1])
return items

return completer


def get_keyvault_version_completion_list(resource_name):

@Completer
def completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument
from azext_keyvault.keyvault import KeyVaultClient, KeyVaultAuthentication
client = KeyVaultClient(KeyVaultAuthentication(_get_token))
func_name = 'get_{}_versions'.format(resource_name)
vault = namespace.vault_base_url
name = getattr(namespace, '{}_name'.format(resource_name))
items = []
for y in list(getattr(client, func_name)(vault, name)):
id_val = getattr(y, 'id', None) or getattr(y, 'kid', None)
items.append(id_val.rsplit('/', 1)[1])
return items

return completer
Loading