1# --------------------------------------------------------------------------------------------
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License. See License.txt in the project root for license information.
4# --------------------------------------------------------------------------------------------
5
6# pylint: disable=too-many-lines
7
8import argparse
9from collections import OrderedDict
10import json
11import re
12
13from azure.cli.core import EXCLUDED_PARAMS
14from azure.cli.core.commands import LongRunningOperation
15from azure.cli.core.commands.client_factory import get_mgmt_service_client
16from azure.cli.core.commands.events import EVENT_INVOKER_PRE_LOAD_ARGUMENTS
17from azure.cli.core.commands.validators import IterateValue
18from azure.cli.core.util import shell_safe_json_parse, get_command_type_kwarg
19from azure.cli.core.profiles import ResourceType, get_sdk
20
21from knack.arguments import CLICommandArgument, ignore_type
22from knack.introspection import extract_args_from_signature
23from knack.log import get_logger
24from knack.util import todict, CLIError
25
26logger = get_logger(__name__)
27EXCLUDED_NON_CLIENT_PARAMS = list(set(EXCLUDED_PARAMS) - set(['self', 'client']))
28
29
30# pylint:disable=too-many-lines
31class ArmTemplateBuilder:
32
33    def __init__(self):
34        template = OrderedDict()
35        template['$schema'] = \
36            'https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#'
37        template['contentVersion'] = '1.0.0.0'
38        template['parameters'] = {}
39        template['variables'] = {}
40        template['resources'] = []
41        template['outputs'] = {}
42        self.template = template
43        self.parameters = OrderedDict()
44
45    def add_resource(self, resource):
46        self.template['resources'].append(resource)
47
48    def add_variable(self, key, value):
49        self.template['variables'][key] = value
50
51    def add_parameter(self, key, value):
52        self.template['parameters'][key] = value
53
54    def add_secure_parameter(self, key, value, description=None):
55        param = {
56            "type": "securestring",
57            "metadata": {
58                "description": description or 'Secure {}'.format(key)
59            }
60        }
61        self.template['parameters'][key] = param
62        self.parameters[key] = {'value': value}
63
64    def add_id_output(self, key, provider, property_type, property_name):
65        new_output = {
66            key: {
67                'type': 'string',
68                'value': "[resourceId('{}/{}', '{}')]".format(
69                    provider, property_type, property_name)
70            }
71        }
72        self.template['outputs'].update(new_output)
73
74    def add_output(self, key, property_name, provider=None, property_type=None,
75                   output_type='string', path=None):
76
77        if provider and property_type:
78            value = "[reference(resourceId('{provider}/{type}', '{property}'),providers('{provider}', '{type}').apiVersions[0])".format(  # pylint: disable=line-too-long
79                provider=provider, type=property_type, property=property_name)
80        else:
81            value = "[reference('{}')".format(property_name)
82        value = '{}.{}]'.format(value, path) if path else '{}]'.format(value)
83        new_output = {
84            key: {
85                'type': output_type,
86                'value': value
87            }
88        }
89        self.template['outputs'].update(new_output)
90
91    def build(self):
92        return json.loads(json.dumps(self.template))
93
94    def build_parameters(self):
95        return json.loads(json.dumps(self.parameters))
96
97
98def raise_subdivision_deployment_error(error_message, error_code=None):
99    from azure.cli.core.azclierror import InvalidTemplateError, DeploymentError
100
101    if error_code == 'InvalidTemplateDeployment':
102        raise InvalidTemplateError(error_message)
103
104    raise DeploymentError(error_message)
105
106
107def handle_template_based_exception(ex):
108    try:
109        raise CLIError(ex.inner_exception.error.message)
110    except AttributeError:
111        if hasattr(ex, 'response'):
112            raise_subdivision_deployment_error(ex.response.internal_response.text, ex.error.code if ex.error else None)
113        else:
114            raise CLIError(ex)
115
116
117def handle_long_running_operation_exception(ex):
118    import azure.cli.core.telemetry as telemetry
119
120    telemetry.set_exception(
121        ex,
122        fault_type='failed-long-running-operation',
123        summary='Unexpected client exception in {}.'.format(LongRunningOperation.__name__))
124
125    message = getattr(ex, 'message', ex)
126    error_message = 'Deployment failed.'
127
128    try:
129        correlation_id = ex.response.headers['x-ms-correlation-request-id']
130        error_message = '{} Correlation ID: {}.'.format(error_message, correlation_id)
131    except:  # pylint: disable=bare-except
132        pass
133
134    try:
135        inner_message = json.loads(ex.response.text)['error']['details'][0]['message']
136        error_message = '{} {}'.format(error_message, inner_message)
137    except:  # pylint: disable=bare-except
138        error_message = '{} {}'.format(error_message, message)
139
140    cli_error = CLIError(error_message)
141    # capture response for downstream commands (webapp) to dig out more details
142    setattr(cli_error, 'response', getattr(ex, 'response', None))
143    raise cli_error
144
145
146def deployment_validate_table_format(result):
147
148    if result.get('error', None):
149        error_result = OrderedDict()
150        error_result['result'] = result['error']['code']
151        try:
152            tracking_id = re.match(r".*(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})", str(result['error']['message'])).group(1)
153            error_result['trackingId'] = tracking_id
154        except:  # pylint: disable=bare-except
155            pass
156        try:
157            error_result['message'] = result['error']['details'][0]['message']
158        except:  # pylint: disable=bare-except
159            error_result['message'] = result['error']['message']
160        return error_result
161    if result.get('properties', None):
162        success_result = OrderedDict()
163        success_result['result'] = result['properties']['provisioningState']
164        success_result['correlationId'] = result['properties']['correlationId']
165        return success_result
166    return result
167
168
169class ResourceId(str):
170
171    def __new__(cls, val):
172        from msrestazure.tools import is_valid_resource_id
173        if not is_valid_resource_id(val):
174            raise ValueError()
175        return str.__new__(cls, val)
176
177
178def resource_exists(cli_ctx, resource_group, name, namespace, type, **_):  # pylint: disable=redefined-builtin
179    ''' Checks if the given resource exists. '''
180    odata_filter = "resourceGroup eq '{}' and name eq '{}'" \
181        " and resourceType eq '{}/{}'".format(resource_group, name, namespace, type)
182    client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES).resources
183    existing = len(list(client.list(filter=odata_filter))) == 1
184    return existing
185
186
187# pylint: disable=too-many-statements
188def register_ids_argument(cli_ctx):
189
190    from knack import events
191
192    ids_metadata = {}
193
194    def add_ids_arguments(_, **kwargs):  # pylint: disable=unused-argument
195
196        command_table = kwargs.get('commands_loader').command_table
197
198        if not command_table:
199            return
200
201        for command in command_table.values():
202
203            # Somewhat blunt hammer, but any create commands will not have an automatic id parameter
204            if command.name.split()[-1] == 'create':
205                continue
206
207            # Only commands with a resource name are candidates for an id parameter
208            id_parts = [a.type.settings.get('id_part') for a in command.arguments.values()]
209            if 'name' not in id_parts and 'resource_name' not in id_parts:
210                continue
211
212            group_name = 'Resource Id'
213
214            # determine which arguments are required and optional and store in ids_metadata
215            ids_metadata[command.name] = {'required': [], 'optional': []}
216            for arg in [a for a in command.arguments.values() if a.type.settings.get('id_part')]:
217                if arg.options.get('required', False):
218                    ids_metadata[command.name]['required'].append(arg.name)
219                else:
220                    ids_metadata[command.name]['optional'].append(arg.name)
221                arg.required = False
222                arg.arg_group = group_name
223
224            # retrieve existing `ids` arg if it exists
225            id_arg = command.loader.argument_registry.arguments[command.name].get('ids', None)
226            deprecate_info = id_arg.settings.get('deprecate_info', None) if id_arg else None
227            id_kwargs = {
228                'metavar': 'ID',
229                'help': "One or more resource IDs (space-delimited). "
230                        "It should be a complete resource ID containing all information of '{gname}' arguments. "
231                        "You should provide either --ids or other '{gname}' arguments.".format(gname=group_name),
232                'dest': 'ids' if id_arg else '_ids',
233                'deprecate_info': deprecate_info,
234                'is_preview': id_arg.settings.get('is_preview', None) if id_arg else None,
235                'is_experimental': id_arg.settings.get('is_experimental', None) if id_arg else None,
236                'nargs': '+',
237                'arg_group': group_name
238            }
239            command.add_argument('ids', '--ids', **id_kwargs)
240
241    def parse_ids_arguments(_, command, args):
242        namespace = args
243        cmd = namespace._cmd  # pylint: disable=protected-access
244
245        # some commands have custom IDs and parsing. This will not work for that.
246        if not ids_metadata.get(command, None):
247            return
248
249        ids = getattr(namespace, 'ids', getattr(namespace, '_ids', None))
250        required_args = [cmd.arguments[x] for x in ids_metadata[command]['required']]
251        optional_args = [cmd.arguments[x] for x in ids_metadata[command]['optional']]
252        combined_args = required_args + optional_args
253
254        if not ids:
255            # ensure the required parameters are provided if --ids is not
256            errors = [arg for arg in required_args if getattr(namespace, arg.name, None) is None]
257            if errors:
258                missing_required = ' '.join((arg.options_list[0] for arg in errors))
259                raise CLIError('({} | {}) are required'.format(missing_required, '--ids'))
260            return
261
262        # show warning if names are used in conjunction with --ids
263        other_values = {arg.name: {'arg': arg, 'value': getattr(namespace, arg.name, None)}
264                        for arg in combined_args}
265        for _, data in other_values.items():
266            if data['value'] and not getattr(data['value'], 'is_default', None):
267                logger.warning("option '%s' will be ignored due to use of '--ids'.",
268                               data['arg'].type.settings['options_list'][0])
269
270        # create the empty lists, overwriting any values that may already be there
271        for arg in combined_args:
272            setattr(namespace, arg.name, IterateValue())
273
274        def assemble_json(ids):
275            lcount = 0
276            lind = None
277            for i, line in enumerate(ids):
278                if line == '[':
279                    if lcount == 0:
280                        lind = i
281                    lcount += 1
282                elif line == ']':
283                    lcount -= 1
284                    # final closed set of matching brackets
285                    if lcount == 0:
286                        left = lind
287                        right = i + 1
288                        l_comp = ids[:left]
289                        m_comp = [''.join(ids[left:right])]
290                        r_comp = ids[right:]
291                        ids = l_comp + m_comp + r_comp
292                        return assemble_json(ids)
293            # base case--no more merging required
294            return ids
295
296        # reassemble JSON strings from bash
297        ids = assemble_json(ids)
298
299        # expand the IDs into the relevant fields
300        full_id_list = []
301        for val in ids:
302            try:
303                # support piping values from JSON. Does not require use of --query
304                json_vals = json.loads(val)
305                if not isinstance(json_vals, list):
306                    json_vals = [json_vals]
307                for json_val in json_vals:
308                    if isinstance(json_val, dict) and 'id' in json_val:
309                        full_id_list += [json_val['id']]
310            except ValueError:
311                # supports piping of --ids to the command when using TSV. Requires use of --query
312                full_id_list = full_id_list + val.splitlines()
313        if full_id_list:
314            setattr(namespace, '_ids', full_id_list)
315
316        from azure.mgmt.core.tools import parse_resource_id, is_valid_resource_id
317        for val in full_id_list:
318            if not is_valid_resource_id(val):
319                raise CLIError('invalid resource ID: {}'.format(val))
320            # place the ID parts into the correct property lists
321            parts = parse_resource_id(val)
322            for arg in combined_args:
323                id_part = arg.type.settings.get('id_part')
324                id_value = parts.get(id_part, None)
325                if id_value is None:
326                    argument_name = arg.type.settings.get('options_list')[0]
327                    raise CLIError("Argument {arg_name} cannot be derived from ID {id}. "
328                                   "Please provide a complete resource ID "
329                                   "containing all information of '{group_name}' "
330                                   "arguments. ".format(id=val,
331                                                        arg_name=argument_name,
332                                                        group_name=arg.arg_group))
333                getattr(namespace, arg.name).append(id_value)
334
335        # support deprecating --ids
336        deprecate_info = cmd.arguments['ids'].type.settings.get('deprecate_info')
337        if deprecate_info:
338            if not hasattr(namespace, '_argument_deprecations'):
339                setattr(namespace, '_argument_deprecations', [deprecate_info])
340            else:
341                namespace._argument_deprecations.append(deprecate_info)  # pylint: disable=protected-access
342
343    cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, add_ids_arguments)
344    cli_ctx.register_event(events.EVENT_INVOKER_POST_PARSE_ARGS, parse_ids_arguments)
345
346
347def register_global_subscription_argument(cli_ctx):
348
349    def add_subscription_parameter(_, **kwargs):
350
351        from azure.cli.core._completers import get_subscription_id_list
352
353        class SubscriptionNameOrIdAction(argparse.Action):  # pylint:disable=too-few-public-methods
354
355            def __call__(self, parser, namespace, value, option_string=None):
356                from azure.cli.core._profile import Profile
357                profile = Profile(cli_ctx=namespace._cmd.cli_ctx)  # pylint: disable=protected-access
358                subscriptions_list = profile.load_cached_subscriptions()
359                sub_id = None
360                for sub in subscriptions_list:
361                    match_val = value.lower()
362                    if sub['id'].lower() == match_val or sub['name'].lower() == match_val:
363                        sub_id = sub['id']
364                        break
365                if not sub_id:
366                    logger.warning("Subscription '%s' not recognized.", value)
367                    sub_id = value
368                namespace._subscription = sub_id  # pylint: disable=protected-access
369
370        commands_loader = kwargs['commands_loader']
371        cmd_tbl = commands_loader.command_table
372
373        default_sub_kwargs = {
374            'help': 'Name or ID of subscription. You can configure the default subscription '
375                    'using `az account set -s NAME_OR_ID`',
376            'completer': get_subscription_id_list,
377            'arg_group': 'Global',
378            'action': SubscriptionNameOrIdAction,
379            'configured_default': 'subscription',
380            'id_part': 'subscription'
381        }
382
383        for _, cmd in cmd_tbl.items():
384            cmd.add_argument('_subscription', *['--subscription'], **default_sub_kwargs)
385
386    cli_ctx.register_event(EVENT_INVOKER_PRE_LOAD_ARGUMENTS, add_subscription_parameter)
387
388
389add_usage = '--add property.listProperty <key=value, string or JSON string>'
390set_usage = '--set property1.property2=<value>'
391remove_usage = '--remove property.list <indexToRemove> OR --remove propertyToRemove'
392
393
394def _get_operations_tmpl(cmd, custom_command=False):
395    operations_tmpl = cmd.command_kwargs.get('operations_tmpl') or \
396        cmd.command_kwargs.get(get_command_type_kwarg(custom_command)).settings['operations_tmpl']
397    if not operations_tmpl:
398        raise CLIError("command authoring error: cmd '{}' does not have an operations_tmpl.".format(cmd.name))
399    return operations_tmpl
400
401
402def _get_client_factory(_, custom_command=False, **kwargs):
403    command_type = kwargs.get(get_command_type_kwarg(custom_command), None)
404    factory = kwargs.get('client_factory', None)
405    if not factory and command_type:
406        factory = command_type.settings.get('client_factory', None)
407    return factory
408
409
410def get_arguments_loader(context, getter_op, cmd_args=None, operation_group=None):
411    getter_args = dict(extract_args_from_signature(context.get_op_handler(getter_op, operation_group=operation_group),
412                                                   excluded_params=EXCLUDED_PARAMS))
413    cmd_args = cmd_args or {}
414    cmd_args.update(getter_args)
415    cmd_args['cmd'] = CLICommandArgument('cmd', arg_type=ignore_type)
416    return cmd_args
417
418
419def show_exception_handler(ex):
420    if getattr(getattr(ex, 'response', ex), 'status_code', None) == 404:
421        import sys
422        from azure.cli.core.azlogging import CommandLoggerContext
423        from azure.cli.core.azclierror import ResourceNotFoundError
424        with CommandLoggerContext(logger):
425            az_error = ResourceNotFoundError(getattr(ex, 'message', ex))
426            az_error.print_error()
427            az_error.send_telemetry()
428            sys.exit(3)
429    raise ex
430
431
432def verify_property(instance, condition):
433    from jmespath import compile as compile_jmespath
434    result = todict(instance)
435    jmes_query = compile_jmespath(condition)
436    value = jmes_query.search(result)
437    return value
438
439
440index_or_filter_regex = re.compile(r'\[(.*)\]')
441
442
443def _split_key_value_pair(expression):
444
445    def _find_split():
446        """ Find the first = sign to split on (that isn't in [brackets])"""
447        key = []
448        value = []
449        brackets = False
450        chars = list(expression)
451        while chars:
452            c = chars.pop(0)
453            if c == '=' and not brackets:
454                # keys done the rest is value
455                value = chars
456                break
457            if c == '[':
458                brackets = True
459                key += c
460            elif c == ']' and brackets:
461                brackets = False
462                key += c
463            else:
464                # normal character
465                key += c
466
467        return ''.join(key), ''.join(value)
468
469    equals_count = expression.count('=')
470    if equals_count == 1:
471        return expression.split('=', 1)
472    return _find_split()
473
474
475def set_properties(instance, expression, force_string):
476    key, value = _split_key_value_pair(expression)
477
478    if key is None or key.strip() == '':
479        raise CLIError('usage error: Empty key in --set. Correct syntax: --set KEY=VALUE [KEY=VALUE ...]')
480
481    if not force_string:
482        try:
483            value = shell_safe_json_parse(value)
484        except:  # pylint:disable=bare-except
485            pass
486
487    # name should be the raw casing as it could refer to a property OR a dictionary key
488    name, path = _get_name_path(key)
489    parent_name = path[-1] if path else 'root'
490    root = instance
491    instance = _find_property(instance, path)
492    if instance is None:
493        parent = _find_property(root, path[:-1])
494        set_properties(parent, '{}={{}}'.format(parent_name), force_string)
495        instance = _find_property(root, path)
496
497    match = index_or_filter_regex.match(name)
498    index_value = int(match.group(1)) if match else None
499    try:
500        if index_value is not None:
501            instance[index_value] = value
502        elif isinstance(instance, dict):
503            instance[name] = value
504        elif isinstance(instance, list):
505            throw_and_show_options(instance, name, key.split('.'))
506        else:
507            # must be a property name
508            if hasattr(instance, make_snake_case(name)):
509                setattr(instance, make_snake_case(name), value)
510            else:
511                if instance.additional_properties is None:
512                    instance.additional_properties = {}
513                instance.additional_properties[name] = value
514                instance.enable_additional_properties_sending()
515                logger.warning(
516                    "Property '%s' not found on %s. Send it as an additional property .", name, parent_name)
517
518    except IndexError:
519        raise CLIError('index {} doesn\'t exist on {}'.format(index_value, name))
520    except (AttributeError, KeyError, TypeError):
521        throw_and_show_options(instance, name, key.split('.'))
522
523
524def add_properties(instance, argument_values, force_string):
525    # The first argument indicates the path to the collection to add to.
526    argument_values = list(argument_values)
527    list_attribute_path = _get_internal_path(argument_values.pop(0))
528    list_to_add_to = _find_property(instance, list_attribute_path)
529
530    if list_to_add_to is None:
531        parent = _find_property(instance, list_attribute_path[:-1])
532        set_properties(parent, '{}=[]'.format(list_attribute_path[-1]), force_string)
533        list_to_add_to = _find_property(instance, list_attribute_path)
534
535    if not isinstance(list_to_add_to, list):
536        raise ValueError
537
538    dict_entry = {}
539    for argument in argument_values:
540        if '=' in argument:
541            # consecutive key=value entries get added to the same dictionary
542            split_arg = argument.split('=', 1)
543            dict_entry[split_arg[0]] = split_arg[1]
544        else:
545            if dict_entry:
546                # if an argument is supplied that is not key=value, append any dictionary entry
547                # to the list and reset. A subsequent key=value pair will be added to another
548                # dictionary.
549                list_to_add_to.append(dict_entry)
550                dict_entry = {}
551
552            if not force_string:
553                # attempt to convert anything else to JSON and fallback to string if error
554                try:
555                    argument = shell_safe_json_parse(argument)
556                except (ValueError, CLIError):
557                    pass
558            list_to_add_to.append(argument)
559
560    # if only key=value pairs used, must check at the end to append the dictionary
561    if dict_entry:
562        list_to_add_to.append(dict_entry)
563
564
565def remove_properties(instance, argument_values):
566    # The first argument indicates the path to the collection to remove from.
567    argument_values = list(argument_values) if isinstance(argument_values, list) else [argument_values]
568
569    list_attribute_path = _get_internal_path(argument_values.pop(0))
570    list_index = None
571    try:
572        list_index = argument_values.pop(0)
573    except IndexError:
574        pass
575
576    if not list_index:
577        property_val = _find_property(instance, list_attribute_path)
578        parent_to_remove_from = _find_property(instance, list_attribute_path[:-1])
579        if isinstance(parent_to_remove_from, dict):
580            del parent_to_remove_from[list_attribute_path[-1]]
581        elif hasattr(parent_to_remove_from, make_snake_case(list_attribute_path[-1])):
582            setattr(parent_to_remove_from, make_snake_case(list_attribute_path[-1]),
583                    [] if isinstance(property_val, list) else None)
584        else:
585            raise ValueError
586    else:
587        list_to_remove_from = _find_property(instance, list_attribute_path)
588        try:
589            list_to_remove_from.pop(int(list_index))
590        except IndexError:
591            raise CLIError('index {} doesn\'t exist on {}'
592                           .format(list_index, list_attribute_path[-1]))
593        except AttributeError:
594            raise CLIError('{} doesn\'t exist'.format(list_attribute_path[-1]))
595
596
597def throw_and_show_options(instance, part, path):
598    from msrest.serialization import Model
599    options = instance.__dict__ if hasattr(instance, '__dict__') else instance
600    if isinstance(instance, Model) and isinstance(getattr(instance, 'additional_properties', None), dict):
601        options.update(options.pop('additional_properties'))
602    parent = '.'.join(path[:-1]).replace('.[', '[')
603    error_message = "Couldn't find '{}' in '{}'.".format(part, parent)
604    if isinstance(options, dict):
605        options = options.keys()
606        options = sorted([make_camel_case(x) for x in options])
607        error_message = '{} Available options: {}'.format(error_message, options)
608    elif isinstance(options, list):
609        options = "index into the collection '{}' with [<index>] or [<key=value>]".format(parent)
610        error_message = '{} Available options: {}'.format(error_message, options)
611    else:
612        error_message = "{} '{}' does not support further indexing.".format(error_message, parent)
613    raise CLIError(error_message)
614
615
616snake_regex_1 = re.compile('(.)([A-Z][a-z]+)')
617snake_regex_2 = re.compile('([a-z0-9])([A-Z])')
618
619
620def make_snake_case(s):
621    if isinstance(s, str):
622        s1 = re.sub(snake_regex_1, r'\1_\2', s)
623        return re.sub(snake_regex_2, r'\1_\2', s1).lower()
624    return s
625
626
627def make_camel_case(s):
628    if isinstance(s, str):
629        parts = s.split('_')
630        return (parts[0].lower() + ''.join(p.capitalize() for p in parts[1:])) if len(parts) > 1 else s
631    return s
632
633
634internal_path_regex = re.compile(r'(\[.*?\])|([^.]+)')
635
636
637def _get_internal_path(path):
638    # to handle indexing in the same way as other dot qualifiers,
639    # we split paths like foo[0][1] into foo.[0].[1]
640    path = path.replace('.[', '[').replace('[', '.[')
641    path_segment_pairs = internal_path_regex.findall(path)
642    final_paths = []
643    for regex_result in path_segment_pairs:
644        # the regex matches two capture group, one of which will be None
645        segment = regex_result[0] or regex_result[1]
646        final_paths.append(segment)
647    return final_paths
648
649
650def _get_name_path(path):
651    pathlist = _get_internal_path(path)
652    return pathlist.pop(), pathlist
653
654
655def _update_instance(instance, part, path):  # pylint: disable=too-many-return-statements, inconsistent-return-statements
656    try:
657        index = index_or_filter_regex.match(part)
658        if index and not isinstance(instance, list):
659            throw_and_show_options(instance, part, path)
660
661        if index and '=' in index.group(1):
662            key, value = index.group(1).split('=', 1)
663            try:
664                value = shell_safe_json_parse(value)
665            except:  # pylint: disable=bare-except
666                pass
667            matches = []
668            for x in instance:
669                if isinstance(x, dict) and x.get(key, None) == value:
670                    matches.append(x)
671                elif not isinstance(x, dict):
672                    snake_key = make_snake_case(key)
673                    if hasattr(x, snake_key) and getattr(x, snake_key, None) == value:
674                        matches.append(x)
675
676            if len(matches) == 1:
677                return matches[0]
678            if len(matches) > 1:
679                raise CLIError("non-unique key '{}' found multiple matches on {}. Key must be unique."
680                               .format(key, path[-2]))
681            if key in getattr(instance, 'additional_properties', {}):
682                instance.enable_additional_properties_sending()
683                return instance.additional_properties[key]
684            raise CLIError("item with value '{}' doesn\'t exist for key '{}' on {}".format(value, key, path[-2]))
685
686        if index:
687            try:
688                index_value = int(index.group(1))
689                return instance[index_value]
690            except IndexError:
691                raise CLIError('index {} doesn\'t exist on {}'.format(index_value, path[-2]))
692
693        if isinstance(instance, dict):
694            return instance[part]
695
696        if hasattr(instance, make_snake_case(part)):
697            return getattr(instance, make_snake_case(part), None)
698        if part in getattr(instance, 'additional_properties', {}):
699            instance.enable_additional_properties_sending()
700            return instance.additional_properties[part]
701        raise AttributeError()
702    except (AttributeError, KeyError):
703        throw_and_show_options(instance, part, path)
704
705
706def _find_property(instance, path):
707    for part in path:
708        instance = _update_instance(instance, part, path)
709    return instance
710
711
712def assign_identity(cli_ctx, getter, setter, identity_role=None, identity_scope=None):
713    import time
714    from msrestazure.azure_exceptions import CloudError
715
716    # get
717    resource = getter()
718    resource = setter(resource)
719
720    # create role assignment:
721    if identity_scope:
722        principal_id = resource.identity.principal_id
723
724        identity_role_id = resolve_role_id(cli_ctx, identity_role, identity_scope)
725        assignments_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION).role_assignments
726        RoleAssignmentCreateParameters = get_sdk(cli_ctx, ResourceType.MGMT_AUTHORIZATION,
727                                                 'RoleAssignmentCreateParameters', mod='models',
728                                                 operation_group='role_assignments')
729        parameters = RoleAssignmentCreateParameters(role_definition_id=identity_role_id, principal_id=principal_id)
730
731        logger.info("Creating an assignment with a role '%s' on the scope of '%s'", identity_role_id, identity_scope)
732        retry_times = 36
733        assignment_name = _gen_guid()
734        for retry_time in range(0, retry_times):
735            try:
736                assignments_client.create(scope=identity_scope, role_assignment_name=assignment_name,
737                                          parameters=parameters)
738                break
739            except CloudError as ex:
740                if 'role assignment already exists' in ex.message:
741                    logger.info('Role assignment already exists')
742                    break
743                if retry_time < retry_times and ' does not exist in the directory ' in ex.message:
744                    time.sleep(5)
745                    logger.warning('Retrying role assignment creation: %s/%s', retry_time + 1,
746                                   retry_times)
747                    continue
748                raise
749    return resource
750
751
752def resolve_role_id(cli_ctx, role, scope):
753    import uuid
754    client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION).role_definitions
755
756    role_id = None
757    if re.match(r'/subscriptions/[^/]+/providers/Microsoft.Authorization/roleDefinitions/',
758                role, re.I):
759        role_id = role
760    else:
761        try:
762            uuid.UUID(role)
763            role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format(
764                client.config.subscription_id, role)
765        except ValueError:
766            pass
767        if not role_id:  # retrieve role id
768            role_defs = list(client.list(scope, "roleName eq '{}'".format(role)))
769            if not role_defs:
770                raise CLIError("Role '{}' doesn't exist.".format(role))
771            if len(role_defs) > 1:
772                ids = [r.id for r in role_defs]
773                err = "More than one role matches the given name '{}'. Please pick an id from '{}'"
774                raise CLIError(err.format(role, ids))
775            role_id = role_defs[0].id
776    return role_id
777
778
779def _gen_guid():
780    import uuid
781    return uuid.uuid4()
782
783
784def get_arm_resource_by_id(cli_ctx, arm_id, api_version=None):
785    from msrestazure.tools import parse_resource_id, is_valid_resource_id
786
787    if not is_valid_resource_id(arm_id):
788        raise CLIError("'{}' is not a valid ID.".format(arm_id))
789
790    client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
791
792    if not api_version:
793
794        parts = parse_resource_id(arm_id)
795
796        # to retrieve the provider, we need to know the namespace
797        namespaces = {k: v for k, v in parts.items() if 'namespace' in k}
798
799        # every ARM ID has at least one namespace, so start with that
800        namespace = namespaces.pop('namespace')
801        namespaces.pop('resource_namespace')
802        # find the most specific child namespace (if any) and use that value instead
803        highest_child = 0
804        for k, v in namespaces.items():
805            child_number = int(k.split('_')[2])
806            if child_number > highest_child:
807                namespace = v
808                highest_child = child_number
809
810        # retrieve provider info for the namespace
811        provider = client.providers.get(namespace)
812
813        # assemble the resource type key used by the provider list operation.  type1/type2/type3/...
814        resource_type_str = ''
815        if not highest_child:
816            resource_type_str = parts['resource_type']
817        else:
818            types = {int(k.split('_')[2]): v for k, v in parts.items() if k.startswith('child_type')}
819            for k in sorted(types.keys()):
820                if k < highest_child:
821                    continue
822                resource_type_str = '{}{}/'.format(resource_type_str, parts['child_type_{}'.format(k)])
823            resource_type_str = resource_type_str.rstrip('/')
824
825        api_version = None
826        rt = next((t for t in provider.resource_types if t.resource_type.lower() == resource_type_str.lower()), None)
827        if not rt:
828            from azure.cli.core.parser import IncorrectUsageError
829            raise IncorrectUsageError('Resource type {} not found.'.format(resource_type_str))
830        try:
831            # Use the most recent non-preview API version unless there is only a
832            # single API version. API versions are returned by the service in a sorted list.
833            api_version = next((x for x in rt.api_versions if not x.endswith('preview')), rt.api_versions[0])
834        except AttributeError:
835            err = "No API versions found for resource type '{}'."
836            raise CLIError(err.format(resource_type_str))
837
838    return client.resources.get_by_id(arm_id, api_version)
839