def add_role_assignment_executor(cmd, role, assignee, resource_group_name=None, scope=None, resolve_assignee=True): factory = get_auth_management_client(cmd.cli_ctx, scope) assignments_client = factory.role_assignments definitions_client = factory.role_definitions # FIXME: is this necessary? if assignments_client.config is None: raise AzCLIError("Assignments client config is undefined.") scope = build_role_scope(resource_group_name, scope, assignments_client.config.subscription_id) # XXX: if role is uuid, this function's output cannot be used as role assignment defintion id # ref: https://github.com/Azure/azure-cli/issues/2458 role_id = resolve_role_id(role, scope, definitions_client) # If the cluster has service principal resolve the service principal client id to get the object id, # if not use MSI object id. object_id = resolve_object_id(cmd.cli_ctx, assignee) if resolve_assignee else assignee assignment_name = uuid.uuid4() custom_headers = None RoleAssignmentCreateParameters = get_sdk( cmd.cli_ctx, ResourceType.MGMT_AUTHORIZATION, "RoleAssignmentCreateParameters", mod="models", operation_group="role_assignments", ) if cmd.supported_api_version( min_api="2018-01-01-preview", resource_type=ResourceType.MGMT_AUTHORIZATION): parameters = RoleAssignmentCreateParameters(role_definition_id=role_id, principal_id=object_id) return assignments_client.create(scope, assignment_name, parameters, custom_headers=custom_headers) # for backward compatibility RoleAssignmentProperties = get_sdk( cmd.cli_ctx, ResourceType.MGMT_AUTHORIZATION, "RoleAssignmentProperties", mod="models", operation_group="role_assignments", ) properties = RoleAssignmentProperties(role_definition_id=role_id, principal_id=object_id) return assignments_client.create(scope, assignment_name, properties, custom_headers=custom_headers)
def get_rg_location(ctx, resource_group_name, subscription_id=None): groups = cf_resource_groups(ctx, subscription_id=subscription_id) # Just do the get, we don't need the result, it will error out if the group doesn't exist. rg = groups.get(resource_group_name) if rg is None: raise AzCLIError(f"Resource group {resource_group_name} not found.") return rg.location
def resolve_role_id(role, scope, definitions_client): role_id = None try: uuid.UUID(role) role_id = role except ValueError: pass if not role_id: # retrieve role id role_defs = list( definitions_client.list(scope, "roleName eq '{}'".format(role))) if len(role_defs) == 0: raise AzCLIError("Role '{}' doesn't exist.".format(role)) if len(role_defs) > 1: ids = [r.id for r in role_defs] err = "More than one role matches the given name '{}'. Please pick a value from '{}'" raise AzCLIError(err.format(role, ids)) role_id = role_defs[0].id return role_id
def ensure_aks_service_principal( cli_ctx, service_principal=None, client_secret=None, subscription_id=None, dns_name_prefix=None, fqdn_subdomain=None, location=None, name=None, ): aad_session_key = None # TODO: This really needs to be unit tested. rbac_client = get_graph_rbac_management_client(cli_ctx) if not service_principal: # --service-principal not specified, make one. if not client_secret: client_secret = _create_client_secret() salt = binascii.b2a_hex(os.urandom(3)).decode("utf-8") if dns_name_prefix: url = "https://{}.{}.{}.cloudapp.azure.com".format( salt, dns_name_prefix, location) else: url = "https://{}.{}.{}.cloudapp.azure.com".format( salt, fqdn_subdomain, location) service_principal, aad_session_key = build_service_principal( rbac_client, cli_ctx, name, url, client_secret) if not service_principal: raise AzCLIError( "Could not create a service principal with the right permissions. " "Are you an Owner on this project?") logger.info("Created a service principal: %s", service_principal) # We don't need to add role assignment for this created SPN else: # --service-principal specfied, validate --client-secret was too if not client_secret: raise AzCLIError( "--client-secret is required if --service-principal is specified" ) return { "client_secret": client_secret, "service_principal": service_principal, "aad_session_key": aad_session_key, }
def build_role_scope(resource_group_name: str, scope: str, subscription_id: str): subscription_scope = '/subscriptions/' + subscription_id if scope is not None: if resource_group_name: err = 'Resource group "{}" is redundant because scope is supplied' raise AzCLIError(err.format(resource_group_name)) elif resource_group_name: scope = subscription_scope + '/resourceGroups/' + resource_group_name else: scope = subscription_scope return scope
def resolve_object_id(cli_ctx, assignee): client = get_graph_rbac_management_client(cli_ctx) result = None if assignee is None: raise AzCLIError('Inputted parameter "assignee" is None.') if assignee.find("@") >= 0: # looks like a user principal name result = list( client.users.list( filter="userPrincipalName eq '{}'".format(assignee))) if not result: result = list( client.service_principals.list( filter="servicePrincipalNames/any(c:c eq '{}')".format( assignee))) if not result: # assume an object id, let us verify it result = _get_object_stubs(client, [assignee]) # 2+ matches should never happen, so we only check 'no match' here if not result: raise AzCLIError( "No matches in graph database for '{}'".format(assignee)) return result[0].object_id
def ensure_aks_acr(cmd, assignee, acr_name_or_id, subscription_id, detach=False, is_service_principal=True): from msrestazure.tools import is_valid_resource_id, parse_resource_id # Check if the ACR exists by resource ID. if is_valid_resource_id(acr_name_or_id): try: parsed_registry = parse_resource_id(acr_name_or_id) acr_client = cf_container_registry_service( cmd.cli_ctx, subscription_id=parsed_registry["subscription"]) registry = acr_client.registries.get( parsed_registry["resource_group"], parsed_registry["name"]) except (CloudError, HttpResponseError) as ex: raise AzCLIError(ex.message) ensure_aks_acr_role_assignment(cmd, assignee, registry.id, detach, is_service_principal) return # Check if the ACR exists by name accross all resource groups. registry_name = acr_name_or_id registry_resource = "Microsoft.ContainerRegistry/registries" try: registry = get_resource_by_name(cmd.cli_ctx, registry_name, registry_resource) except (CloudError, HttpResponseError) as ex: if "was not found" in ex.message: raise AzCLIError( "ACR {} not found. Have you provided the right ACR name?". format(registry_name)) raise AzCLIError(ex.message) ensure_aks_acr_role_assignment(cmd, assignee, registry.id, detach, is_service_principal) return
def ensure_aks_acr_role_assignment(cmd, assignee, registry_id, detach=False, is_service_principal=True): if detach: if not delete_role_assignments( cmd.cli_ctx, "acrpull", assignee, scope=registry_id, is_service_principal=is_service_principal): raise AzCLIError("Could not delete role assignments for ACR. " "Are you an Owner on this subscription?") return if not add_role_assignment(cmd, "acrpull", assignee, scope=registry_id, is_service_principal=is_service_principal): raise AzCLIError("Could not create a role assignment for ACR. " "Are you an Owner on this subscription?") return
def delete_role_assignments_executor( cli_ctx, ids=None, assignee=None, role=None, resource_group_name=None, scope=None, include_inherited=False, yes=None, is_service_principal=True, ): factory = get_auth_management_client(cli_ctx, scope) assignments_client = factory.role_assignments definitions_client = factory.role_definitions ids = ids or [] if ids: if assignee or role or resource_group_name or scope or include_inherited: raise AzCLIError( 'When assignment ids are used, other parameter values are not required' ) for i in ids: assignments_client.delete_by_id(i) return if not any( [ids, assignee, role, resource_group_name, scope, assignee, yes]): msg = 'This will delete all role assignments under the subscription. Are you sure?' if not prompt_y_n(msg, default="n"): return scope = build_role_scope(resource_group_name, scope, assignments_client.config.subscription_id) assignments = search_role_assignments( cli_ctx, assignments_client, definitions_client, scope, assignee, role, include_inherited, include_groups=False, is_service_principal=is_service_principal, ) if assignments: for a in assignments: assignments_client.delete_by_id(a.id)
def _build_application_creds(password=None, key_value=None, key_type=None, key_usage=None, start_date=None, end_date=None): if password and key_value: raise AzCLIError( "specify either --password or --key-value, but not both.") if not start_date: start_date = datetime.datetime.utcnow() elif isinstance(start_date, str): start_date = dateutil.parser.parse(start_date) if not end_date: end_date = start_date + relativedelta(years=1) elif isinstance(end_date, str): end_date = dateutil.parser.parse(end_date) key_type = key_type or "AsymmetricX509Cert" key_usage = key_usage or "Verify" password_creds = None key_creds = None if password: password_creds = [ PasswordCredential(start_date=start_date, end_date=end_date, key_id=str(uuid.uuid4()), value=password) ] elif key_value: key_creds = [ KeyCredential( start_date=start_date, end_date=end_date, value=key_value, key_id=str(uuid.uuid4()), usage=key_usage, type=key_type, ) ] return (password_creds, key_creds)
def create_application( client, display_name, homepage, identifier_uris, available_to_other_tenants=False, password=None, reply_urls=None, key_value=None, key_type=None, key_usage=None, start_date=None, end_date=None, required_resource_accesses=None, ): password_creds, key_creds = _build_application_creds( password, key_value, key_type, key_usage, start_date, end_date) app_create_param = ApplicationCreateParameters( available_to_other_tenants=available_to_other_tenants, display_name=display_name, identifier_uris=identifier_uris, homepage=homepage, reply_urls=reply_urls, key_credentials=key_creds, password_credentials=password_creds, required_resource_access=required_resource_accesses, ) try: result = client.create(app_create_param, raw=True) return result.output, result.response.headers["ocp-aad-session-key"] except GraphErrorException as ex: if "insufficient privileges" in str(ex).lower(): link = "https://docs.microsoft.com/azure/azure-resource-manager/resource-group-create-service-principal-portal" # pylint: disable=line-too-long raise AzCLIError( "Directory permission is needed for the current user to register the application. " "For how to configure, please refer '{}'. Original error: {}". format(link, ex)) raise
def error(self, message): # Get a recommended command from the CommandRecommender command_arguments = self._get_failure_recovery_arguments() cli_ctx = self.cli_ctx or (self.cli_help.cli_ctx if self.cli_help else None) recommender = CommandRecommender(*command_arguments, message, cli_ctx) recommender.set_help_examples(self.get_examples(self.prog)) recommendation = recommender.recommend_a_command() az_error = AzCLIError(AzCLIErrorType.ArgumentParseError, message, command=self.prog) if '--query' in message: from azure.cli.core.util import QUERY_REFERENCE az_error.set_recommendation(QUERY_REFERENCE) elif recommendation: az_error.set_recommendation("Try this: '{}'".format(recommendation)) az_error.set_recommendation(OVERVIEW_REFERENCE.format(command=self.prog)) az_error.print_error() az_error.send_telemetry() # For ai-did-you-mean-this failure_recovery_recommendations = self._get_failure_recovery_recommendations() self._suggestion_msg.extend(failure_recovery_recommendations) self._print_suggestion_msg(sys.stderr) self.exit(2)
def validation_error(self, message): az_error = AzCLIError(AzCLIErrorType.ValidationError, message, command=self.prog) az_error.print_error() az_error.send_telemetry() self.exit(2)
def ensure_container_insights_for_monitoring( cmd, addon, cluster_subscription, cluster_resource_group_name, cluster_name, cluster_region, remove_monitoring=False, aad_route=False, create_dcr=False, create_dcra=False, ): """ Either adds the ContainerInsights solution to a LA Workspace OR sets up a DCR (Data Collection Rule) and DCRA (Data Collection Rule Association). Both let the monitoring addon send data to a Log Analytics Workspace. Set aad_route == True to set up the DCR data route. Otherwise the solution route will be used. Create_dcr and create_dcra have no effect if aad_route == False. Set remove_monitoring to True and create_dcra to True to remove the DCRA from a cluster. The association makes it very hard to delete either the DCR or cluster. (It is not obvious how to even navigate to the association from the portal, and it prevents the cluster and DCR from being deleted individually). """ if not addon.enabled: return None # workaround for this addon key which has been seen lowercased in the wild for key in list(addon.config): if (key.lower() == CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID.lower() and key != CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID): addon.config[ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID] = addon.config.pop( key) workspace_resource_id = addon.config[ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID] workspace_resource_id = sanitize_loganalytics_ws_resource_id( workspace_resource_id) # extract subscription ID and resource group from workspace_resource_id URL try: subscription_id = workspace_resource_id.split("/")[2] resource_group = workspace_resource_id.split("/")[4] workspace_name = workspace_resource_id.split("/")[8] except IndexError: raise AzCLIError( "Could not locate resource group in workspace-resource-id URL.") # region of workspace can be different from region of RG so find the location of the workspace_resource_id if not remove_monitoring: resources = cf_resources(cmd.cli_ctx, subscription_id) try: resource = resources.get_by_id(workspace_resource_id, "2015-11-01-preview") location = resource.location except HttpResponseError as ex: raise ex if aad_route: cluster_resource_id = ( f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/" f"providers/Microsoft.ContainerService/managedClusters/{cluster_name}" ) dataCollectionRuleName = f"MSCI-{workspace_name}" dcr_resource_id = ( f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/" f"providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" ) if create_dcr: # first get the association between region display names and region IDs (because for some reason # the "which RPs are available in which regions" check returns region display names) region_names_to_id = {} # retry the request up to two times for _ in range(3): try: location_list_url = ( f"https://management.azure.com/subscriptions/{subscription_id}/" "locations?api-version=2019-11-01") r = send_raw_request(cmd.cli_ctx, "GET", location_list_url) # this is required to fool the static analyzer. The else statement will only run if an exception # is thrown, but flake8 will complain that e is undefined if we don't also define it here. error = None break except AzCLIError as e: error = e else: # This will run if the above for loop was not broken out of. This means all three requests failed raise error json_response = json.loads(r.text) for region_data in json_response["value"]: region_names_to_id[ region_data["displayName"]] = region_data["name"] # check if region supports DCRs and DCR-A for _ in range(3): try: feature_check_url = ( f"https://management.azure.com/subscriptions/{subscription_id}/" "providers/Microsoft.Insights?api-version=2020-10-01") r = send_raw_request(cmd.cli_ctx, "GET", feature_check_url) error = None break except AzCLIError as e: error = e else: raise error json_response = json.loads(r.text) for resource in json_response["resourceTypes"]: region_ids = map( lambda x: region_names_to_id[x], resource["locations"] ) # map is lazy, so doing this for every region isn't slow if (resource["resourceType"].lower() == "datacollectionrules" and location not in region_ids): raise ClientRequestError( f"Data Collection Rules are not supported for LA workspace region {location}" ) if (resource["resourceType"].lower() == "datacollectionruleassociations" and cluster_region not in region_ids): raise ClientRequestError( f"Data Collection Rule Associations are not supported for cluster region {location}" ) # create the DCR dcr_creation_body = json.dumps({ "location": location, "properties": { "dataSources": { "extensions": [{ "name": "ContainerInsightsExtension", "streams": [ "Microsoft-Perf", "Microsoft-ContainerInventory", "Microsoft-ContainerLog", "Microsoft-ContainerLogV2", "Microsoft-ContainerNodeInventory", "Microsoft-KubeEvents", "Microsoft-KubeHealth", "Microsoft-KubeMonAgentEvents", "Microsoft-KubeNodeInventory", "Microsoft-KubePodInventory", "Microsoft-KubePVInventory", "Microsoft-KubeServices", "Microsoft-InsightsMetrics", ], "extensionName": "ContainerInsights", }] }, "dataFlows": [{ "streams": [ "Microsoft-Perf", "Microsoft-ContainerInventory", "Microsoft-ContainerLog", "Microsoft-ContainerLogV2", "Microsoft-ContainerNodeInventory", "Microsoft-KubeEvents", "Microsoft-KubeHealth", "Microsoft-KubeMonAgentEvents", "Microsoft-KubeNodeInventory", "Microsoft-KubePodInventory", "Microsoft-KubePVInventory", "Microsoft-KubeServices", "Microsoft-InsightsMetrics", ], "destinations": ["la-workspace"], }], "destinations": { "logAnalytics": [{ "workspaceResourceId": workspace_resource_id, "name": "la-workspace", }] }, }, }) dcr_url = f"https://management.azure.com/{dcr_resource_id}?api-version=2019-11-01-preview" for _ in range(3): try: send_raw_request(cmd.cli_ctx, "PUT", dcr_url, body=dcr_creation_body) error = None break except AzCLIError as e: error = e else: raise error if create_dcra: # only create or delete the association between the DCR and cluster association_body = json.dumps({ "location": cluster_region, "properties": { "dataCollectionRuleId": dcr_resource_id, "description": "routes monitoring data to a Log Analytics workspace", }, }) association_url = ( f"https://management.azure.com/{cluster_resource_id}/providers/Microsoft.Insights/" f"dataCollectionRuleAssociations/send-to-{workspace_name}?api-version=2019-11-01-preview" ) for _ in range(3): try: send_raw_request( cmd.cli_ctx, "PUT" if not remove_monitoring else "DELETE", association_url, body=association_body, ) error = None break except AzCLIError as e: error = e else: raise error
def _check_value(self, action, value): # pylint: disable=too-many-statements, too-many-locals # Override to customize the error message when a argument is not among the available choices # converted value must be one of the choices (if specified) if action.choices is not None and value not in action.choices: # pylint: disable=too-many-nested-blocks # self.cli_ctx is None when self.prog is beyond 'az', such as 'az iot'. # use cli_ctx from cli_help which is not lost. cli_ctx = self.cli_ctx or (self.cli_help.cli_ctx if self.cli_help else None) caused_by_extension_not_installed = False command_name_inferred = self.prog error_msg = None if not self.command_source: candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) if candidates: # use the most likely candidate to replace the misspelled command args = self.prog.split() + self._raw_arguments args_inferred = [item if item != value else candidates[0] for item in args] command_name_inferred = ' '.join(args_inferred).split('-')[0] use_dynamic_install = self._get_extension_use_dynamic_install_config() if use_dynamic_install != 'no' and not candidates: # Check if the command is from an extension from azure.cli.core.util import roughly_parse_command cmd_list = self.prog.split() + self._raw_arguments command_str = roughly_parse_command(cmd_list[1:]) ext_name = self._search_in_extension_commands(command_str) if ext_name: caused_by_extension_not_installed = True telemetry.set_command_details(command_str, parameters=AzCliCommandInvoker._extract_parameter_names(cmd_list), # pylint: disable=protected-access extension_name=ext_name) run_after_extension_installed = self._get_extension_run_after_dynamic_install_config() if use_dynamic_install == 'yes_without_prompt': logger.warning('The command requires the extension %s. ' 'It will be installed first.', ext_name) go_on = True else: from knack.prompting import prompt_y_n, NoTTYException prompt_msg = 'The command requires the extension {}. ' \ 'Do you want to install it now?'.format(ext_name) if run_after_extension_installed: prompt_msg = '{} The command will continue to run after the extension is installed.' \ .format(prompt_msg) NO_PROMPT_CONFIG_MSG = "Run 'az config set extension.use_dynamic_install=" \ "yes_without_prompt' to allow installing extensions without prompt." try: go_on = prompt_y_n(prompt_msg, default='y') if go_on: logger.warning(NO_PROMPT_CONFIG_MSG) except NoTTYException: logger.warning("The command requires the extension %s.\n " "Unable to prompt for extension install confirmation as no tty " "available. %s", ext_name, NO_PROMPT_CONFIG_MSG) go_on = False if go_on: from azure.cli.core.extension.operations import add_extension add_extension(cli_ctx=cli_ctx, extension_name=ext_name, upgrade=True) if run_after_extension_installed: import subprocess import platform exit_code = subprocess.call(cmd_list, shell=platform.system() == 'Windows') error_msg = ("Extension {} dynamically installed and commands will be " "rerun automatically.").format(ext_name) telemetry.set_user_fault(error_msg) self.exit(exit_code) else: with CommandLoggerContext(logger): error_msg = 'Extension {} installed. Please rerun your command.'.format(ext_name) logger.error(error_msg) telemetry.set_user_fault(error_msg) self.exit(2) else: error_msg = "The command requires the latest version of extension {ext_name}. " \ "To install, run 'az extension add --upgrade -n {ext_name}'.".format(ext_name=ext_name) if not error_msg: # parser has no `command_source`, value is part of command itself error_msg = "'{value}' is misspelled or not recognized by the system.".format(value=value) az_error = AzCLIError(AzCLIErrorType.CommandNotFoundError, error_msg, command=self.prog) else: # `command_source` indicates command values have been parsed, value is an argument parameter = action.option_strings[0] if action.option_strings else action.dest error_msg = "{prog}: '{value}' is not a valid value for '{param}'.".format( prog=self.prog, value=value, param=parameter) candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) az_error = AzCLIError(AzCLIErrorType.ArgumentParseError, error_msg, command=self.prog) command_arguments = self._get_failure_recovery_arguments(action) if candidates: az_error.set_recommendation("Did you mean '{}' ?".format(candidates[0])) # recommand a command for user recommender = CommandRecommender(*command_arguments, error_msg, cli_ctx) recommender.set_help_examples(self.get_examples(command_name_inferred)) recommended_command = recommender.recommend_a_command() if recommended_command: az_error.set_recommendation("Try this: '{}'".format(recommended_command)) # remind user to check extensions if we can not find a command to recommend if az_error.error_type == AzCLIErrorType.CommandNotFoundError \ and not az_error.recommendations and self.prog == 'az' \ and use_dynamic_install == 'no': az_error.set_recommendation(EXTENSION_REFERENCE) az_error.set_recommendation(OVERVIEW_REFERENCE.format(command=self.prog)) az_error.print_error() az_error.send_telemetry() if not caused_by_extension_not_installed: failure_recovery_recommendations = self._get_failure_recovery_recommendations(action) self._suggestion_msg.extend(failure_recovery_recommendations) self._print_suggestion_msg(sys.stderr) self.exit(2)
def ensure_container_insights_for_monitoring( cmd, addon, cluster_subscription, cluster_resource_group_name, cluster_name, cluster_region, remove_monitoring=False, aad_route=False, create_dcr=False, create_dcra=False, ): """ Either adds the ContainerInsights solution to a LA Workspace OR sets up a DCR (Data Collection Rule) and DCRA (Data Collection Rule Association). Both let the monitoring addon send data to a Log Analytics Workspace. Set aad_route == True to set up the DCR data route. Otherwise the solution route will be used. Create_dcr and create_dcra have no effect if aad_route == False. Set remove_monitoring to True and create_dcra to True to remove the DCRA from a cluster. The association makes it very hard to delete either the DCR or cluster. (It is not obvious how to even navigate to the association from the portal, and it prevents the cluster and DCR from being deleted individually). """ if not addon.enabled: return None # workaround for this addon key which has been seen lowercased in the wild for key in list(addon.config): if (key.lower() == CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID.lower() and key != CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID): addon.config[ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID] = addon.config.pop( key) workspace_resource_id = addon.config[ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID] workspace_resource_id = sanitize_loganalytics_ws_resource_id( workspace_resource_id) # extract subscription ID and resource group from workspace_resource_id URL try: subscription_id = workspace_resource_id.split("/")[2] resource_group = workspace_resource_id.split("/")[4] except IndexError: raise AzCLIError( "Could not locate resource group in workspace-resource-id URL.") # region of workspace can be different from region of RG so find the location of the workspace_resource_id if not remove_monitoring: resources = cf_resources(cmd.cli_ctx, subscription_id) try: resource = resources.get_by_id(workspace_resource_id, "2015-11-01-preview") location = resource.location except HttpResponseError as ex: raise ex if aad_route: cluster_resource_id = ( f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/" f"providers/Microsoft.ContainerService/managedClusters/{cluster_name}" ) dataCollectionRuleName = f"MSCI-{cluster_name}-{cluster_region}" dcr_resource_id = ( f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/" f"providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" ) if create_dcr: # first get the association between region display names and region IDs (because for some reason # the "which RPs are available in which regions" check returns region display names) region_names_to_id = {} # retry the request up to two times for _ in range(3): try: location_list_url = cmd.cli_ctx.cloud.endpoints.resource_manager + \ f"/subscriptions/{subscription_id}/locations?api-version=2019-11-01" r = send_raw_request(cmd.cli_ctx, "GET", location_list_url) # this is required to fool the static analyzer. The else statement will only run if an exception # is thrown, but flake8 will complain that e is undefined if we don't also define it here. error = None break except AzCLIError as e: error = e else: # This will run if the above for loop was not broken out of. This means all three requests failed raise error json_response = json.loads(r.text) for region_data in json_response["value"]: region_names_to_id[ region_data["displayName"]] = region_data["name"] # check if region supports DCRs and DCR-A for _ in range(3): try: feature_check_url = cmd.cli_ctx.cloud.endpoints.resource_manager + \ f"/subscriptions/{subscription_id}/providers/Microsoft.Insights?api-version=2020-10-01" r = send_raw_request(cmd.cli_ctx, "GET", feature_check_url) error = None break except AzCLIError as e: error = e else: raise error json_response = json.loads(r.text) for resource in json_response["resourceTypes"]: if resource["resourceType"].lower() == "datacollectionrules": region_ids = map(lambda x: region_names_to_id[x], resource["locations"]) if location not in region_ids: raise ClientRequestError( f"Data Collection Rules are not supported for LA workspace region {location}" ) if resource["resourceType"].lower( ) == "datacollectionruleassociations": region_ids = map(lambda x: region_names_to_id[x], resource["locations"]) if cluster_region not in region_ids: raise ClientRequestError( f"Data Collection Rule Associations are not supported for cluster region {cluster_region}" ) dcr_url = cmd.cli_ctx.cloud.endpoints.resource_manager + \ f"{dcr_resource_id}?api-version=2019-11-01-preview" # get existing tags on the container insights extension DCR if the customer added any existing_tags = get_existing_container_insights_extension_dcr_tags( cmd, dcr_url) # create the DCR dcr_creation_body = json.dumps({ "location": location, "tags": existing_tags, "properties": { "dataSources": { "extensions": [{ "name": "ContainerInsightsExtension", "streams": [ "Microsoft-Perf", "Microsoft-ContainerInventory", "Microsoft-ContainerLog", "Microsoft-ContainerLogV2", "Microsoft-ContainerNodeInventory", "Microsoft-KubeEvents", "Microsoft-KubeMonAgentEvents", "Microsoft-KubeNodeInventory", "Microsoft-KubePodInventory", "Microsoft-KubePVInventory", "Microsoft-KubeServices", "Microsoft-InsightsMetrics", ], "extensionName": "ContainerInsights", }] }, "dataFlows": [{ "streams": [ "Microsoft-Perf", "Microsoft-ContainerInventory", "Microsoft-ContainerLog", "Microsoft-ContainerLogV2", "Microsoft-ContainerNodeInventory", "Microsoft-KubeEvents", "Microsoft-KubeMonAgentEvents", "Microsoft-KubeNodeInventory", "Microsoft-KubePodInventory", "Microsoft-KubePVInventory", "Microsoft-KubeServices", "Microsoft-InsightsMetrics", ], "destinations": ["la-workspace"], }], "destinations": { "logAnalytics": [{ "workspaceResourceId": workspace_resource_id, "name": "la-workspace", }] }, }, }) for _ in range(3): try: send_raw_request(cmd.cli_ctx, "PUT", dcr_url, body=dcr_creation_body) error = None break except AzCLIError as e: error = e else: raise error if create_dcra: # only create or delete the association between the DCR and cluster association_body = json.dumps({ "location": cluster_region, "properties": { "dataCollectionRuleId": dcr_resource_id, "description": "routes monitoring data to a Log Analytics workspace", }, }) association_url = cmd.cli_ctx.cloud.endpoints.resource_manager + \ f"{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/ContainerInsightsExtension?api-version=2019-11-01-preview" for _ in range(3): try: send_raw_request( cmd.cli_ctx, "PUT" if not remove_monitoring else "DELETE", association_url, body=association_body, ) error = None break except AzCLIError as e: error = e else: raise error if not _is_container_insights_solution_exists(cmd, workspace_resource_id): unix_time_in_millis = int( (datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) solution_deployment_name = "ContainerInsights-{}".format( unix_time_in_millis) # pylint: disable=line-too-long template = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "workspaceResourceId": { "type": "string", "metadata": { "description": "Azure Monitor Log Analytics Resource ID" }, }, "workspaceRegion": { "type": "string", "metadata": { "description": "Azure Monitor Log Analytics workspace region" }, }, "solutionDeploymentName": { "type": "string", "metadata": { "description": "Name of the solution deployment" }, }, }, "resources": [{ "type": "Microsoft.Resources/deployments", "name": "[parameters('solutionDeploymentName')]", "apiVersion": "2017-05-10", "subscriptionId": "[split(parameters('workspaceResourceId'),'/')[2]]", "resourceGroup": "[split(parameters('workspaceResourceId'),'/')[4]]", "properties": { "mode": "Incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": {}, "variables": {}, "resources": [{ "apiVersion": "2015-11-01-preview", "type": "Microsoft.OperationsManagement/solutions", "location": "[parameters('workspaceRegion')]", "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId'),'/')[8], ')')]", "properties": { "workspaceResourceId": "[parameters('workspaceResourceId')]" }, "plan": { "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId'),'/')[8], ')')]", "product": "[Concat('OMSGallery/', 'ContainerInsights')]", "promotionCode": "", "publisher": "Microsoft", }, }], }, "parameters": {}, }, }], } params = { "workspaceResourceId": { "value": workspace_resource_id }, "workspaceRegion": { "value": location }, "solutionDeploymentName": { "value": solution_deployment_name }, } deployment_name = "aks-monitoring-{}".format(unix_time_in_millis) # publish the Container Insights solution to the Log Analytics workspace return _invoke_deployment( cmd, resource_group, deployment_name, template, params, validate=False, no_wait=False, subscription_id=subscription_id, )
def handle_exception(ex): # pylint: disable=too-many-return-statements, too-many-statements # For error code, follow guidelines at https://docs.python.org/2/library/sys.html#sys.exit, from jmespath.exceptions import JMESPathTypeError from msrestazure.azure_exceptions import CloudError from msrest.exceptions import HttpOperationError, ValidationError, ClientRequestError from azure.cli.core.azlogging import CommandLoggerContext from azure.common import AzureException from azure.core.exceptions import AzureError from requests.exceptions import SSLError import traceback logger.debug( "azure.cli.core.util.handle_exception is called with an exception:") # Print the traceback and exception message logger.debug(traceback.format_exc()) with CommandLoggerContext(logger): error_msg = getattr(ex, 'message', str(ex)) exit_code = 1 if isinstance(ex, AzCLIError): az_error = ex elif isinstance(ex, JMESPathTypeError): error_msg = "Invalid jmespath query supplied for `--query`: {}".format( error_msg) az_error = AzCLIError(AzCLIErrorType.ArgumentParseError, error_msg) az_error.set_recommendation(QUERY_REFERENCE) elif isinstance(ex, ValidationError): az_error = AzCLIError(AzCLIErrorType.ValidationError, error_msg) # TODO: Fine-grained analysis to decide whether they are ValidationErrors elif isinstance(ex, (CLIError, CloudError, AzureError)): try: error_msg = ex.args[0] for detail in ex.args[0].error.details: error_msg += ('\n' + detail) except Exception: # pylint: disable=broad-except pass az_error = AzCLIError(AzCLIErrorType.ValidationError, error_msg) exit_code = ex.args[1] if len(ex.args) >= 2 else 1 # TODO: Fine-grained analysis elif isinstance(ex, AzureException): az_error = AzCLIError(AzCLIErrorType.ServiceError, error_msg) exit_code = ex.args[1] if len(ex.args) >= 2 else 1 # TODO: Fine-grained analysis elif isinstance(ex, (ClientRequestError, SSLError)): az_error = AzCLIError(AzCLIErrorType.ClientError, error_msg) if 'SSLError' in error_msg: az_error.set_recommendation(SSLERROR_TEMPLATE) # TODO: Fine-grained analysis elif isinstance(ex, HttpOperationError): try: response = json.loads(ex.response.text) if isinstance(response, str): error = response else: error = response['error'] # ARM should use ODATA v4. So should try this first. # http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 if isinstance(error, dict): code = "{} - ".format(error.get('code', 'Unknown Code')) message = error.get('message', ex) error_msg = "code: {}, {}".format(code, message) else: error_msg = error except (ValueError, KeyError): pass az_error = AzCLIError(AzCLIErrorType.ServiceError, error_msg) elif isinstance(ex, KeyboardInterrupt): error_msg = 'Keyboard interrupt is captured.' az_error = AzCLIError(AzCLIErrorType.ManualInterrupt, error_msg) else: error_msg = "The command failed with an unexpected error. Here is the traceback:" az_error = AzCLIError(AzCLIErrorType.UnexpectedError, error_msg) az_error.set_raw_exception(ex) az_error.set_recommendation( "To open an issue, please run: 'az feedback'") az_error.print_error() az_error.send_telemetry() return exit_code