diff --git a/doc/generating_configuration_guidance.md b/doc/generating_configuration_guidance.md new file mode 100644 index 00000000000..25fdaf3f604 --- /dev/null +++ b/doc/generating_configuration_guidance.md @@ -0,0 +1,545 @@ +# Generating Configuration Guidance + +[Azdev](https://github.com/Azure/azure-cli-dev-tools) supports to generate a snapshot of command interfaces for a specified module since version 0.2.0. + +## Output + +### Folder structure +After generation, there will be a folder named `configuration` under the module dir. +And the subdirectories of `configuration` are named by profiles, such as `latest`, `2020-09-01-hybrid` and `2019-03-01-hybrid`. +Each profile named folders contain two json files named `commands.json` and `examples.json` respectively. + +``` +- + - ... + - configuration + - latest + - commands.json + - examples.json + - 2020-09-01-hybrid + - commands.json + - examples.json + - + - commands.json + - examples.json +``` + +### Simple Schema of `commands.json` + +The schema of commands is a tree composed of `command group` nodes, `command` nodes and `argument` nodes. +Its root is a command group node names `az`. In our example, it's the father of sub command group names `network`, +and `network` is the father of command nodes (`list-service-aliases` and `list-service-tags`) and +command group nodes (`application-gateway`, `dns` and `lb`), and a command node, such as `create`, is the father of argument nodes. + +``` +{ + "az": { + "full-name": "", + "command-groups": { + "network": { + "full-name": "network", + "help": "Manage Azure Network resources.", + "commands": { + "list-service-aliases": { + "full-name": "network list-service-aliases", + ... + }, + "list-service-tags": { + "full-name": "network list-service-tags", + ... + } + }, + "command-groups": { + "application-gateway": { + "full-name": "network application-gateway", + "help": { + "short-summary": "Manage application-level routing and load balancing services.", + "long-summary": "To learn more about Application Gateway, visit https://docs.microsoft.com/azure/application-gateway/application-gateway-create-gateway-cli" + }, + "commands": { + "create": { + "full-name": "network application-gateway create", + "help": "Create an application gateway.", + "operation": "@.custom#create_application_gateway", + "arguments": { + "application_gateway_name": { + ... + }, + "connection_draining_timeout": { + "min-api": "2016-12-01", + "options": [ + "--connection-draining-timeout" + ], + "help": "The time in seconds after a backend server is removed during which on open connection remains active. Range: 0 (disabled) to 3600", + "arg-group": "Gateway" + } + } + }, + "delete": { + "full-name": "network application-gateway delete", + ... + } + }, + "command-groups": { + "address-pool": { + "full-name": "network application-gateway address-pool", + ... + }, + "auth-cert": { + "full-name": "network application-gateway auth-cert", + ... + } + } + }, + "dns": { + "full-name": "network dns", + ... + }, + "lb": { + "full-name": "network lb", + ... + } + } + } + } + } +} +``` + +### Simple Schema of `examples.json` + +The schema is also a tree composed of `command group` nodes and `command` nodes. The examples will be in the corresponding command node. + +``` +{ + "az": { + "command-groups": { + "network": { + "commands": { + "list-service-aliases": [ + { + "autogenerated": true, + "name": "List available service aliases in the region which can be used for Service Endpoint Policies. (autogenerated)", + "text": "az network list-service-aliases --location westus2" + } + ], + "list-service-tags": [ + { + "autogenerated": true, + "name": "Gets a list of service tag information resources. (autogenerated)", + "text": "az network list-service-tags --location westus2" + } + ], + "list-usages": [ + ... + ] + }, + "command-groups": { + "application-gateway": { + "commands": { + "create": [ + { + "name": "Create an application gateway with VMs as backend servers.", + "text": "az network application-gateway create -g MyResourceGroup -n MyAppGateway --capacity 2 --sku Standard_Medium --vnet-name MyVNet --subnet MySubnet --http-settings-cookie-based-affinity Enabled --public-ip-address MyAppGatewayPublicIp --servers 10.0.0.4 10.0.0.5" + }, + { + "autogenerated": true, + "name": "Create an application gateway. (autogenerated)", + "text": "az network application-gateway create --capacity 2 --frontend-port MyFrontendPort --http-settings-cookie-based-affinity Enabled --http-settings-port 80 --http-settings-protocol Http --location westus2 --name MyAppGateway --public-ip-address MyAppGatewayPublicIp --resource-group MyResourceGroup --sku Standard_Small --subnet MySubnet --vnet-name MyVNet" + } + ], + "delete": [ + ... + ] + }, + "command-groups": { + "address-pool": { + ... + }, + "auth-cert": { + ... + } + } + }, + "dns": { + ... + }, + "lb": { + ... + } + } + } + } + } +} +``` + +## Code adaptions + +In order to support configuration, some adjustments to existing code are required. + +### Add decorators for special functions and classes + +This decorators can be imported from package `azure.cli.core.translator`. + +#### `client_factory` property + +The `client_factory` property used in command definition. + +If the value of `client_factory` is a function +```python +from azure.cli.command_modules.network._client_factory import cf_application_gateways + +network_ag_sdk = CliCommandType( + operations_tmpl='azure.mgmt.network.operations#ApplicationGatewaysOperations.{}', + client_factory=cf_application_gateways +) + +with self.command_group('network application-gateway waf-config') as g: + g.custom_show_command('show', 'show_ag_waf_config') + g.custom_command('list-rule-sets', 'list_ag_waf_rule_sets', client_factory=cf_application_gateways) +``` +That function should be decorated by `client_factory_func` +```python +from azure.cli.core.translator import client_factory_func + + +@client_factory_func +def cf_application_gateways(cli_ctx, _): + return network_client_factory(cli_ctx).application_gateways +``` + +#### `exception_handler` property +The `exception_handler` property used in command definition. + +If the value of `exception_handler` is a function +```python +from azure.cli.core.commands.arm import handle_template_based_exception + +with self.command_group('network lb', network_lb_sdk) as g: + g.custom_command('create', 'create_load_balancer', exception_handler=handle_template_based_exception) +``` +That function should be decorated by `exception_handler_func` +```python +from azure.cli.core.translator import exception_handler_func + + +@exception_handler_func +def handle_template_based_exception(ex): + try: + raise CLIError(ex.inner_exception.error.message) + except AttributeError: + raise CLIError(ex) +``` + +#### `transformer` and `table_transformer` properties +The `transformer` and `table_transformer` properties are used in command definition. + +If the value is a function +```python +from azure.cli.command_modules.network._format import transform_network_usage_list, transform_network_usage_table + +with self.command_group('network') as g: + g.command('list-usages', 'list', transform=transform_network_usage_list, table_transformer=transform_network_usage_table) +``` +Those function should be decorated by `transformer_func` +```python +from azure.cli.core.translator import transformer_func + + +@transformer_func +def transform_network_usage_list(result): + result = list(result) + for item in result: + item.current_value = str(item.current_value) + item.limit = str(item.limit) + item.local_name = item.name.localized_value + return result + + +@transformer_func +def transform_network_usage_table(result): + transformed = [] + for item in result: + transformed.append(OrderedDict([ + ('Name', item['localName']), + ('CurrentValue', item['currentValue']), + ('Limit', item['limit']) + ])) + return transformed +``` + +#### `validator` property + +The `validator` property is used both in argument definition and command definition. + +If the value of `validator` is a function +```python +from azure.cli.core.commands.validators import get_default_location_from_resource_group + +with self.argument_context('network') as c: + c.argument('location', arg_type=get_location_type(self.cli_ctx), validator=get_default_location_from_resource_group) +``` +That function should be decorated by `validator_func` +```python +from azure.cli.core.translator import validator_func + + +@validator_func +def get_default_location_from_resource_group(cmd, namespace): + if not namespace.location: + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from msrestazure.azure_exceptions import CloudError + from knack.util import CLIError + + resource_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) + try: + rg = resource_client.resource_groups.get(namespace.resource_group_name) + except CloudError as ex: + raise CLIError('error retrieving default location: {}'.format(ex.message)) + namespace.location = rg.location # pylint: disable=no-member + logger.debug("using location '%s' from resource group '%s'", namespace.location, rg.name) + +``` + +If the value of `validator` is a returned value of a function +```python +from azure.cli.command_modules.network._validators import get_public_ip_validator + + +with self.argument_context('network application-gateway frontend-ip create') as c: + c.argument('public_ip_address', validator=get_public_ip_validator()) + +``` +That function should be decorated by `validator_by_factory` +```python +from azure.cli.core.translator import validator_by_factory + + +@validator_by_factory +def get_public_ip_validator(has_type_field=False, allow_none=False, allow_new=False, default_none=False): + def simple_validator(cmd, namespace): + pass + + def complex_validator_with_type(cmd, namespace): + pass + + return complex_validator_with_type if has_type_field else simple_validator +``` + +#### For `action` property + +The `action` property is used in argument definition. + +If the value of `action` is a class +```python +with self.argument_context('network application-gateway') as c: + c.argument('trusted_client_cert', action=TrustedClientCertificateCreate) +``` +This class should be decorated by `action_class` +```python +from azure.cli.core.translator import action_class + + +@action_class +class TrustedClientCertificateCreate(argparse._AppendAction): + pass +``` + +If the value of `action` is a class returned by a function +```python +with self.argument_context('network application-gateway') as c: + c.argument('trusted_client_cert', action=build_client_certification_action('special_value')) +``` +This function should be decorated by `action_class_by_factory` +```python +from azure.cli.core.translator import action_class_by_factory + + +@action_class_by_factory +def build_client_certification_action(special_value): + class TrustedClientCertificateCreate(argparse._AppendAction): + pass + return TrustedClientCertificateCreate +``` + +#### For `arg_type` property + +The `arg_type` property is to define common used arguments. + +If the value of `arg_type` is a instance defined in previous. +```python +name_arg_type = CLIArgumentType(options_list=['--name', '-n'], metavar='NAME') + +with self.argument_context('network application-gateway ssl-policy predefined', min_api='2017-06-01') as c: + c.argument('predefined_policy_name', name_arg_type) + +with self.argument_context('network application-gateway ssl-policy', min_api='2017-06-01') as c: + c.argument('policy_name', name_arg_type) +``` +`CLIArgumentType` should be replaced by `register_arg_type`, and `register_arg_type` function needs a unique name in module to register this arg_type definition. +```python +from azure.cli.core.translator import register_arg_type +name_arg_type = register_arg_type('name_arg_type', options_list=['--name', '-n'], metavar='NAME') + +with self.argument_context('network application-gateway ssl-policy predefined', min_api='2017-06-01') as c: + c.argument('predefined_policy_name', arg_type=name_arg_type) + +with self.argument_context('network application-gateway ssl-policy', min_api='2017-06-01') as c: + c.argument('policy_name', arg_type=name_arg_type) +``` + +If the value of `arg_type` is a returned value of a function +```python + with self.argument_context('network application-gateway rewrite-rule', arg_group='URL Configuration') as c: + c.argument('enable_reroute', arg_type=get_three_state_flag()) + +``` +That function should be decorated by `arg_type_by_factory` +```python +from azure.cli.core.translator import arg_type_by_factory + + +@arg_type_by_factory +def get_three_state_flag(positive_label='true', negative_label='false', invert=False, return_label=False): + choices = [positive_label, negative_label] + action = get_three_state_action(positive_label=positive_label, negative_label=negative_label, invert=invert, + return_label=return_label) + params = { + 'choices': CaseInsensitiveList(choices), + 'nargs': '?', + 'action': action + } + return CLIArgumentType(**params) +``` + + +#### `type` property + +The `type` property is used in argument definition to convert the value of input. + +If the value of `type` is a function +```python +from azure.cli.core.commands.parameters import file_type + +with self.argument_context('network application-gateway create', arg_group='Gateway') as c: + c.argument('cert_data', options_list='--cert-file', type=file_type, completer=FilesCompleter()) +``` +That function should be decorated by `type_converter_func` +```python +from azure.cli.core.translator import type_converter_func + + +@type_converter_func +def file_type(path): + import os + return os.path.expanduser(path) +``` + +If the value of `type` is a returned value of a function +```python +from azure.cli.command_modules.monitor.actions import get_period_type + + +with self.argument_context('monitor alert update', arg_group='Condition') as c: + c.argument('period', type=get_period_type()) +``` +That function should be decorated by `type_converter_by_factory` +```python +from azure.cli.core.translator import type_converter_by_factory + + +@type_converter_by_factory +def get_period_type(as_timedelta=False): + + def period_type(value): + pass + + return period_type +``` + +Warning: Please use `json_object_type` defined in `azure.cli.core.commands` instead of `get_json_object` defined in `azure.cli.core.util` + + +#### `completer` property + +The `completer` property is used in argument definition. + +If the value of `completer` is a function +```python +from azure.cli.command_modules.network._completers import subnet_completion_list + +with self.argument_context('network lb create', arg_group='Subnet') as c: + c.argument('subnet', completer=subnet_completion_list) +``` +That function should be decorated by `completer_func` instead of `Completer` +```python +from azure.cli.core.translator import completer_func + + +@completer_func +def subnet_completion_list(cmd, prefix, namespace, **kwargs): + client = network_client_factory(cmd.cli_ctx) + if namespace.resource_group_name and namespace.virtual_network_name: + rg = namespace.resource_group_name + vnet = namespace.virtual_network_name + return [r.name for r in client.subnets.list(resource_group_name=rg, virtual_network_name=vnet)] + +``` + +If the value of `completer` is a returned value of a function +```python +from azure.cli.command_modules.network._completers import get_lb_subresource_completion_list + +with self.argument_context('network lb rule') as c: + c.argument('item_name', options_list=['--name', '-n'], completer=get_lb_subresource_completion_list('load_balancing_rules')) +``` +That function should be decorated by `completer_by_factory` +```python +from azure.cli.core.translator import completer_by_factory + + +@completer_by_factory +def get_lb_subresource_completion_list(prop): + + # @Completer # The 'Completer' decorator should be removed. + def completer(cmd, prefix, namespace, **kwargs): + client = network_client_factory(cmd.cli_ctx) + try: + lb_name = namespace.load_balancer_name + except AttributeError: + lb_name = namespace.resource_name + if namespace.resource_group_name and lb_name: + lb = client.load_balancers.get(namespace.resource_group_name, lb_name) + return [r.name for r in getattr(lb, prop)] + return completer +``` + +If the value of `completer` is provided by external library +```python +from argcomplete.completers import FilesCompleter +from argcomplete.completers import DirectoriesCompleter + + +with self.argument_context('network vnet-gateway root-cert create') as c: + c.argument('public_cert_data', type=file_type, completer=FilesCompleter()) + c.argument('cert_dir', type=file_type, completer=DirectoriesCompleter()) + +``` +Those import should be registered in file `azure.cli.core.translator.external_completer` and import from that file. +`FilesCompleter` and `DirectoriesCompleter` are the most popular external completer, so they've already been registered. + + +### Use `register_custom_resource_type` instead of `CustomResourceType` + +`CustomResourceType` is used in extension modules. +```python +from azure.cli.core.profiles import CustomResourceType +CUSTOM_MGMT_AKS_PREVIEW = CustomResourceType('azext_aks_preview.vendored_sdks.azure_mgmt_preview_aks', 'ContainerServiceClient') +``` +It should be replaced by `register_custom_resource_type` with unique registered name provided. +```python +from azure.cli.core.translator import register_custom_resource_type +CUSTOM_MGMT_AKS_PREVIEW = register_custom_resource_type( + 'CUSTOM_MGMT_AKS_PREVIEW', 'azext_aks_preview.vendored_sdks.azure_mgmt_preview_aks', 'ContainerServiceClient') +``` diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index ec34afbd1de..1ec58aa7332 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -774,6 +774,7 @@ def command_group(self, group_name, command_type=None, **kwargs): def argument_context(self, scope, **kwargs): return self._argument_context_cls(self, scope, **kwargs) + # keyvault and batch are using this function def _cli_command(self, name, operation=None, handler=None, argument_loader=None, description_loader=None, **kwargs): from knack.deprecation import Deprecated @@ -833,6 +834,30 @@ def default_description_loader(): handler or default_command_handler, **kwargs) + def add_cli_command(self, name, command_operation, **kwargs): + 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 087557cf1c1..8f90af6515f 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -1160,6 +1160,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, @@ -1247,16 +1253,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) + operation = operations_tmpl.format(method_name) + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = CommandOperation( + ctx=self.command_loader, + operation=operation, + **merged_kwargs + ) + self.command_loader.add_cli_command(command_name, + command_operation=command_operation, + **merged_kwargs) return command_name # pylint: disable=no-self-use @@ -1286,7 +1300,7 @@ 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)) @@ -1296,9 +1310,9 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, 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), + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = GenericUpdateCommandOperation( + ctx=self.command_loader, getter_op=getter_op, setter_op=setter_op, setter_arg_name=setter_arg_name, @@ -1306,7 +1320,11 @@ def generic_update_command(self, name, getter_name='get', getter_type=None, 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) @@ -1318,7 +1336,7 @@ 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) @@ -1326,8 +1344,16 @@ def _wait_command(self, name, getter_name='get', getter_type=None, custom_comman 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) + + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name + command_operation = WaitCommandOperation( + ctx=self.command_loader, + operation=getter_op, + **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) @@ -1336,16 +1362,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) + operation = 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( + ctx=self.command_loader, + operation=operation, + **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 f78e9fb9da4..cf8f82dd0f1 100644 --- a/src/azure-cli-core/azure/cli/core/commands/arm.py +++ b/src/azure-cli-core/azure/cli/core/commands/arm.py @@ -7,22 +7,20 @@ import argparse from collections import OrderedDict -import copy import json import re -from six import string_types -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 azure.cli.core.translator import transformer_func, exception_handler_func 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 @@ -98,6 +96,7 @@ def build_parameters(self): return json.loads(json.dumps(self.parameters)) +@exception_handler_func def handle_template_based_exception(ex): try: raise CLIError(ex.inner_exception.error.message) @@ -134,6 +133,7 @@ def handle_long_running_operation_exception(ex): raise cli_error +@transformer_func def deployment_validate_table_format(result): if result.get('error', None): @@ -407,358 +407,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, string_types): - raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) - if not isinstance(setter_op, string_types): - raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) - if custom_function_op and not isinstance(custom_function_op, string_types): - 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, string_types): - 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, string_types): - 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..fdc5b76459a --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/commands/command_operation.py @@ -0,0 +1,489 @@ +# -------------------------------------------------------------------------------------------- +# 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 six import string_types, get_method_function +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: + + def __init__(self, ctx, **kwargs): + if not isinstance(ctx, AzCommandsLoader): + raise TypeError("'ctx' expected type '{}'. Got: '{}'".format(AzCommandsLoader.__name__, type(ctx))) + self.ctx = ctx + self.cmd = None + self.kwargs = kwargs + self.client_factory = kwargs.get('client_factory') + self.operation_group = kwargs.get('operation_group') + + @property + def cli_ctx(self): + return self.cmd.cli_ctx if self.cmd else self.ctx.cli_ctx + + def handler(self, command_args): + raise NotImplementedError() + + def arguments_loader(self): + raise NotImplementedError() + + def description_loader(self): + raise NotImplementedError() + + def get_op_handler(self, operation): + """ Import and load the operation handler """ + # 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 operation.startswith(rt.import_prefix + '.'): + operation = operation.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 = operation.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 get_method_function(handler) + except (ValueError, AttributeError): + raise ValueError("The operation '{}' is invalid.".format(operation)) + + def load_getter_op_arguments(self, getter_op, cmd_args=None): + op = self.get_op_handler(getter_op) + getter_args = dict( + extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + cmd_args = cmd_args or {} + cmd_args.update(getter_args) + cmd_args['cmd'] = CLICommandArgument('cmd', arg_type=ignore_type) + return cmd_args + + def apply_doc_string(self, handler): + return self.ctx._apply_doc_string(handler, self.kwargs) # pylint: disable=protected-access + + def load_op_description(self, handler=None): + if handler is None: + def default_handler(): + """""" # default_handler should have __doc__ property + handler = default_handler + self.apply_doc_string(handler) + return extract_full_summary_from_signature(handler) + + def resolve_client_arg_name(self, operation): + from azure.cli.core.commands.client_factory import resolve_client_arg_name + return resolve_client_arg_name(operation, self.kwargs) + + +class CommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("Operation must be a string. Got '{}'".format(operation)) + super(CommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): + from azure.cli.core.util import get_arg_list, augment_no_wait_handler_args + + op = self.get_op_handler(self.operation) + op_args = get_arg_list(op) + cmd = command_args.get('cmd') if 'cmd' in op_args else command_args.pop('cmd') + + client = self.client_factory(cmd.cli_ctx, command_args) if self.client_factory else None + supports_no_wait = self.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.operation) + if client_arg_name in op_args: + command_args[client_arg_name] = client + return op(**command_args) + + def arguments_loader(self): + op = self.get_op_handler(self.operation) + self.apply_doc_string(op) + cmd_args = list(extract_args_from_signature(op, excluded_params=self.ctx.excluded_command_handler_args)) + return cmd_args + + def description_loader(self): + op = self.get_op_handler(self.operation) + return self.load_op_description(op) + + +class WaitCommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("operation must be a string. Got '{}'".format(operation)) + super(WaitCommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): # pylint: disable=too-many-statements + 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.operation) + + 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.operation) + 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.operation) + + 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): + cmd_args = self.load_getter_op_arguments(self.operation) + + 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): + return self.load_op_description() + + +class ShowCommandOperation(BaseCommandOperation): + + def __init__(self, ctx, operation, **kwargs): + if not isinstance(operation, string_types): + raise TypeError("operation must be a string. Got '{}'".format(operation)) + super(ShowCommandOperation, self).__init__(ctx, **kwargs) + self.operation = operation + + def handler(self, command_args): + from azure.cli.core.commands.arm import show_exception_handler, EXCLUDED_NON_CLIENT_PARAMS + + op = self.get_op_handler(self.operation) + 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.operation) + 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.operation) + try: + return op(**command_args) + except Exception as ex: # pylint: disable=broad-except + show_exception_handler(ex) + + def arguments_loader(self): + cmd_args = self.load_getter_op_arguments(self.operation) + return [(k, v) for k, v in cmd_args.items()] + + def description_loader(self): + op = self.get_op_handler(self.operation) + return self.load_op_description(op) + + +class GenericUpdateCommandOperation(BaseCommandOperation): # pylint: disable=too-many-instance-attributes + + 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)) + + def __init__(self, ctx, getter_op, setter_op, setter_arg_name, custom_function_op, child_collection_prop_name, + child_collection_key, child_arg_name, **kwargs): + if not isinstance(getter_op, string_types): + raise TypeError("Getter operation must be a string. Got '{}'".format(getter_op)) + if not isinstance(setter_op, string_types): + raise TypeError("Setter operation must be a string. Got '{}'".format(setter_op)) + if custom_function_op and not isinstance(custom_function_op, string_types): + raise TypeError("Custom function operation must be a string. Got '{}'".format(custom_function_op)) + super(GenericUpdateCommandOperation, self).__init__(ctx, **kwargs) + + self.getter_operation = getter_op + self.setter_operation = setter_op + self.custom_function_operation = custom_function_op + 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 + 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_handler_and_args(command_args, self.getter_operation) + + 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_operation: + custom_function, custom_func_args = self._extract_handler_and_args( + command_args, self.custom_function_operation) + 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_handler_and_args(command_args, self.setter_operation) + + 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_handler_and_args(self, args, operation): + 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(operation) + op = self.get_op_handler(operation) + 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): + from azure.cli.core.commands.arm import set_usage, add_usage, remove_usage + + arguments = self.load_getter_op_arguments(self.getter_operation) + arguments.update(self._set_arguments_loader()) + arguments.update(self._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(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 _set_arguments_loader(self): + op = self.get_op_handler(self.setter_operation) + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def _function_arguments_loader(self): + if not self.custom_function_operation: + return {} + op = self.get_op_handler(self.custom_function_operation) + self.apply_doc_string(op) # pylint: disable=protected-access + return dict(extract_args_from_signature(op, excluded_params=EXCLUDED_PARAMS)) + + def description_loader(self): + return self.load_op_description() diff --git a/src/azure-cli-core/azure/cli/core/commands/parameters.py b/src/azure-cli-core/azure/cli/core/commands/parameters.py index 075fab99a4e..5ecb89a7878 100644 --- a/src/azure-cli-core/azure/cli/core/commands/parameters.py +++ b/src/azure-cli-core/azure/cli/core/commands/parameters.py @@ -6,14 +6,17 @@ import argparse import platform +from enum import Enum + from azure.cli.core import EXCLUDED_PARAMS from azure.cli.core.commands.constants import CLI_PARAM_KWARGS, CLI_POSITIONAL_PARAM_KWARGS from azure.cli.core.commands.validators import validate_tag, validate_tags, generate_deployment_name -from azure.cli.core.decorators import Completer from azure.cli.core.profiles import ResourceType from azure.cli.core.local_context import LocalContextAttribute, LocalContextAction, ALL - +from azure.cli.core.translator import (action_class, action_class_by_factory, + completer_func, completer_by_factory, type_converter_func, + type_converter_by_factory, register_arg_type, arg_type_by_factory) from knack.arguments import ( CLIArgumentType, CaseInsensitiveList, ignore_type, ArgumentsContext) from knack.log import get_logger @@ -28,28 +31,16 @@ def get_subscription_locations(cli_ctx): return list(subscription_client.subscriptions.list_locations(subscription_id)) -@Completer +@completer_func def get_location_completion_list(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument result = get_subscription_locations(cmd.cli_ctx) return [item.name for item in result] -# pylint: disable=redefined-builtin -def get_datetime_type(help=None, date=True, time=True, timezone=True): - - help_string = help + ' ' if help else '' - accepted_formats = [] - if date: - accepted_formats.append('date (yyyy-mm-dd)') - if time: - accepted_formats.append('time (hh:mm:ss.xxxxx)') - if timezone: - accepted_formats.append('timezone (+/-hh:mm)') - help_string = help_string + 'Format: ' + ' '.join(accepted_formats) - +@action_class_by_factory +def get_datetime_action(date=True, time=True, timezone=True): # pylint: disable=too-few-public-methods class DatetimeAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): """ Parse a date value and return the ISO8601 string. """ import dateutil.parser @@ -63,6 +54,15 @@ def __call__(self, parser, namespace, values, option_string=None): except ValueError: pass + accepted_formats = [] + if date: + accepted_formats.append('date (yyyy-mm-dd)') + if time: + accepted_formats.append('time (hh:mm:ss.xxxxx)') + if timezone: + accepted_formats.append('timezone (+/-hh:mm)') + help_string = ' '.join(accepted_formats) + # TODO: custom parsing attempts here if not dt_val: raise CLIError("Unable to parse: '{}'. Expected format: {}".format(value_string, help_string)) @@ -82,15 +82,38 @@ def __call__(self, parser, namespace, values, option_string=None): iso_string = dt_val.isoformat() setattr(namespace, self.dest, iso_string) + return DatetimeAction - return CLIArgumentType(action=DatetimeAction, nargs='+', help=help_string) +# pylint: disable=redefined-builtin +@arg_type_by_factory +def get_datetime_type(help=None, date=True, time=True, timezone=True): + help_string = help + ' ' if help else '' + accepted_formats = [] + if date: + accepted_formats.append('date (yyyy-mm-dd)') + if time: + accepted_formats.append('time (hh:mm:ss.xxxxx)') + if timezone: + accepted_formats.append('timezone (+/-hh:mm)') + help_string = help_string + 'Format: ' + ' '.join(accepted_formats) + action = get_datetime_action(date=date, time=time, timezone=timezone) + return CLIArgumentType(action=action, nargs='+', help=help_string) + +@type_converter_func def file_type(path): import os return os.path.expanduser(path) +@type_converter_func +def json_object_type(json_string): + from azure.cli.core.util import get_json_object + return get_json_object(json_string) + + +@type_converter_by_factory def get_location_name_type(cli_ctx): def location_name_type(name): if ' ' in name: @@ -114,7 +137,7 @@ def get_resource_groups(cli_ctx): return list(rcf.resource_groups.list()) -@Completer +@completer_func def get_resource_group_completion_list(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument result = get_resource_groups(cmd.cli_ctx) return [item.name for item in result] @@ -138,9 +161,9 @@ def get_resources_in_subscription(cli_ctx, resource_type=None): return list(rcf.resources.list(filter=filter_str)) +@completer_by_factory def get_resource_name_completion_list(resource_type=None): - @Completer def completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument rg = getattr(namespace, 'resource_group_name', None) if rg: @@ -150,29 +173,18 @@ def completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argum return completer +@completer_by_factory def get_generic_completion_list(generic_list): - @Completer def completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument return generic_list return completer -def get_three_state_flag(positive_label='true', negative_label='false', invert=False, return_label=False): - """ Creates a flag-like argument that can also accept positive/negative values. This allows - consistency between create commands that typically use flags and update commands that require - positive/negative values without introducing breaking changes. Flag-like behavior always - implies the affirmative unless invert=True then invert the logic. - - positive_label: label for the positive value (ex: 'enabled') - - negative_label: label for the negative value (ex: 'disabled') - - invert: invert the boolean logic for the flag - - return_label: if true, return the corresponding label. Otherwise, return a boolean value - """ - choices = [positive_label, negative_label] - +@action_class_by_factory +def get_three_state_action(positive_label='true', negative_label='false', invert=False, return_label=False): # pylint: disable=too-few-public-methods class ThreeStateAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): values = values or positive_label is_positive = values.lower() == positive_label.lower() @@ -183,58 +195,82 @@ def __call__(self, parser, namespace, values, option_string=None): else: set_val = is_positive setattr(namespace, self.dest, set_val) + return ThreeStateAction + +@arg_type_by_factory +def get_three_state_flag(positive_label='true', negative_label='false', invert=False, return_label=False): + """ Creates a flag-like argument that can also accept positive/negative values. This allows + consistency between create commands that typically use flags and update commands that require + positive/negative values without introducing breaking changes. Flag-like behavior always + implies the affirmative unless invert=True then invert the logic. + - positive_label: label for the positive value (ex: 'enabled') + - negative_label: label for the negative value (ex: 'disabled') + - invert: invert the boolean logic for the flag + - return_label: if true, return the corresponding label. Otherwise, return a boolean value + """ + choices = [positive_label, negative_label] + action = get_three_state_action(positive_label=positive_label, negative_label=negative_label, invert=invert, + return_label=return_label) params = { 'choices': CaseInsensitiveList(choices), 'nargs': '?', - 'action': ThreeStateAction + 'action': action } return CLIArgumentType(**params) + # pylint: disable=too-few-public-methods + + +@action_class +class EnumAction(argparse.Action): + + def __call__(self, parser, args, values, option_string=None): + + def _get_value(val): + return next((x for x in self.choices if x.lower() == val.lower()), val) + if isinstance(values, list): + values = [_get_value(v) for v in values] + else: + values = _get_value(values) + setattr(args, self.dest, values) + + +@arg_type_by_factory def get_enum_type(data, default=None): """ Creates the argparse choices and type kwargs for a supplied enum type or list of strings. """ if not data: return None + if isinstance(data, type) and issubclass(data, Enum): + enum_model = data + else: + enum_model = None + # transform enum types, otherwise assume list of string choices try: choices = [x.value for x in data] except AttributeError: choices = data - # pylint: disable=too-few-public-methods - class DefaultAction(argparse.Action): - - def __call__(self, parser, args, values, option_string=None): - - def _get_value(val): - return next((x for x in self.choices if x.lower() == val.lower()), val) - - if isinstance(values, list): - values = [_get_value(v) for v in values] - else: - values = _get_value(values) - setattr(args, self.dest, values) - - def _type(value): - return next((x for x in choices if x.lower() == value.lower()), value) if value else value - default_value = None if default: default_value = next((x for x in choices if x.lower() == default.lower()), None) if not default_value: raise CLIError("Command authoring exception: unrecognized default '{}' from choices '{}'" .format(default, choices)) - arg_type = CLIArgumentType(choices=CaseInsensitiveList(choices), action=DefaultAction, default=default_value) + arg_type = CLIArgumentType(choices=CaseInsensitiveList(choices), action=EnumAction, enum_model=enum_model, + default=default_value) else: - arg_type = CLIArgumentType(choices=CaseInsensitiveList(choices), action=DefaultAction) + arg_type = CLIArgumentType(choices=CaseInsensitiveList(choices), action=EnumAction, enum_model=enum_model) return arg_type # GLOBAL ARGUMENT DEFINITIONS -resource_group_name_type = CLIArgumentType( +resource_group_name_type = register_arg_type( + 'resource_group_name_type', options_list=['--resource-group', '-g'], completer=get_resource_group_completion_list, id_part='resource_group', @@ -246,9 +282,10 @@ def _type(value): scopes=[ALL] )) -name_type = CLIArgumentType(options_list=['--name', '-n'], help='the primary resource name') +name_type = register_arg_type('name_type', options_list=['--name', '-n'], help='the primary resource name') +@arg_type_by_factory def get_location_type(cli_ctx): location_type = CLIArgumentType( options_list=['--location', '-l'], @@ -266,7 +303,8 @@ def get_location_type(cli_ctx): return location_type -deployment_name_type = CLIArgumentType( +deployment_name_type = register_arg_type( + 'deployment_name_type', help=argparse.SUPPRESS, required=False, validator=generate_deployment_name @@ -275,45 +313,53 @@ def get_location_type(cli_ctx): quotes = '""' if platform.system() == 'Windows' else "''" quote_text = 'Use {} to clear existing tags.'.format(quotes) -tags_type = CLIArgumentType( +tags_type = register_arg_type( + 'tags_type', validator=validate_tags, help="space-separated tags: key[=value] [key[=value] ...]. {}".format(quote_text), nargs='*' ) -tag_type = CLIArgumentType( +tag_type = register_arg_type( + 'tag_type', type=validate_tag, help="a single tag in 'key[=value]' format. {}".format(quote_text), nargs='?', const='' ) -no_wait_type = CLIArgumentType( +no_wait_type = register_arg_type( + 'no_wait_type', options_list=['--no-wait', ], help='do not wait for the long-running operation to finish', action='store_true' ) -zones_type = CLIArgumentType( +zones_type = register_arg_type( + 'zones_type', options_list=['--zones', '-z'], nargs='+', help='Space-separated list of availability zones into which to provision the resource.', choices=['1', '2', '3'] ) -zone_type = CLIArgumentType( +zone_type = register_arg_type( + 'zone_type', options_list=['--zone', '-z'], help='Availability zone into which to provision the resource.', choices=['1', '2', '3'], nargs=1 ) -vnet_name_type = CLIArgumentType( +vnet_name_type = register_arg_type( + 'vnet_name_type', local_context_attribute=LocalContextAttribute(name='vnet_name', actions=[LocalContextAction.GET]) ) -subnet_name_type = CLIArgumentType( - local_context_attribute=LocalContextAttribute(name='subnet_name', actions=[LocalContextAction.GET])) +subnet_name_type = register_arg_type( + 'subnet_name_type', + local_context_attribute=LocalContextAttribute(name='subnet_name', actions=[LocalContextAction.GET]) +) def patch_arg_make_required(argument): @@ -350,6 +396,10 @@ def _flatten_kwargs(self, kwargs, arg_type): if arg_type: arg_type_copy = arg_type.settings.copy() arg_type_copy.update(merged_kwargs) + if '_arg_type' in arg_type_copy: + raise KeyError('"_arg_type" is a reserved key') + # The key name `arg_type` will exist for nested arg_type, so use `_arg_type`. + arg_type_copy['_arg_type'] = arg_type return arg_type_copy return merged_kwargs diff --git a/src/azure-cli-core/azure/cli/core/commands/validators.py b/src/azure-cli-core/azure/cli/core/commands/validators.py index 5db086822f1..4f7b63515b9 100644 --- a/src/azure-cli-core/azure/cli/core/commands/validators.py +++ b/src/azure-cli-core/azure/cli/core/commands/validators.py @@ -8,7 +8,7 @@ import random from azure.cli.core.profiles import ResourceType - +from azure.cli.core.translator import validator_func from knack.log import get_logger from knack.validators import DefaultStr, DefaultInt # pylint: disable=unused-import @@ -35,6 +35,7 @@ class IterateValue(list): pass # pylint: disable=unnecessary-pass +@validator_func def validate_tags(ns): """ Extracts multiple space-separated tags in key[=value] format """ if isinstance(ns.tags, list): @@ -62,12 +63,14 @@ def validate_key_value_pairs(string): return result +@validator_func def generate_deployment_name(namespace): if not namespace.deployment_name: namespace.deployment_name = \ 'azurecli{}{}'.format(str(time.time()), str(random.randint(1, 100000))) +@validator_func def get_default_location_from_resource_group(cmd, namespace): if not namespace.location: from azure.cli.core.commands.client_factory import get_mgmt_service_client diff --git a/src/azure-cli-core/azure/cli/core/translator/__init__.py b/src/azure-cli-core/azure/cli/core/translator/__init__.py new file mode 100644 index 00000000000..d65a79a009e --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/translator/__init__.py @@ -0,0 +1,25 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-import + +# action +from ._decorators import action_class, action_class_by_factory +# arg_type +from ._decorators import register_arg_type, arg_type_by_factory +# client_factory +from ._decorators import client_factory_func +# completer +from ._decorators import completer_func, completer_by_factory +# exception_handler +from ._decorators import exception_handler_func +# resource_type +from ._decorators import register_custom_resource_type +# transformer +from ._decorators import transformer_func +# type_converter +from ._decorators import type_converter_func, type_converter_by_factory +# validator +from ._decorators import validator_func, validator_by_factory diff --git a/src/azure-cli-core/azure/cli/core/translator/_decorators.py b/src/azure-cli-core/azure/cli/core/translator/_decorators.py new file mode 100644 index 00000000000..66293c75b93 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/translator/_decorators.py @@ -0,0 +1,78 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.arguments import CLIArgumentType +from azure.cli.core.profiles import CustomResourceType +from azure.cli.core.decorators import Completer + +# The following functions will be hooked by functions defined in azdev.operations.translator.hook, +# when config generation task is launched. + + +# action +def action_class(action_cls): + return action_cls + + +def action_class_by_factory(factory): + return factory + + +# arg_type +def register_arg_type(register_name, overrides=None, **kwargs): # pylint: disable=unused-argument + return CLIArgumentType(overrides=overrides, **kwargs) + + +def arg_type_by_factory(factory): + return factory + + +# client_factory +def client_factory_func(func): + return func + + +# completer +def completer_func(func): + return Completer(func) + + +def completer_by_factory(factory): + def wrapper(*args, **kwargs): + func = factory(*args, **kwargs) + return Completer(func) + return wrapper + + +# exception_handler +def exception_handler_func(func): + return func + + +# resource_type +def register_custom_resource_type(register_name, import_prefix, client_name): # pylint: disable=unused-argument + return CustomResourceType(import_prefix=import_prefix, client_name=client_name) + + +# transformer +def transformer_func(func): + return func + + +# type_converter +def type_converter_func(func): + return func + + +def type_converter_by_factory(factory): + return factory + + +# validator +def validator_func(func): + return func + + +def validator_by_factory(factory): + return factory diff --git a/src/azure-cli-core/azure/cli/core/translator/external_completer.py b/src/azure-cli-core/azure/cli/core/translator/external_completer.py new file mode 100644 index 00000000000..0b5e800858f --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/translator/external_completer.py @@ -0,0 +1,21 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from argcomplete.completers import FilesCompleter +from argcomplete.completers import DirectoriesCompleter + + +# this function will be hooked in azdev +def _build_external_completer_instance(cls, args, kwargs): + return cls(*args, **kwargs) + + +def _external_completer_cls_wrapper(cls): + def wrapper(*args, **kwargs): + return _build_external_completer_instance(cls, args, kwargs) + return wrapper + + +FilesCompleter = _external_completer_cls_wrapper(FilesCompleter) +DirectoriesCompleter = _external_completer_cls_wrapper(DirectoriesCompleter)