diff --git a/src/aks-preview/azext_aks_preview/addonconfiguration.py b/src/aks-preview/azext_aks_preview/addonconfiguration.py index 1d74ada79b5..8d4e966d007 100644 --- a/src/aks-preview/azext_aks_preview/addonconfiguration.py +++ b/src/aks-preview/azext_aks_preview/addonconfiguration.py @@ -6,13 +6,16 @@ import json from knack.log import get_logger from knack.util import CLIError -from azure.cli.core.azclierror import ArgumentUsageError, ClientRequestError +from azure.cli.core.azclierror import ArgumentUsageError from azure.cli.core.commands import LongRunningOperation from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait +from azure.cli.command_modules.acs.addonconfiguration import ( + ensure_container_insights_for_monitoring, + sanitize_loganalytics_ws_resource_id, + ensure_default_log_analytics_workspace_for_monitoring +) from azext_aks_preview._client_factory import CUSTOM_MGMT_AKS_PREVIEW -from azext_aks_preview._client_factory import get_resources_client, get_resource_groups_client -from azext_aks_preview._resourcegroup import get_rg_location from azext_aks_preview._roleassignments import add_role_assignment from azext_aks_preview._consts import ( ADDONS, @@ -316,394 +319,6 @@ def update_addons(cmd, # pylint: disable=too-many-branches,too-many-statements return instance -def ensure_default_log_analytics_workspace_for_monitoring(cmd, subscription_id, resource_group_name): - # mapping for azure public cloud - # log analytics workspaces cannot be created in WCUS region due to capacity limits - # so mapped to EUS per discussion with log analytics team - AzureCloudLocationToOmsRegionCodeMap = { - "australiasoutheast": "ASE", - "australiaeast": "EAU", - "australiacentral": "CAU", - "canadacentral": "CCA", - "centralindia": "CIN", - "centralus": "CUS", - "eastasia": "EA", - "eastus": "EUS", - "eastus2": "EUS2", - "eastus2euap": "EAP", - "francecentral": "PAR", - "japaneast": "EJP", - "koreacentral": "SE", - "northeurope": "NEU", - "southcentralus": "SCUS", - "southeastasia": "SEA", - "uksouth": "SUK", - "usgovvirginia": "USGV", - "westcentralus": "EUS", - "westeurope": "WEU", - "westus": "WUS", - "westus2": "WUS2", - "brazilsouth": "CQ", - "brazilsoutheast": "BRSE", - "norwayeast": "NOE", - "southafricanorth": "JNB", - "northcentralus": "NCUS", - "uaenorth": "DXB", - "germanywestcentral": "DEWC", - "ukwest": "WUK", - "switzerlandnorth": "CHN", - "switzerlandwest": "CHW", - "uaecentral": "AUH" - } - AzureCloudRegionToOmsRegionMap = { - "australiacentral": "australiacentral", - "australiacentral2": "australiacentral", - "australiaeast": "australiaeast", - "australiasoutheast": "australiasoutheast", - "brazilsouth": "brazilsouth", - "canadacentral": "canadacentral", - "canadaeast": "canadacentral", - "centralus": "centralus", - "centralindia": "centralindia", - "eastasia": "eastasia", - "eastus": "eastus", - "eastus2": "eastus2", - "francecentral": "francecentral", - "francesouth": "francecentral", - "japaneast": "japaneast", - "japanwest": "japaneast", - "koreacentral": "koreacentral", - "koreasouth": "koreacentral", - "northcentralus": "northcentralus", - "northeurope": "northeurope", - "southafricanorth": "southafricanorth", - "southafricawest": "southafricanorth", - "southcentralus": "southcentralus", - "southeastasia": "southeastasia", - "southindia": "centralindia", - "uksouth": "uksouth", - "ukwest": "ukwest", - "westcentralus": "eastus", - "westeurope": "westeurope", - "westindia": "centralindia", - "westus": "westus", - "westus2": "westus2", - "norwayeast": "norwayeast", - "norwaywest": "norwayeast", - "switzerlandnorth": "switzerlandnorth", - "switzerlandwest": "switzerlandwest", - "uaenorth": "uaenorth", - "germanywestcentral": "germanywestcentral", - "germanynorth": "germanywestcentral", - "uaecentral": "uaecentral", - "eastus2euap": "eastus2euap", - "brazilsoutheast": "brazilsoutheast" - } - - # mapping for azure china cloud - # log analytics only support China East2 region - AzureChinaLocationToOmsRegionCodeMap = { - "chinaeast": "EAST2", - "chinaeast2": "EAST2", - "chinanorth": "EAST2", - "chinanorth2": "EAST2" - } - AzureChinaRegionToOmsRegionMap = { - "chinaeast": "chinaeast2", - "chinaeast2": "chinaeast2", - "chinanorth": "chinaeast2", - "chinanorth2": "chinaeast2" - } - - # mapping for azure us governmner cloud - AzureFairfaxLocationToOmsRegionCodeMap = { - "usgovvirginia": "USGV", - "usgovarizona": "PHX" - } - AzureFairfaxRegionToOmsRegionMap = { - "usgovvirginia": "usgovvirginia", - "usgovtexas": "usgovvirginia", - "usgovarizona": "usgovarizona" - } - - rg_location = get_rg_location(cmd.cli_ctx, resource_group_name) - cloud_name = cmd.cli_ctx.cloud.name - - if cloud_name.lower() == 'azurecloud': - workspace_region = AzureCloudRegionToOmsRegionMap.get( - rg_location, "eastus") - workspace_region_code = AzureCloudLocationToOmsRegionCodeMap.get( - workspace_region, "EUS") - elif cloud_name.lower() == 'azurechinacloud': - workspace_region = AzureChinaRegionToOmsRegionMap.get( - rg_location, "chinaeast2") - workspace_region_code = AzureChinaLocationToOmsRegionCodeMap.get( - workspace_region, "EAST2") - elif cloud_name.lower() == 'azureusgovernment': - workspace_region = AzureFairfaxRegionToOmsRegionMap.get( - rg_location, "usgovvirginia") - workspace_region_code = AzureFairfaxLocationToOmsRegionCodeMap.get( - workspace_region, "USGV") - else: - logger.error( - "AKS Monitoring addon not supported in cloud : %s", cloud_name) - - default_workspace_resource_group = 'DefaultResourceGroup-' + workspace_region_code - default_workspace_name = 'DefaultWorkspace-{0}-{1}'.format( - subscription_id, workspace_region_code) - - default_workspace_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights' \ - '/workspaces/{2}'.format(subscription_id, - default_workspace_resource_group, default_workspace_name) - resource_groups = get_resource_groups_client(cmd.cli_ctx, subscription_id) - resources = get_resources_client(cmd.cli_ctx, subscription_id) - - from azure.cli.core.profiles import ResourceType - # check if default RG exists - if resource_groups.check_existence(default_workspace_resource_group): - from azure.core.exceptions import HttpResponseError - try: - resource = resources.get_by_id( - default_workspace_resource_id, '2015-11-01-preview') - return resource.id - except HttpResponseError as ex: - if ex.status_code != 404: - raise ex - else: - ResourceGroup = cmd.get_models( - 'ResourceGroup', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) - resource_group = ResourceGroup(location=workspace_region) - resource_groups.create_or_update( - default_workspace_resource_group, resource_group) - - GenericResource = cmd.get_models( - 'GenericResource', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) - generic_resource = GenericResource(location=workspace_region, properties={ - 'sku': {'name': 'standalone'}}) - - async_poller = resources.begin_create_or_update_by_id(default_workspace_resource_id, '2015-11-01-preview', - generic_resource) - - ws_resource_id = '' - while True: - result = async_poller.result(15) - if async_poller.done(): - ws_resource_id = result.id - break - - return ws_resource_id - - -def sanitize_loganalytics_ws_resource_id(workspace_resource_id): - workspace_resource_id = workspace_resource_id.strip() - if not workspace_resource_id.startswith('/'): - workspace_resource_id = '/' + workspace_resource_id - if workspace_resource_id.endswith('/'): - workspace_resource_id = workspace_resource_id.rstrip('/') - return workspace_resource_id - - -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].strip( - ) - if not workspace_resource_id.startswith('/'): - workspace_resource_id = '/' + workspace_resource_id - - if workspace_resource_id.endswith('/'): - workspace_resource_id = workspace_resource_id.rstrip('/') - - # 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 CLIError( - '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 = get_resources_client(cmd.cli_ctx, subscription_id) - from azure.core.exceptions import HttpResponseError - 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}/providers/Microsoft.ContainerService/managedClusters/{cluster_name}" - dataCollectionRuleName = f"MSCI-{workspace_name}" - dcr_resource_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" - from azure.cli.core.util import send_raw_request - from azure.cli.core.profiles import ResourceType - - 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 CLIError 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 CLIError 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}') - elif 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 CLIError 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/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 CLIError as e: - error = e - else: - raise error - - def add_monitoring_role_assignment(result, cluster_resource_id, cmd): service_principal_msi_id = None # Check if service principal exists, if it does, assign permissions to service principal diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 00399b82cb1..dac9b67dafc 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -43,6 +43,12 @@ from six.moves.urllib.error import URLError from six.moves.urllib.request import urlopen +from azure.cli.command_modules.acs.addonconfiguration import ( + ensure_container_insights_for_monitoring, + sanitize_loganalytics_ws_resource_id, + ensure_default_log_analytics_workspace_for_monitoring +) + from azext_aks_preview._client_factory import ( CUSTOM_MGMT_AKS_PREVIEW, cf_agent_pools, @@ -97,10 +103,7 @@ add_ingress_appgw_addon_role_assignment, add_monitoring_role_assignment, add_virtual_node_role_assignment, - enable_addons, - ensure_container_insights_for_monitoring, - ensure_default_log_analytics_workspace_for_monitoring, - sanitize_loganalytics_ws_resource_id, + enable_addons ) from azext_aks_preview.aks_draft.commands import ( aks_draft_cmd_create, diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 7306429e239..c7aa793c9fd 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -2286,17 +2286,17 @@ def create_new_cluster_with_monitoring_aad_auth(self, resource_group, resource_g workspace_resource_group = workspace_resource_id.split("/")[4] # check that the DCR was created - dataCollectionRuleName = f"MSCI-{workspace_name}" + dataCollectionRuleName = f"MSCI-{aks_name}-{resource_group_location}" dcr_resource_id = f"/subscriptions/{subscription}/resourceGroups/{workspace_resource_group}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" - get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2019-11-01-preview' + get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2021-04-01' self.cmd(get_cmd, checks=[ self.check( 'properties.destinations.logAnalytics[0].workspaceResourceId', f'{workspace_resource_id}') ]) # check that the DCR-A was created - dcra_resource_id = f"{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/send-to-{workspace_name}" - get_cmd = f'rest --method get --url https://management.azure.com{dcra_resource_id}?api-version=2019-11-01-preview' + dcra_resource_id = f"{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/ContainerInsightsExtension" + get_cmd = f'rest --method get --url https://management.azure.com{dcra_resource_id}?api-version=2021-04-01' self.cmd(get_cmd, checks=[ self.check('properties.dataCollectionRuleId', f'{dcr_resource_id}') ]) @@ -2364,17 +2364,17 @@ def enable_monitoring_existing_cluster_aad_atuh(self, resource_group, resource_g workspace_resource_group = workspace_resource_id.split("/")[4] # check that the DCR was created - dataCollectionRuleName = f"MSCI-{workspace_name}" + dataCollectionRuleName = f"MSCI-{aks_name}-{resource_group_location}" dcr_resource_id = f"/subscriptions/{subscription}/resourceGroups/{workspace_resource_group}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" - get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2019-11-01-preview' + get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2021-04-01' self.cmd(get_cmd, checks=[ self.check( 'properties.destinations.logAnalytics[0].workspaceResourceId', f'{workspace_resource_id}') ]) # check that the DCR-A was created - dcra_resource_id = f"{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/send-to-{workspace_name}" - get_cmd = f'rest --method get --url https://management.azure.com{dcra_resource_id}?api-version=2019-11-01-preview' + dcra_resource_id = f"{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/ContainerInsightsExtension" + get_cmd = f'rest --method get --url https://management.azure.com{dcra_resource_id}?api-version=2021-04-01' self.cmd(get_cmd, checks=[ self.check('properties.dataCollectionRuleId', f'{dcr_resource_id}') ]) @@ -2417,14 +2417,13 @@ def test_aks_create_with_monitoring_legacy_auth(self, resource_group, resource_g cluster_resource_id = response["id"] subscription = cluster_resource_id.split("/")[2] workspace_resource_id = response["addonProfiles"]["omsagent"]["config"]["logAnalyticsWorkspaceResourceID"] - workspace_name = workspace_resource_id.split("/")[-1] workspace_resource_group = workspace_resource_id.split("/")[4] try: # check that the DCR was created - dataCollectionRuleName = f"MSCI-{workspace_name}" + dataCollectionRuleName = f"MSCI-{aks_name}-{resource_group_location}" dcr_resource_id = f"/subscriptions/{subscription}/resourceGroups/{workspace_resource_group}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" - get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2019-11-01-preview' + get_cmd = f'rest --method get --url https://management.azure.com{dcr_resource_id}?api-version=2021-04-01' self.cmd(get_cmd, checks=[ self.check( 'properties.destinations.logAnalytics[0].workspaceResourceId', f'{workspace_resource_id}') @@ -4403,7 +4402,7 @@ def test_aks_create_and_update_with_blob_csi_driver(self, resource_group, resour 'name': aks_name, 'ssh_key_value': self.generate_ssh_keys() }) - + # create aks with blob driver create_cmd = 'aks create --resource-group={resource_group} --name={name} --ssh-key-value={ssh_key_value} -o json \ --enable-blob-driver \ @@ -4412,7 +4411,7 @@ def test_aks_create_and_update_with_blob_csi_driver(self, resource_group, resour self.check('provisioningState', 'Succeeded'), self.check('storageProfile.blobCsiDriver.enabled', True), ]) - + # check standard reconcile scenario update_cmd = 'aks update --resource-group={resource_group} --name={name} -y -o json \ --aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/EnableBlobCSIDriver' @@ -4578,7 +4577,7 @@ def test_aks_create_with_web_application_routing(self, resource_group, resource_ @AllowLargeResponse() @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') def test_aks_create_web_application_routing_dns_zone_not_exist(self, resource_group, resource_group_location): - # Test creation failure when using an non-existing dns zone resource ID. + # Test creation failure when using an non-existing dns zone resource ID. aks_name = self.create_random_name('cliakstest', 16) self.kwargs.update({ 'resource_group': resource_group, @@ -4759,7 +4758,7 @@ def test_aks_draft_with_manifest(self): abs_file_path = os.path.join(script_dir, create_config) with tempfile.TemporaryDirectory() as tmp_dir: - + create_cmd = f'aks draft create --path={tmp_dir} --create-config={abs_file_path} --destination={tmp_dir}' self.cmd(create_cmd) assert os.path.isdir(f'{tmp_dir}/manifests') and os.path.isfile(f'{tmp_dir}/Dockerfile')