diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 6f40999d61a..3a6277f850e 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -770,6 +770,8 @@ def command_group(self, group_name, command_type=None, **kwargs): def argument_context(self, scope, **kwargs): return self._argument_context_cls(self, scope, **kwargs) + # Please use add_cli_command instead of _cli_command. + # Currently "keyvault" and "batch" modules are still rely on this function, so it cannot be removed now. def _cli_command(self, name, operation=None, handler=None, argument_loader=None, description_loader=None, **kwargs): from knack.deprecation import Deprecated @@ -829,6 +831,31 @@ def default_description_loader(): handler or default_command_handler, **kwargs) + def add_cli_command(self, name, command_operation, **kwargs): + """Register a command in command_table with command operation provided""" + from knack.deprecation import Deprecated + from .commands.command_operation import BaseCommandOperation + if not issubclass(type(command_operation), BaseCommandOperation): + raise TypeError("CommandOperation must be an instance of subclass of BaseCommandOperation." + " Got instance of '{}'".format(type(command_operation))) + + kwargs['deprecate_info'] = Deprecated.ensure_new_style_deprecation(self.cli_ctx, kwargs, 'command') + + name = ' '.join(name.split()) + + if self.supported_api_version(resource_type=kwargs.get('resource_type'), + min_api=kwargs.get('min_api'), + max_api=kwargs.get('max_api'), + operation_group=kwargs.get('operation_group')): + self._populate_command_group_table_with_subgroups(' '.join(name.split()[:-1])) + self.command_table[name] = self.command_cls(loader=self, + name=name, + handler=command_operation.handler, + arguments_loader=command_operation.arguments_loader, + description_loader=command_operation.description_loader, + command_operation=command_operation, + **kwargs) + def get_op_handler(self, operation, operation_group=None): """ Import and load the operation handler """ # Patch the unversioned sdk path to include the appropriate API version for the diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 6f1f37d9a11..aa0873abd7b 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -1157,6 +1157,12 @@ def update(self, other=None, **kwargs): class AzCommandGroup(CommandGroup): def __init__(self, command_loader, group_name, **kwargs): + """ + :param command_loader: The command loader that commands will be registered into + :type command_loader: azure.cli.core.AzCommandsLoader + :param group_name: The name of the group of commands in the command hierarchy + :type group_name: str + """ merged_kwargs = self._merge_kwargs(kwargs, base_kwargs=command_loader.module_kwargs) operations_tmpl = merged_kwargs.pop('operations_tmpl', None) super(AzCommandGroup, self).__init__(command_loader, group_name, @@ -1244,16 +1250,24 @@ def custom_command(self, name, method_name=None, **kwargs): return self._command(name, method_name=method_name, custom_command=True, **kwargs) def _command(self, name, method_name, custom_command=False, **kwargs): + from .command_operation import CommandOperation + self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) operations_tmpl = merged_kwargs['operations_tmpl'] - command_name = '{} {}'.format(self.group_name, name) if self.group_name else name - self.command_loader._cli_command(command_name, # pylint: disable=protected-access - operation=operations_tmpl.format(method_name), - **merged_kwargs) + op_path = operations_tmpl.format(method_name) + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = CommandOperation( + command_loader=self.command_loader, + op_path=op_path, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) return command_name # pylint: disable=no-self-use @@ -1283,27 +1297,31 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, setter_name='create_or_update', setter_type=None, setter_arg_name='parameters', child_collection_prop_name=None, child_collection_key='name', child_arg_name='item_name', custom_func_name=None, custom_func_type=None, **kwargs): - from azure.cli.core.commands.arm import _cli_generic_update_command + from azure.cli.core.commands.command_operation import GenericUpdateCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg()) merged_kwargs_custom = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command=True)) self._apply_tags(merged_kwargs, kwargs, name) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type) - setter_op = self._resolve_operation(merged_kwargs, setter_name, setter_type) - custom_func_op = self._resolve_operation(merged_kwargs_custom, custom_func_name, custom_func_type, - custom_command=True) if custom_func_name else None - _cli_generic_update_command( - self.command_loader, - '{} {}'.format(self.group_name, name), - getter_op=getter_op, - setter_op=setter_op, + getter_op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type) + setter_op_path = self._resolve_operation(merged_kwargs, setter_name, setter_type) + custom_function_op_path = self._resolve_operation(merged_kwargs_custom, custom_func_name, custom_func_type, + custom_command=True) if custom_func_name else None + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = GenericUpdateCommandOperation( + command_loader=self.command_loader, + getter_op_path=getter_op_path, + setter_op_path=setter_op_path, setter_arg_name=setter_arg_name, - custom_function_op=custom_func_op, + custom_function_op_path=custom_function_op_path, child_collection_prop_name=child_collection_prop_name, child_collection_key=child_collection_key, child_arg_name=child_arg_name, - **merged_kwargs) + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def wait_command(self, name, getter_name='get', **kwargs): self._wait_command(name, getter_name=getter_name, custom_command=False, **kwargs) @@ -1315,16 +1333,24 @@ def generic_wait_command(self, name, getter_name='get', getter_type=None, **kwar self._wait_command(name, getter_name=getter_name, getter_type=getter_type, **kwargs) def _wait_command(self, name, getter_name='get', getter_type=None, custom_command=False, **kwargs): - from azure.cli.core.commands.arm import _cli_wait_command + from azure.cli.core.commands.command_operation import WaitCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) - _cli_wait_command(self.command_loader, '{} {}'.format(self.group_name, name), getter_op=getter_op, - custom_command=custom_command, **merged_kwargs) + getter_op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) + + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = WaitCommandOperation( + command_loader=self.command_loader, + op_path=getter_op_path, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def show_command(self, name, getter_name='get', **kwargs): self._show_command(name, getter_name=getter_name, custom_command=False, **kwargs) @@ -1333,16 +1359,24 @@ def custom_show_command(self, name, getter_name='get', **kwargs): self._show_command(name, getter_name=getter_name, custom_command=True, **kwargs) def _show_command(self, name, getter_name='get', getter_type=None, custom_command=False, **kwargs): - from azure.cli.core.commands.arm import _cli_show_command + from azure.cli.core.commands.command_operation import ShowCommandOperation self._check_stale() merged_kwargs = self._flatten_kwargs(kwargs, get_command_type_kwarg(custom_command)) self._apply_tags(merged_kwargs, kwargs, name) if getter_type: merged_kwargs = _merge_kwargs(getter_type.settings, merged_kwargs, CLI_COMMAND_KWARGS) - getter_op = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) - _cli_show_command(self.command_loader, '{} {}'.format(self.group_name, name), getter_op=getter_op, - custom_command=custom_command, **merged_kwargs) + op_path = self._resolve_operation(merged_kwargs, getter_name, getter_type, custom_command=custom_command) + + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = ShowCommandOperation( + command_loader=self.command_loader, + op_path=op_path, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) def _apply_tags(self, merged_kwargs, kwargs, command_name): # don't inherit deprecation or preview info from command group diff --git a/src/azure-cli-core/azure/cli/core/commands/arm.py b/src/azure-cli-core/azure/cli/core/commands/arm.py index a1b6c7cb8da..a48078aafc5 100644 --- a/src/azure-cli-core/azure/cli/core/commands/arm.py +++ b/src/azure-cli-core/azure/cli/core/commands/arm.py @@ -7,21 +7,19 @@ import argparse from collections import OrderedDict -import copy import json import re -from azure.cli.core import AzCommandsLoader, EXCLUDED_PARAMS -from azure.cli.core.commands import LongRunningOperation, _is_poller, cached_get, cached_put +from azure.cli.core import EXCLUDED_PARAMS +from azure.cli.core.commands import LongRunningOperation from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.commands.events import EVENT_INVOKER_PRE_LOAD_ARGUMENTS from azure.cli.core.commands.validators import IterateValue -from azure.cli.core.util import ( - shell_safe_json_parse, augment_no_wait_handler_args, get_command_type_kwarg, find_child_item) +from azure.cli.core.util import shell_safe_json_parse, get_command_type_kwarg from azure.cli.core.profiles import ResourceType, get_sdk from knack.arguments import CLICommandArgument, ignore_type -from knack.introspection import extract_args_from_signature, extract_full_summary_from_signature +from knack.introspection import extract_args_from_signature from knack.log import get_logger from knack.util import todict, CLIError @@ -406,358 +404,6 @@ def get_arguments_loader(context, getter_op, cmd_args=None, operation_group=None return cmd_args -# pylint: disable=too-many-statements -def _cli_generic_update_command(context, name, getter_op, setter_op, setter_arg_name='parameters', - child_collection_prop_name=None, child_collection_key='name', - child_arg_name='item_name', custom_function_op=None, **kwargs): - if not isinstance(context, AzCommandsLoader): - raise TypeError("'context' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(context))) - if not isinstance(getter_op, str): - raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) - if not isinstance(setter_op, str): - raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) - if custom_function_op and not isinstance(custom_function_op, str): - raise TypeError("Custom function operation must be a string. Got '{}'".format( - custom_function_op)) - - def set_arguments_loader(): - return dict(extract_args_from_signature(context.get_op_handler( - setter_op, operation_group=kwargs.get('operation_group')), excluded_params=EXCLUDED_PARAMS)) - - def function_arguments_loader(): - if not custom_function_op: - return {} - - custom_op = context.get_op_handler(custom_function_op) - context._apply_doc_string(custom_op, kwargs) # pylint: disable=protected-access - return dict(extract_args_from_signature(custom_op, excluded_params=EXCLUDED_PARAMS)) - - def generic_update_arguments_loader(): - arguments = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - arguments.update(set_arguments_loader()) - arguments.update(function_arguments_loader()) - arguments.pop('instance', None) # inherited from custom_function(instance, ...) - arguments.pop('parent', None) - arguments.pop('expand', None) # possibly inherited from the getter - arguments.pop(setter_arg_name, None) - - # Add the generic update parameters - class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods - - def __call__(self, parser, namespace, values, option_string=None): - if not getattr(namespace, 'ordered_arguments', None): - setattr(namespace, 'ordered_arguments', []) - namespace.ordered_arguments.append((option_string, values)) - - group_name = 'Generic Update' - arguments['properties_to_set'] = CLICommandArgument( - 'properties_to_set', options_list=['--set'], nargs='+', - action=OrderedArgsAction, default=[], - help='Update an object by specifying a property path and value to set. Example: {}'.format(set_usage), - metavar='KEY=VALUE', arg_group=group_name - ) - arguments['properties_to_add'] = CLICommandArgument( - 'properties_to_add', options_list=['--add'], nargs='+', - action=OrderedArgsAction, default=[], - help='Add an object to a list of objects by specifying a path and ' - 'key value pairs. Example: {}'.format(add_usage), - metavar='LIST KEY=VALUE', arg_group=group_name - ) - arguments['properties_to_remove'] = CLICommandArgument( - 'properties_to_remove', options_list=['--remove'], nargs='+', - action=OrderedArgsAction, default=[], - help='Remove a property or an element from a list. Example: {}'.format(remove_usage), - metavar='LIST INDEX', arg_group=group_name - ) - arguments['force_string'] = CLICommandArgument( - 'force_string', action='store_true', arg_group=group_name, - help="When using 'set' or 'add', preserve string literals instead of attempting to convert to JSON." - ) - return [(k, v) for k, v in arguments.items()] - - def _extract_handler_and_args(args, commmand_kwargs, op, context): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - factory = _get_client_factory(name, **commmand_kwargs) - client = None - if factory: - try: - client = factory(context.cli_ctx) - except TypeError: - client = factory(context.cli_ctx, args) - - client_arg_name = resolve_client_arg_name(op, kwargs) - op_handler = context.get_op_handler(op, operation_group=kwargs.get('operation_group')) - raw_args = dict(extract_args_from_signature(op_handler, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - op_args = {key: val for key, val in args.items() if key in raw_args} - if client_arg_name in raw_args: - op_args[client_arg_name] = client - return op_handler, op_args - - def handler(args): # pylint: disable=too-many-branches,too-many-statements - cmd = args.get('cmd') - context_copy = copy.copy(context) - context_copy.cli_ctx = cmd.cli_ctx - force_string = args.get('force_string', False) - ordered_arguments = args.pop('ordered_arguments', []) - dest_names = child_arg_name.split('.') - child_names = [args.get(key, None) for key in dest_names] - for item in ['properties_to_add', 'properties_to_set', 'properties_to_remove']: - if args[item]: - raise CLIError("Unexpected '{}' was not empty.".format(item)) - del args[item] - - getter, getterargs = _extract_handler_and_args(args, cmd.command_kwargs, getter_op, context_copy) - - if child_collection_prop_name: - parent = cached_get(cmd, getter, **getterargs) - instance = find_child_item( - parent, *child_names, path=child_collection_prop_name, key_path=child_collection_key) - else: - parent = None - instance = cached_get(cmd, getter, **getterargs) - - # pass instance to the custom_function, if provided - if custom_function_op: - custom_function, custom_func_args = _extract_handler_and_args( - args, cmd.command_kwargs, custom_function_op, context_copy) - if child_collection_prop_name: - parent = custom_function(instance=instance, parent=parent, **custom_func_args) - else: - instance = custom_function(instance=instance, **custom_func_args) - - # apply generic updates after custom updates - setter, setterargs = _extract_handler_and_args(args, cmd.command_kwargs, setter_op, context_copy) - - for arg in ordered_arguments: - arg_type, arg_values = arg - if arg_type == '--set': - try: - for expression in arg_values: - set_properties(instance, expression, force_string) - except ValueError: - raise CLIError('invalid syntax: {}'.format(set_usage)) - elif arg_type == '--add': - try: - add_properties(instance, arg_values, force_string) - except ValueError: - raise CLIError('invalid syntax: {}'.format(add_usage)) - elif arg_type == '--remove': - try: - remove_properties(instance, arg_values) - except ValueError: - raise CLIError('invalid syntax: {}'.format(remove_usage)) - - # Done... update the instance! - setterargs[setter_arg_name] = parent if child_collection_prop_name else instance - - # Handle no-wait - supports_no_wait = cmd.command_kwargs.get('supports_no_wait', None) - if supports_no_wait: - no_wait_enabled = args.get('no_wait', False) - augment_no_wait_handler_args(no_wait_enabled, - setter, - setterargs) - else: - no_wait_param = cmd.command_kwargs.get('no_wait_param', None) - if no_wait_param: - setterargs[no_wait_param] = args[no_wait_param] - - if setter_arg_name == 'parameters': - result = cached_put(cmd, setter, **setterargs) - else: - result = cached_put(cmd, setter, setterargs[setter_arg_name], setter_arg_name=setter_arg_name, **setterargs) - - if supports_no_wait and no_wait_enabled: - return None - - no_wait_param = cmd.command_kwargs.get('no_wait_param', None) - if no_wait_param and setterargs.get(no_wait_param, None): - return None - - if _is_poller(result): - result = result.result() - - if child_collection_prop_name: - result = find_child_item( - result, *child_names, path=child_collection_prop_name, key_path=child_collection_key) - return result - - context._cli_command(name, handler=handler, argument_loader=generic_update_arguments_loader, **kwargs) # pylint: disable=protected-access - - -def _cli_wait_command(context, name, getter_op, custom_command=False, **kwargs): - - if not isinstance(getter_op, str): - raise ValueError("Getter operation must be a string. Got '{}'".format(type(getter_op))) - - factory = _get_client_factory(name, custom_command=custom_command, **kwargs) - - def generic_wait_arguments_loader(): - cmd_args = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - - group_name = 'Wait Condition' - cmd_args['timeout'] = CLICommandArgument( - 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, - help='maximum wait in seconds' - ) - cmd_args['interval'] = CLICommandArgument( - 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, - help='polling interval in seconds' - ) - cmd_args['deleted'] = CLICommandArgument( - 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, - help='wait until deleted' - ) - cmd_args['created'] = CLICommandArgument( - 'created', options_list=['--created'], action='store_true', arg_group=group_name, - help="wait until created with 'provisioningState' at 'Succeeded'" - ) - cmd_args['updated'] = CLICommandArgument( - 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, - help="wait until updated with provisioningState at 'Succeeded'" - ) - cmd_args['exists'] = CLICommandArgument( - 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, - help="wait until the resource exists" - ) - cmd_args['custom'] = CLICommandArgument( - 'custom', options_list=['--custom'], arg_group=group_name, - help="Wait until the condition satisfies a custom JMESPath query. E.g. " - "provisioningState!='InProgress', " - "instanceView.statuses[?code=='PowerState/running']" - ) - return [(k, v) for k, v in cmd_args.items()] - - def get_provisioning_state(instance): - provisioning_state = getattr(instance, 'provisioning_state', None) - if not provisioning_state: - # some SDK, like resource-group, has 'provisioning_state' under 'properties' - properties = getattr(instance, 'properties', None) - if properties: - provisioning_state = getattr(properties, 'provisioning_state', None) - # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' - if not provisioning_state: - additional_properties = getattr(properties, 'additional_properties', {}) - provisioning_state = additional_properties.get('provisioningState') - return provisioning_state - - def handler(args): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - from msrest.exceptions import ClientException - from azure.core.exceptions import HttpResponseError - import time - - context_copy = copy.copy(context) - getter_args = dict(extract_args_from_signature(context.get_op_handler( - getter_op, operation_group=kwargs.get('operation_group')), excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - cmd = args.get('cmd') if 'cmd' in getter_args else args.pop('cmd') - context_copy.cli_ctx = cmd.cli_ctx - operations_tmpl = _get_operations_tmpl(cmd, custom_command=custom_command) - client_arg_name = resolve_client_arg_name(operations_tmpl, kwargs) - try: - client = factory(context_copy.cli_ctx) if factory else None - except TypeError: - client = factory(context_copy.cli_ctx, args) if factory else None - if client and (client_arg_name in getter_args): - args[client_arg_name] = client - - getter = context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')) - - timeout = args.pop('timeout') - interval = args.pop('interval') - wait_for_created = args.pop('created') - wait_for_deleted = args.pop('deleted') - wait_for_updated = args.pop('updated') - wait_for_exists = args.pop('exists') - custom_condition = args.pop('custom') - if not any([wait_for_created, wait_for_updated, wait_for_deleted, - wait_for_exists, custom_condition]): - raise CLIError( - "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") - - progress_indicator = context_copy.cli_ctx.get_progress_controller() - progress_indicator.begin() - for _ in range(0, timeout, interval): - try: - progress_indicator.add(message='Waiting') - instance = getter(**args) - if wait_for_exists: - progress_indicator.end() - return None - provisioning_state = get_provisioning_state(instance) - # until we have any needs to wait for 'Failed', let us bail out on this - if provisioning_state: - provisioning_state = provisioning_state.lower() - if provisioning_state == 'failed': - progress_indicator.stop() - raise CLIError('The operation failed') - if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ - custom_condition and bool(verify_property(instance, custom_condition)): - progress_indicator.end() - return None - except (ClientException, HttpResponseError) as ex: - progress_indicator.stop() - if getattr(ex, 'status_code', None) == 404: - if wait_for_deleted: - return None - if not any([wait_for_created, wait_for_exists, custom_condition]): - raise - else: - raise - except Exception: # pylint: disable=broad-except - progress_indicator.stop() - raise - - time.sleep(interval) - - progress_indicator.end() - return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) - - context._cli_command(name, handler=handler, argument_loader=generic_wait_arguments_loader, **kwargs) # pylint: disable=protected-access - - -def _cli_show_command(context, name, getter_op, custom_command=False, **kwargs): - - if not isinstance(getter_op, str): - raise ValueError("Getter operation must be a string. Got '{}'".format(type(getter_op))) - - factory = _get_client_factory(name, custom_command=custom_command, **kwargs) - - def generic_show_arguments_loader(): - cmd_args = get_arguments_loader(context, getter_op, operation_group=kwargs.get('operation_group')) - return [(k, v) for k, v in cmd_args.items()] - - def description_loader(): - return extract_full_summary_from_signature( - context.get_op_handler(getter_op, operation_group=kwargs.get('operation_group'))) - - def handler(args): - from azure.cli.core.commands.client_factory import resolve_client_arg_name - context_copy = copy.copy(context) - getter_args = dict(extract_args_from_signature( - context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')), - excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) - cmd = args.get('cmd') if 'cmd' in getter_args else args.pop('cmd') - context_copy.cli_ctx = cmd.cli_ctx - operations_tmpl = _get_operations_tmpl(cmd, custom_command=custom_command) - client_arg_name = resolve_client_arg_name(operations_tmpl, kwargs) - try: - client = factory(context_copy.cli_ctx) if factory else None - except TypeError: - client = factory(context_copy.cli_ctx, args) if factory else None - - if client and (client_arg_name in getter_args): - args[client_arg_name] = client - - getter = context_copy.get_op_handler(getter_op, operation_group=kwargs.get('operation_group')) - try: - return getter(**args) - except Exception as ex: # pylint: disable=broad-except - show_exception_handler(ex) - context._cli_command(name, handler=handler, argument_loader=generic_show_arguments_loader, # pylint: disable=protected-access - description_loader=description_loader, **kwargs) - - def show_exception_handler(ex): if getattr(getattr(ex, 'response', ex), 'status_code', None) == 404: import sys diff --git a/src/azure-cli-core/azure/cli/core/commands/command_operation.py b/src/azure-cli-core/azure/cli/core/commands/command_operation.py new file mode 100644 index 00000000000..27f2cc77570 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -0,0 +1,510 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import argparse +from azure.cli.core import AzCommandsLoader, EXCLUDED_PARAMS +from knack.introspection import extract_args_from_signature, extract_full_summary_from_signature +from knack.arguments import CLICommandArgument, ignore_type + + +class BaseCommandOperation: + """ Base class of command operation classes """ + + def __init__(self, command_loader, **merged_kwargs): + if not isinstance(command_loader, AzCommandsLoader): + raise TypeError("'command_loader' expected type '{}'. Got: '{}'".format( + AzCommandsLoader.__name__, type(command_loader))) + self.command_loader = command_loader + self.cmd = None + self.merged_kwargs = merged_kwargs + self.client_factory = merged_kwargs.get('client_factory') + self.operation_group = merged_kwargs.get('operation_group') + + @property + def cli_ctx(self): + """ Return the cli_ctx of command or command_loader """ + return self.cmd.cli_ctx if self.cmd else self.command_loader.cli_ctx + + def handler(self, command_args): + """ Callback function of CLICommand handler """ + raise NotImplementedError() + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + raise NotImplementedError() + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + raise NotImplementedError() + + def get_op_handler(self, op_path): + """ Import and load the operation handler by path """ + # Patch the unversioned sdk path to include the appropriate API version for the + # resource type in question. + from importlib import import_module + import types + + from azure.cli.core.profiles import AZURE_API_PROFILES + from azure.cli.core.profiles._shared import get_versioned_sdk_path + + for rt in AZURE_API_PROFILES[self.cli_ctx.cloud.profile]: + if op_path.startswith(rt.import_prefix + '.'): + op_path = op_path.replace(rt.import_prefix, + get_versioned_sdk_path(self.cli_ctx.cloud.profile, rt, + operation_group=self.operation_group)) + + try: + mod_to_import, attr_path = op_path.split('#') + handler = import_module(mod_to_import) + for part in attr_path.split('.'): + handler = getattr(handler, part) + if isinstance(handler, types.FunctionType): + return handler + return handler.__func__ + except (ValueError, AttributeError): + raise ValueError("The operation '{}' is invalid.".format(op_path)) + + def load_getter_op_arguments(self, getter_op_path, cmd_args=None): + """ Load arguments from function signature of getter command op """ + op = self.get_op_handler(getter_op_path) + getter_args = dict( + extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + cmd_args = cmd_args or {} + cmd_args.update(getter_args) + # The cmd argument is required when calling self.handler function. + cmd_args['cmd'] = CLICommandArgument('cmd', arg_type=ignore_type) + return cmd_args + + def apply_doc_string(self, handler): + return self.command_loader._apply_doc_string(handler, self.merged_kwargs) # pylint: disable=protected-access + + def load_op_handler_description(self, handler=None): + """ Load the description from function signature of command op """ + if handler is None: + def default_handler(): + """""" # Use empty __doc__ property here, which is required in extract_full_summary_from_signature + handler = default_handler + self.apply_doc_string(handler) + return extract_full_summary_from_signature(handler) + + def resolve_client_arg_name(self, op_path): + from azure.cli.core.commands.client_factory import resolve_client_arg_name + return resolve_client_arg_name(op_path, self.merged_kwargs) + + +class CommandOperation(BaseCommandOperation): + + def __init__(self, command_loader, op_path, **merged_kwargs): + if not isinstance(op_path, str): + raise TypeError("Operation must be a string. Got '{}'".format(op_path)) + super(CommandOperation, self).__init__(command_loader, **merged_kwargs) + self.op_path = op_path + + def handler(self, command_args): + """ Callback function of CLICommand handler """ + from azure.cli.core.util import get_arg_list, augment_no_wait_handler_args + + op = self.get_op_handler(self.op_path) + op_args = get_arg_list(op) + self.cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') + + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + supports_no_wait = self.merged_kwargs.get('supports_no_wait', None) + if supports_no_wait: + no_wait_enabled = command_args.pop('no_wait', False) + augment_no_wait_handler_args(no_wait_enabled, op, command_args) + if client: + client_arg_name = self.resolve_client_arg_name(self.op_path) + if client_arg_name in op_args: + command_args[client_arg_name] = client + return op(**command_args) + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + op = self.get_op_handler(self.op_path) + self.apply_doc_string(op) + cmd_args = list(extract_args_from_signature( + op, excluded_params=self.command_loader.excluded_command_handler_args)) + return cmd_args + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + op = self.get_op_handler(self.op_path) + return self.load_op_handler_description(op) + + +class GenericUpdateCommandOperation(BaseCommandOperation): # pylint: disable=too-many-instance-attributes + + class OrderedArgsAction(argparse.Action): # pylint:disable=too-few-public-methods + """Action for 'properties_to_set', 'properties_to_add' and 'properties_to_remove' arguments""" + def __call__(self, parser, namespace, values, option_string=None): + if not getattr(namespace, 'ordered_arguments', None): + setattr(namespace, 'ordered_arguments', []) + namespace.ordered_arguments.append((option_string, values)) + + def __init__(self, command_loader, getter_op_path, setter_op_path, setter_arg_name, custom_function_op_path, + child_collection_prop_name, child_collection_key, child_arg_name, **merged_kwargs): + if not isinstance(getter_op_path, str): + raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op_path)) + if not isinstance(setter_op_path, str): + raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op_path)) + if custom_function_op_path and not isinstance(custom_function_op_path, str): + raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op_path)) + super(GenericUpdateCommandOperation, self).__init__(command_loader, **merged_kwargs) + + self.getter_op_path = getter_op_path + self.setter_op_path = setter_op_path + self.custom_function_op_path = custom_function_op_path + self.setter_arg_name = setter_arg_name + self.child_collection_prop_name = child_collection_prop_name + self.child_collection_key = child_collection_key + self.child_arg_name = child_arg_name + + def handler(self, command_args): # pylint: disable=too-many-locals, too-many-statements, too-many-branches + """ Callback function of CLICommand handler """ + from knack.util import CLIError + from azure.cli.core.commands import cached_get, cached_put, _is_poller + from azure.cli.core.util import find_child_item, augment_no_wait_handler_args + from azure.cli.core.commands.arm import add_usage, remove_usage, set_usage,\ + add_properties, remove_properties, set_properties + + self.cmd = command_args.get('cmd') + + force_string = command_args.get('force_string', False) + ordered_arguments = command_args.pop('ordered_arguments', []) + dest_names = self.child_arg_name.split('.') + child_names = [command_args.get(key, None) for key in dest_names] + for item in ['properties_to_add', 'properties_to_set', 'properties_to_remove']: + if command_args[item]: + raise CLIError("Unexpected '{}' was not empty.".format(item)) + del command_args[item] + + getter, getterargs = self._extract_op_handler_and_args(command_args, self.getter_op_path) + + if self.child_collection_prop_name: + parent = cached_get(self.cmd, getter, **getterargs) + instance = find_child_item( + parent, *child_names, path=self.child_collection_prop_name, key_path=self.child_collection_key) + else: + parent = None + instance = cached_get(self.cmd, getter, **getterargs) + + # pass instance to the custom_function, if provided + if self.custom_function_op_path: + custom_function, custom_func_args = self._extract_op_handler_and_args( + command_args, self.custom_function_op_path) + if self.child_collection_prop_name: + parent = custom_function(instance=instance, parent=parent, **custom_func_args) + else: + instance = custom_function(instance=instance, **custom_func_args) + + # apply generic updates after custom updates + setter, setterargs = self._extract_op_handler_and_args(command_args, self.setter_op_path) + + for arg in ordered_arguments: + arg_type, arg_values = arg + if arg_type == '--set': + try: + for expression in arg_values: + set_properties(instance, expression, force_string) + except ValueError: + raise CLIError('invalid syntax: {}'.format(set_usage)) + elif arg_type == '--add': + try: + add_properties(instance, arg_values, force_string) + except ValueError: + raise CLIError('invalid syntax: {}'.format(add_usage)) + elif arg_type == '--remove': + try: + remove_properties(instance, arg_values) + except ValueError: + raise CLIError('invalid syntax: {}'.format(remove_usage)) + + # Done... update the instance! + setterargs[self.setter_arg_name] = parent if self.child_collection_prop_name else instance + + # Handle no-wait + supports_no_wait = self.cmd.command_kwargs.get('supports_no_wait', None) + if supports_no_wait: + no_wait_enabled = command_args.get('no_wait', False) + augment_no_wait_handler_args(no_wait_enabled, + setter, + setterargs) + else: + no_wait_param = self.cmd.command_kwargs.get('no_wait_param', None) + if no_wait_param: + setterargs[no_wait_param] = command_args[no_wait_param] + + if self.setter_arg_name == 'parameters': + result = cached_put(self.cmd, setter, **setterargs) + else: + result = cached_put(self.cmd, setter, setterargs[self.setter_arg_name], + setter_arg_name=self.setter_arg_name, **setterargs) + + if supports_no_wait and no_wait_enabled: + return None + + no_wait_param = self.cmd.command_kwargs.get('no_wait_param', None) + if no_wait_param and setterargs.get(no_wait_param, None): + return None + + if _is_poller(result): + result = result.result() + + if self.child_collection_prop_name: + result = find_child_item( + result, *child_names, path=self.child_collection_prop_name, key_path=self.child_collection_key) + return result + + def _extract_op_handler_and_args(self, args, op_path): + from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS + + client = None + if self.client_factory: + try: + client = self.client_factory(self.cli_ctx) + except TypeError: + client = self.client_factory(self.cli_ctx, args) + + client_arg_name = self.resolve_client_arg_name(op_path) + op = self.get_op_handler(op_path) + raw_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + op_args = {key: val for key, val in args.items() if key in raw_args} + if client_arg_name in raw_args: + op_args[client_arg_name] = client + return op, op_args + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + from azure.cli.core.commands.arm import set_usage, add_usage, remove_usage + + arguments = self.load_getter_op_arguments(self.getter_op_path) + arguments.update(self.load_setter_op_arguments()) + arguments.update(self.load_custom_function_op_arguments()) + arguments.pop('instance', None) # inherited from custom_function(instance, ...) + arguments.pop('parent', None) + arguments.pop('expand', None) # possibly inherited from the getter + arguments.pop(self.setter_arg_name, None) + + # Add the generic update parameters + group_name = 'Generic Update' + arguments['properties_to_set'] = CLICommandArgument( + 'properties_to_set', options_list=['--set'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Update an object by specifying a property path and value to set. Example: {}'.format(set_usage), + metavar='KEY=VALUE', arg_group=group_name + ) + arguments['properties_to_add'] = CLICommandArgument( + 'properties_to_add', options_list=['--add'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Add an object to a list of objects by specifying a path and ' + 'key value pairs. Example: {}'.format(add_usage), + metavar='LIST KEY=VALUE', arg_group=group_name + ) + arguments['properties_to_remove'] = CLICommandArgument( + 'properties_to_remove', options_list=['--remove'], nargs='+', + action=self.OrderedArgsAction, default=[], + help='Remove a property or an element from a list. Example: {}'.format(remove_usage), + metavar='LIST INDEX', arg_group=group_name + ) + arguments['force_string'] = CLICommandArgument( + 'force_string', action='store_true', arg_group=group_name, + help="When using 'set' or 'add', preserve string literals instead of attempting to convert to JSON." + ) + return [(k, v) for k, v in arguments.items()] + + def load_setter_op_arguments(self): + op = self.get_op_handler(self.setter_op_path) + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def load_custom_function_op_arguments(self): + if not self.custom_function_op_path: + return {} + op = self.get_op_handler(self.custom_function_op_path) + self.apply_doc_string(op) # pylint: disable=protected-access + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + return self.load_op_handler_description() + + +class ShowCommandOperation(BaseCommandOperation): + + def __init__(self, command_loader, op_path, **merged_kwargs): + if not isinstance(op_path, str): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) + super(ShowCommandOperation, self).__init__(command_loader, **merged_kwargs) + self.op_path = op_path + + def handler(self, command_args): + """ Callback function of CLICommand handler """ + from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS + + op = self.get_op_handler(self.op_path) + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.op_path) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + op = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set + try: + return op(**command_args) + except Exception as ex: # pylint: disable=broad-except + show_exception_handler(ex) + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + op = self.get_op_handler(self.op_path) + return self.load_op_handler_description(op) + + +class WaitCommandOperation(BaseCommandOperation): + + def __init__(self, command_loader, op_path, **merged_kwargs): + if not isinstance(op_path, str): + raise TypeError("operation must be a string. Got '{}'".format(op_path)) + super(WaitCommandOperation, self).__init__(command_loader, **merged_kwargs) + self.op_path = op_path + + def handler(self, command_args): # pylint: disable=too-many-statements + """ Callback function of CLICommand handler """ + from msrest.exceptions import ClientException + from azure.core.exceptions import HttpResponseError + from knack.util import CLIError + from azure.cli.core.commands.arm import EXCLUDED_NON_CLIENT_PARAMS, verify_property + + import time + + op = self.get_op_handler(self.op_path) + getter_args = dict(extract_args_from_signature(op, excluded_params=EXCLUDED_NON_CLIENT_PARAMS)) + self.cmd = command_args.get('cmd') if 'cmd' in getter_args else command_args.pop('cmd') + + client_arg_name = self.resolve_client_arg_name(self.op_path) + try: + client = self.client_factory(self.cli_ctx) if self.client_factory else None + except TypeError: + client = self.client_factory(self.cli_ctx, command_args) if self.client_factory else None + if client and (client_arg_name in getter_args): + command_args[client_arg_name] = client + + getter = self.get_op_handler(self.op_path) # Fetch op handler again after cmd property is set + + timeout = command_args.pop('timeout') + interval = command_args.pop('interval') + wait_for_created = command_args.pop('created') + wait_for_deleted = command_args.pop('deleted') + wait_for_updated = command_args.pop('updated') + wait_for_exists = command_args.pop('exists') + custom_condition = command_args.pop('custom') + if not any([wait_for_created, wait_for_updated, wait_for_deleted, + wait_for_exists, custom_condition]): + raise CLIError( + "incorrect usage: --created | --updated | --deleted | --exists | --custom JMESPATH") + + progress_indicator = self.cli_ctx.get_progress_controller() + progress_indicator.begin() + for _ in range(0, timeout, interval): + try: + progress_indicator.add(message='Waiting') + instance = getter(**command_args) + if wait_for_exists: + progress_indicator.end() + return None + provisioning_state = self._get_provisioning_state(instance) + # until we have any needs to wait for 'Failed', let us bail out on this + if provisioning_state: + provisioning_state = provisioning_state.lower() + if provisioning_state == 'failed': + progress_indicator.stop() + raise CLIError('The operation failed') + if ((wait_for_created or wait_for_updated) and provisioning_state == 'succeeded') or \ + custom_condition and bool(verify_property(instance, custom_condition)): + progress_indicator.end() + return None + except (ClientException, HttpResponseError) as ex: + progress_indicator.stop() + if getattr(ex, 'status_code', None) == 404: + if wait_for_deleted: + return None + if not any([wait_for_created, wait_for_exists, custom_condition]): + raise + else: + raise + except Exception: # pylint: disable=broad-except + progress_indicator.stop() + raise + + time.sleep(interval) + + progress_indicator.end() + return CLIError('Wait operation timed-out after {} seconds'.format(timeout)) + + @staticmethod + def _get_provisioning_state(instance): + provisioning_state = getattr(instance, 'provisioning_state', None) + if not provisioning_state: + # some SDK, like resource-group, has 'provisioning_state' under 'properties' + properties = getattr(instance, 'properties', None) + if properties: + provisioning_state = getattr(properties, 'provisioning_state', None) + # some SDK, like keyvault, has 'provisioningState' under 'properties.additional_properties' + if not provisioning_state: + additional_properties = getattr(properties, 'additional_properties', {}) + provisioning_state = additional_properties.get('provisioningState') + return provisioning_state + + def arguments_loader(self): + """ Callback function of CLICommand arguments_loader """ + cmd_args = self.load_getter_op_arguments(self.op_path) + + group_name = 'Wait Condition' + cmd_args['timeout'] = CLICommandArgument( + 'timeout', options_list=['--timeout'], default=3600, arg_group=group_name, type=int, + help='maximum wait in seconds' + ) + cmd_args['interval'] = CLICommandArgument( + 'interval', options_list=['--interval'], default=30, arg_group=group_name, type=int, + help='polling interval in seconds' + ) + cmd_args['deleted'] = CLICommandArgument( + 'deleted', options_list=['--deleted'], action='store_true', arg_group=group_name, + help='wait until deleted' + ) + cmd_args['created'] = CLICommandArgument( + 'created', options_list=['--created'], action='store_true', arg_group=group_name, + help="wait until created with 'provisioningState' at 'Succeeded'" + ) + cmd_args['updated'] = CLICommandArgument( + 'updated', options_list=['--updated'], action='store_true', arg_group=group_name, + help="wait until updated with provisioningState at 'Succeeded'" + ) + cmd_args['exists'] = CLICommandArgument( + 'exists', options_list=['--exists'], action='store_true', arg_group=group_name, + help="wait until the resource exists" + ) + cmd_args['custom'] = CLICommandArgument( + 'custom', options_list=['--custom'], arg_group=group_name, + help="Wait until the condition satisfies a custom JMESPath query. E.g. " + "provisioningState!='InProgress', " + "instanceView.statuses[?code=='PowerState/running']" + ) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + """ Callback function of CLICommand description_loader """ + return self.load_op_handler_description()