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