Esempio n. 1
0
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)
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
0
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,
    }
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
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
Esempio n. 8
0
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
Esempio n. 9
0
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)
Esempio n. 10
0
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)
Esempio n. 11
0
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
Esempio n. 12
0
    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)
Esempio n. 13
0
 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)
Esempio n. 14
0
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
Esempio n. 15
0
    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)
Esempio n. 16
0
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,
            )
Esempio n. 17
0
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