diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 66cd5aea491..3a9ff5ff82d 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +1.4.1 +++++++++++++++++++ +* microsoft.azureml.kubernetes: Fix sslSecret parameter in update operation +* microsoft.azuremonitor.containers.metrics : public preview support for managed prometheus in ARC clusters + 1.4.0 ++++++++++++++++++ * microsoft.dapr: Update version comparison logic to use semver based comparison @@ -39,7 +44,7 @@ Release History 1.3.4 ++++++++++++++++++ -* Fix to address the error TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running command az k8s-extension extension-types list +* Fix to address the error TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running command az k8s-extension extension-types list 1.3.3 ++++++++++++++++++ @@ -93,9 +98,9 @@ Release History 1.2.0 ++++++++++++++++++ * microsoft.azureml.kubernetes: Update AzureMLKubernetes install parameters on inferenceRouterServiceType and internalLoadBalancerProvider -* microsoft.openservicemesh: Change extension validation logic osm-arc -* microsoft.azuremonitor.containers: Add Managed Identity Auth support for ContainerInsights Extension -* microsoft.azuremonitor.containers: Bring back containerInsights solution addition in MSI mode +* microsoft.openservicemesh: Change extension validation logic osm-arc +* microsoft.azuremonitor.containers: Add Managed Identity Auth support for ContainerInsights Extension +* microsoft.azuremonitor.containers: Bring back containerInsights solution addition in MSI mode 1.1.0 ++++++++++++++++++ @@ -158,7 +163,7 @@ Release History 0.5.0 ++++++++++++++++++ * Add microsoft.openservicemesh customization to check distros -* Delete customization for partners +* Delete customization for partners 0.4.3 ++++++++++++++++++ @@ -195,7 +200,7 @@ Release History ++++++++++++++++++ * Remove `k8s-extension update` until PATCH is supported -* Improved logging for overwriting extension name with default +* Improved logging for overwriting extension name with default 0.2.0 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 96d98a3af86..323ef341fd4 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -24,6 +24,7 @@ from ._validators import validate_cc_registration from .partner_extensions.ContainerInsights import ContainerInsights +from .partner_extensions.AzureMonitorMetrics import AzureMonitorMetrics from .partner_extensions.AzureDefender import AzureDefender from .partner_extensions.OpenServiceMesh import OpenServiceMesh from .partner_extensions.AzureMLKubernetes import AzureMLKubernetes @@ -44,6 +45,7 @@ def ExtensionFactory(extension_name): extension_map = { "microsoft.azuremonitor.containers": ContainerInsights, + "microsoft.azuremonitor.containers.metrics": AzureMonitorMetrics, "microsoft.azuredefender.kubernetes": AzureDefender, "microsoft.openservicemesh": OpenServiceMesh, "microsoft.azureml.kubernetes": AzureMLKubernetes, diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index e0e88de3851..acd60254d91 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -366,14 +366,17 @@ def Update(self, cmd, resource_group_name, cluster_name, auto_upgrade_minor_vers configuration_protected_settings = _dereference(self.reference_mapping, configuration_protected_settings) - if self.sslKeyPemFile in configuration_protected_settings and \ - self.sslCertPemFile in configuration_protected_settings: - logger.info(f"Both {self.sslKeyPemFile} and {self.sslCertPemFile} are set, update ssl key.") - fe_ssl_cert_file = configuration_protected_settings.get(self.sslCertPemFile) - fe_ssl_key_file = configuration_protected_settings.get(self.sslKeyPemFile) - - if fe_ssl_cert_file and fe_ssl_key_file: - self.__set_inference_ssl_from_file(configuration_protected_settings, fe_ssl_cert_file, fe_ssl_key_file) + fe_ssl_secret = _get_value_from_config_protected_config( + self.SSL_SECRET, configuration_settings, configuration_protected_settings) + fe_ssl_cert_file = configuration_protected_settings.get(self.sslCertPemFile) + fe_ssl_key_file = configuration_protected_settings.get(self.sslKeyPemFile) + # always take ssl key/cert first, then secret if key/cert file is not provided + if fe_ssl_cert_file and fe_ssl_key_file: + logger.info(f"Both {self.sslKeyPemFile} and {self.sslCertPemFile} are set, updating ssl key.") + self.__set_inference_ssl_from_file(configuration_protected_settings, fe_ssl_cert_file, fe_ssl_key_file) + elif fe_ssl_secret: + logger.info(f"{self.SSL_SECRET} is set, updating ssl secret.") + self.__set_inference_ssl_from_secret(configuration_settings, fe_ssl_secret) # if no entries are existed in configuration_protected_settings, configuration_settings, return whatever passed # in the Update function(empty dict or None). diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMonitorMetrics.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMonitorMetrics.py new file mode 100644 index 00000000000..9eb4a74dab0 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMonitorMetrics.py @@ -0,0 +1,77 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +import datetime +import json +import re + +from ..utils import get_cluster_rp_api_version + +from knack.log import get_logger + +from azure.cli.core.commands.client_factory import get_subscription_id + +from ..vendored_sdks.models import Extension +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import Scope + +from .DefaultExtension import DefaultExtension +from .azuremonitormetrics.azuremonitorprofile import ensure_azure_monitor_profile_prerequisites, unlink_azure_monitor_profile_artifacts + +from .._client_factory import ( + cf_resources, cf_resource_groups, cf_log_analytics) + +logger = get_logger(__name__) + + +class AzureMonitorMetrics(DefaultExtension): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, cluster_rp, + extension_type, scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file, + plan_name, plan_publisher, plan_product): + """ExtensionType 'microsoft.azuremonitor.containers.metrics' specific validations & defaults for Create + Must create and return a valid 'Extension' object. + + """ + name = 'azuremonitor-metrics' + release_namespace = 'kube-system' + # Scope is always cluster + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + # If release-train is not input, set it to 'stable' + if release_train is None: + release_train = 'stable' + + cluster_subscription = get_subscription_id(cmd.cli_ctx) + ensure_azure_monitor_profile_prerequisites( + cmd, + cluster_rp, + cluster_subscription, + resource_group_name, + cluster_name, + configuration_settings, + cluster_type + ) + + create_identity = True + extension = Extension( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings + ) + return extension, name, create_identity + + def Delete(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, cluster_rp, yes): + # cluster_rp, _ = get_cluster_rp_api_version(cluster_type=cluster_type, cluster_rp=cluster_rp) + cluster_subscription = get_subscription_id(cmd.cli_ctx) + unlink_azure_monitor_profile_artifacts(cmd, cluster_subscription, resource_group_name, cluster_name) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amg/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amg/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amg/link.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amg/link.py new file mode 100644 index 00000000000..07bb2016e8a --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amg/link.py @@ -0,0 +1,91 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +import uuid +from knack.util import CLIError +from ..constants import ( + GRAFANA_API, + GRAFANA_ROLE_ASSIGNMENT_API, + GrafanaLink +) +from ..helper import safe_key_check, safe_value_get, sanitize_resource_id + + +def link_grafana_instance(cmd, azure_monitor_workspace_resource_id, configuration_settings): + from azure.cli.core.util import send_raw_request + # GET grafana principal ID + try: + grafana_resource_id = "" + if safe_key_check('grafana-resource-id', configuration_settings): + grafana_resource_id = safe_value_get('grafana-resource-id', configuration_settings) + if grafana_resource_id is None or grafana_resource_id == "": + return GrafanaLink.NOPARAMPROVIDED + grafana_resource_id = sanitize_resource_id(grafana_resource_id) + grafanaURI = "{0}{1}?api-version={2}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + grafana_resource_id, + GRAFANA_API + ) + headers = ['User-Agent=arc-azuremonitormetrics.link_grafana_instance'] + grafanaArmResponse = send_raw_request(cmd.cli_ctx, "GET", grafanaURI, body={}, headers=headers) + servicePrincipalId = grafanaArmResponse.json()["identity"]["principalId"] + except CLIError as e: + raise CLIError(e) + # Add Role Assignment + try: + MonitoringDataReader = "b0d8363b-8ddd-447d-831f-62ca05bff136" + roleDefinitionURI = "{0}{1}/providers/Microsoft.Authorization/roleAssignments/{2}?api-version={3}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + azure_monitor_workspace_resource_id, + uuid.uuid4(), + GRAFANA_ROLE_ASSIGNMENT_API + ) + roleDefinitionId = "{0}/providers/Microsoft.Authorization/roleDefinitions/{1}".format( + azure_monitor_workspace_resource_id, + MonitoringDataReader + ) + association_body = json.dumps({ + "properties": { + "roleDefinitionId": roleDefinitionId, + "principalId": servicePrincipalId + } + }) + headers = ['User-Agent=arc-azuremonitormetrics.add_role_assignment'] + send_raw_request(cmd.cli_ctx, "PUT", roleDefinitionURI, body=association_body, headers=headers) + except CLIError as e: + if e.response.status_code != 409: + erroString = "Role Assingment failed. Please manually assign the `Monitoring Data Reader` role\ + to the Azure Monitor Workspace ({0}) for the Azure Managed Grafana\ + System Assigned Managed Identity ({1})".format( + azure_monitor_workspace_resource_id, + servicePrincipalId + ) + print(erroString) + # Setting up AMW Integration + targetGrafanaArmPayload = grafanaArmResponse.json() + if targetGrafanaArmPayload["properties"] is None: + raise CLIError("Invalid grafana payload to add AMW integration") + if "grafanaIntegrations" not in json.dumps(targetGrafanaArmPayload): + targetGrafanaArmPayload["properties"]["grafanaIntegrations"] = {} + if "azureMonitorWorkspaceIntegrations" not in json.dumps(targetGrafanaArmPayload): + targetGrafanaArmPayload["properties"]["grafanaIntegrations"]["azureMonitorWorkspaceIntegrations"] = [] + amwIntegrations = targetGrafanaArmPayload["properties"]["grafanaIntegrations"]["azureMonitorWorkspaceIntegrations"] + if amwIntegrations != [] and azure_monitor_workspace_resource_id in json.dumps(amwIntegrations).lower(): + return GrafanaLink.ALREADYPRESENT + try: + grafanaURI = "{0}{1}?api-version={2}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + grafana_resource_id, + GRAFANA_API + ) + targetGrafanaArmPayload["properties"]["grafanaIntegrations"]["azureMonitorWorkspaceIntegrations"].append({ + "azureMonitorWorkspaceResourceId": azure_monitor_workspace_resource_id + }) + targetGrafanaArmPayload = json.dumps(targetGrafanaArmPayload) + headers = ['User-Agent=arc-azuremonitormetrics.setup_amw_grafana_integration', 'Content-Type=application/json'] + send_raw_request(cmd.cli_ctx, "PUT", grafanaURI, body=targetGrafanaArmPayload, headers=headers) + except CLIError as e: + raise CLIError(e) + return GrafanaLink.SUCCESS diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/create.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/create.py new file mode 100644 index 00000000000..03f904895ab --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/create.py @@ -0,0 +1,45 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from azure.core.exceptions import HttpResponseError +from knack.util import CLIError + +from ..constants import MAC_API +from .defaults import get_default_mac_name_and_region +from ...._client_factory import cf_resources, cf_resource_groups + + +def create_default_mac(cmd, cluster_subscription, cluster_region): + from azure.cli.core.util import send_raw_request + default_mac_name, default_mac_region = get_default_mac_name_and_region(cmd, cluster_region) + default_resource_group_name = f"DefaultResourceGroup-{default_mac_region}" + azure_monitor_workspace_resource_id = f"/subscriptions/{cluster_subscription}/resourceGroups/{default_resource_group_name}/providers/microsoft.monitor/accounts/{default_mac_name}" + # Check if default resource group exists or not, if it does not then create it + resource_groups = cf_resource_groups(cmd.cli_ctx, cluster_subscription) + resources = cf_resources(cmd.cli_ctx, cluster_subscription) + + if resource_groups.check_existence(default_resource_group_name): + try: + resource = resources.get_by_id(azure_monitor_workspace_resource_id, MAC_API) + # If MAC already exists then return from here + # location can have spaces for example 'East US' + # and some workspaces it will be "eastus" hence remove the spaces and converting lowercase + amw_location = resource.location.replace(" ", "").lower() + return azure_monitor_workspace_resource_id, amw_location + except HttpResponseError as ex: + if ex.status_code != 404: + raise ex + else: + resource_groups.create_or_update(default_resource_group_name, {"location": default_mac_region}) + association_body = json.dumps({"location": default_mac_region, "properties": {}}) + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}{azure_monitor_workspace_resource_id}?api-version={MAC_API}" + try: + headers = ['User-Agent=arc-azuremonitormetrics.create_default_mac'] + send_raw_request(cmd.cli_ctx, "PUT", association_url, + body=association_body, headers=headers) + return azure_monitor_workspace_resource_id, default_mac_region.lower() + except CLIError as e: + raise e diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/defaults.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/defaults.py new file mode 100644 index 00000000000..996eeafbcbf --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/defaults.py @@ -0,0 +1,40 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from ..deaults import get_default_region +from ..responseparsers.amwlocationresponseparser import ( + parseResourceProviderResponseForLocations +) +from ..constants import RP_LOCATION_API +from knack.util import CLIError + + +def get_supported_rp_locations(cmd, rp_name): + from azure.cli.core.util import send_raw_request + supported_locations = [] + headers = ['User-Agent=arc-azuremonitormetrics.get_supported_rp_locations'] + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}/providers/{rp_name}?api-version={RP_LOCATION_API}" + r = send_raw_request(cmd.cli_ctx, "GET", association_url, headers=headers) + data = json.loads(r.text) + supported_locations = parseResourceProviderResponseForLocations(data) + return supported_locations + + +def get_default_mac_region(cmd, cluster_region): + supported_locations = get_supported_rp_locations(cmd, 'Microsoft.Monitor') + if cluster_region in supported_locations: + return cluster_region + if len(supported_locations) > 0: + return supported_locations[0] + # default to eastus/usgovvirginia based on cloud (mooncake not supported yet) + return get_default_region(cmd) + + +def get_default_mac_name_and_region(cmd, cluster_region): + default_mac_region = get_default_mac_region(cmd, cluster_region) + default_mac_name = "DefaultAzureMonitorWorkspace-" + default_mac_region + default_mac_name = default_mac_name[0:43] + return default_mac_name, default_mac_region diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/helper.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/helper.py new file mode 100644 index 00000000000..dcf1d01b405 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/amw/helper.py @@ -0,0 +1,39 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from azure.core.exceptions import HttpResponseError +from .create import create_default_mac +from ..helper import sanitize_resource_id, safe_key_check, safe_value_get +from ..constants import MAC_API +from ...._client_factory import cf_resources + + +def get_amw_region(cmd, azure_monitor_workspace_resource_id): + # Region of MAC can be different from region of RG so find the location of the azure_monitor_workspace_resource_id + amw_subscription_id = azure_monitor_workspace_resource_id.split("/")[2] + resources = cf_resources(cmd.cli_ctx, amw_subscription_id) + try: + resource = resources.get_by_id( + azure_monitor_workspace_resource_id, MAC_API) + amw_location = resource.location.replace(" ", "").lower() + return amw_location + except HttpResponseError as ex: + raise ex + + +def get_azure_monitor_workspace_resource(cmd, cluster_subscription, cluster_region, configuration_settings): + azure_monitor_workspace_resource_id = "" + if safe_key_check('azure-monitor-workspace-resource-id', configuration_settings): + azure_monitor_workspace_resource_id = safe_value_get('azure-monitor-workspace-resource-id', configuration_settings) + if azure_monitor_workspace_resource_id is None or azure_monitor_workspace_resource_id == "": + azure_monitor_workspace_resource_id, azure_monitor_workspace_location = create_default_mac( + cmd, + cluster_subscription, + cluster_region + ) + else: + azure_monitor_workspace_resource_id = sanitize_resource_id(azure_monitor_workspace_resource_id) + azure_monitor_workspace_location = get_amw_region(cmd, azure_monitor_workspace_resource_id) + print(f"Using Azure Monitor Workspace (stores prometheus metrics) : {azure_monitor_workspace_resource_id}") + return azure_monitor_workspace_resource_id, azure_monitor_workspace_location diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/azuremonitorprofile.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/azuremonitorprofile.py new file mode 100644 index 00000000000..dc574ea8657 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/azuremonitorprofile.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from azure.cli.core.azclierror import InvalidArgumentValueError +from knack.util import CLIError +from .helper import ( + get_cluster_region, + rp_registrations +) +from .amw.helper import get_azure_monitor_workspace_resource +from .dc.dce_api import create_dce +from .dc.dcr_api import create_dcr +from .dc.dcra_api import create_dcra +from .amg.link import link_grafana_instance +from .recordingrules.create import create_rules +from .recordingrules.delete import delete_rules +from .dc.delete import get_dc_objects_list, delete_dc_objects_if_prometheus_enabled +from .helper import safe_key_check, safe_value_get + + +# pylint: disable=line-too-long +def link_azure_monitor_profile_artifacts( + cmd, + cluster_rp, + cluster_subscription, + cluster_resource_group_name, + cluster_name, + configuration_settings, + cluster_type +): + cluster_region = get_cluster_region(cmd, cluster_rp, cluster_subscription, cluster_resource_group_name, cluster_name, cluster_type) + # MAC creation if required + azure_monitor_workspace_resource_id, azure_monitor_workspace_location = get_azure_monitor_workspace_resource(cmd, cluster_subscription, cluster_region, configuration_settings) + # DCE creation + dce_resource_id = create_dce(cmd, cluster_subscription, cluster_resource_group_name, cluster_name, azure_monitor_workspace_location) + # DCR creation + dcr_resource_id = create_dcr(cmd, azure_monitor_workspace_location, azure_monitor_workspace_resource_id, cluster_subscription, cluster_resource_group_name, cluster_name, dce_resource_id) + # DCRA creation + create_dcra(cmd, cluster_region, cluster_subscription, cluster_resource_group_name, cluster_name, dcr_resource_id) + # Link grafana + link_grafana_instance(cmd, azure_monitor_workspace_resource_id, configuration_settings) + # create recording rules and alerts + create_rules(cmd, cluster_subscription, cluster_resource_group_name, cluster_name, azure_monitor_workspace_resource_id, azure_monitor_workspace_location) + + +# pylint: disable=line-too-long +def unlink_azure_monitor_profile_artifacts(cmd, cluster_subscription, cluster_resource_group_name, cluster_name): + # Remove DC* if prometheus is enabled + dc_objects_list = get_dc_objects_list(cmd, cluster_subscription, cluster_resource_group_name, cluster_name) + delete_dc_objects_if_prometheus_enabled(cmd, dc_objects_list, cluster_subscription, cluster_resource_group_name, cluster_name) + # Delete rules (Conflict({"error":{"code":"InvalidResourceLocation","message":"The resource 'NodeRecordingRulesRuleGroup-' already exists in location 'eastus2' in resource group ''. + # A resource with the same name cannot be created in location 'eastus'. Please select a new resource name."}}) + delete_rules(cmd, cluster_subscription, cluster_resource_group_name, cluster_name) + + +# pylint: disable=too-many-locals,too-many-branches,too-many-statements,line-too-long +def ensure_azure_monitor_profile_prerequisites( + cmd, + cluster_rp, + cluster_subscription, + cluster_resource_group_name, + cluster_name, + configuration_settings, + cluster_type +): + cloud_name = cmd.cli_ctx.cloud.name + if cloud_name.lower() == 'azurechinacloud': + raise CLIError("Azure China Cloud is not supported for the Azure Monitor Metrics extension") + + if cloud_name.lower() == "azureusgovernment": + if safe_key_check('grafana-resource-id', configuration_settings): + grafana_resource_id = safe_value_get('grafana-resource-id', configuration_settings) + if grafana_resource_id is not None: + if grafana_resource_id != "": + raise InvalidArgumentValueError("Azure US Government cloud does not support Azure Managed Grarfana yet. Please follow this documenation for enabling it via the public cloud : aka.ms/ama-grafana-link-ff") + + # Do RP registrations if required + rp_registrations(cmd, cluster_subscription) + link_azure_monitor_profile_artifacts( + cmd, + cluster_rp, + cluster_subscription, + cluster_resource_group_name, + cluster_name, + configuration_settings, + cluster_type + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/constants.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/constants.py new file mode 100644 index 00000000000..0abf3081f01 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/constants.py @@ -0,0 +1,89 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from enum import Enum + +AKS_CLUSTER_API = "2023-01-01" +MAC_API = "2023-04-03" +DC_API = "2022-06-01" +GRAFANA_API = "2022-08-01" +GRAFANA_ROLE_ASSIGNMENT_API = "2022-04-01" +RULES_API = "2023-03-01" +FEATURE_API = "2020-09-01" +RP_API = "2021-04-01" +ALERTS_API = "2023-01-01-preview" +RP_LOCATION_API = "2022-01-01" +ClUSTER_RESOURCE_API = "2020-01-01-preview" + +CLUSTER_RESOURCE_ID = "/subscriptions/{0}/resourceGroups/{1}/providers/{2}/{3}/{4}" + +MapToClosestMACRegion = { + "australiacentral": "eastus", + "australiacentral2": "eastus", + "australiaeast": "eastus", + "australiasoutheast": "eastus", + "brazilsouth": "eastus", + "canadacentral": "eastus", + "canadaeast": "eastus", + "centralus": "centralus", + "centralindia": "centralindia", + "eastasia": "westeurope", + "eastus": "eastus", + "eastus2": "eastus2", + "francecentral": "westeurope", + "francesouth": "westeurope", + "japaneast": "eastus", + "japanwest": "eastus", + "koreacentral": "westeurope", + "koreasouth": "westeurope", + "northcentralus": "eastus", + "northeurope": "westeurope", + "southafricanorth": "westeurope", + "southafricawest": "westeurope", + "southcentralus": "eastus", + "southeastasia": "westeurope", + "southindia": "centralindia", + "uksouth": "westeurope", + "ukwest": "westeurope", + "westcentralus": "eastus", + "westeurope": "westeurope", + "westindia": "centralindia", + "westus": "westus", + "westus2": "westus2", + "westus3": "westus", + "norwayeast": "westeurope", + "norwaywest": "westeurope", + "switzerlandnorth": "westeurope", + "switzerlandwest": "westeurope", + "uaenorth": "westeurope", + "germanywestcentral": "westeurope", + "germanynorth": "westeurope", + "uaecentral": "westeurope", + "eastus2euap": "eastus2euap", + "centraluseuap": "westeurope", + "brazilsoutheast": "eastus", + "jioindiacentral": "centralindia", + "swedencentral": "westeurope", + "swedensouth": "westeurope", + "qatarcentral": "westeurope" +} + + +class GrafanaLink(Enum): + """ + Status of Grafana link to the Prometheus Addon + """ + SUCCESS = "SUCCESS" + FAILURE = "FAILURE" + ALREADYPRESENT = "ALREADYPRESENT" + NOPARAMPROVIDED = "NOPARAMPROVIDED" + + +class DC_TYPE(Enum): + """ + Types of DC* objects + """ + DCE = "DCE" + DCR = "DCR" + DCRA = "DCRA" diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dce_api.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dce_api.py new file mode 100644 index 00000000000..100cd6dffac --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dce_api.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from knack.util import CLIError +from ..constants import DC_API +from .defaults import get_default_dce_name + + +def create_dce(cmd, cluster_subscription, cluster_resource_group_name, cluster_name, mac_region): + from azure.cli.core.util import send_raw_request + dce_name = get_default_dce_name(cmd, mac_region, cluster_name) + dce_resource_id = f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/providers/Microsoft.Insights/dataCollectionEndpoints/{dce_name}" + try: + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + dce_url = f"{armendpoint}{dce_resource_id}?api-version={DC_API}" + dce_creation_body = json.dumps({"name": dce_name, + "location": mac_region, + "kind": "Linux", + "properties": {}}) + headers = ['User-Agent=arc-azuremonitormetrics.create_dce'] + send_raw_request(cmd.cli_ctx, "PUT", + dce_url, body=dce_creation_body, headers=headers) + return dce_resource_id + except CLIError as error: + raise error diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcr_api.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcr_api.py new file mode 100644 index 00000000000..0363a99140f --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcr_api.py @@ -0,0 +1,45 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from knack.util import CLIError +from ..constants import MapToClosestMACRegion +from .defaults import get_default_region, sanitize_name +from ..constants import ( + DC_TYPE, + DC_API +) + + +def get_default_dcr_name(cmd, mac_region, cluster_name): + region = get_default_region(cmd) + if dict.get(MapToClosestMACRegion, mac_region): + region = MapToClosestMACRegion[mac_region] + default_dcr_name = "MSProm-" + region + "-" + cluster_name + return sanitize_name(default_dcr_name, DC_TYPE.DCR, 64) + + +# pylint: disable=too-many-locals,too-many-branches,too-many-statements,line-too-long +def create_dcr(cmd, mac_region, azure_monitor_workspace_resource_id, cluster_subscription, cluster_resource_group_name, cluster_name, dce_resource_id): + from azure.cli.core.util import send_raw_request + dcr_name = get_default_dcr_name(cmd, mac_region, cluster_name) + dcr_resource_id = f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/providers/Microsoft.Insights/dataCollectionRules/{dcr_name}" + dcr_creation_body = json.dumps({"location": mac_region, + "kind": "Linux", + "properties": { + "dataCollectionEndpointId": dce_resource_id, + "dataSources": {"prometheusForwarder": [{"name": "PrometheusDataSource", "streams": ["Microsoft-PrometheusMetrics"], "labelIncludeFilter": {}}]}, + "dataFlows": [{"destinations": ["MonitoringAccount1"], "streams": ["Microsoft-PrometheusMetrics"]}], + "description": "DCR description", + "destinations": { + "monitoringAccounts": [{"accountResourceId": azure_monitor_workspace_resource_id, "name": "MonitoringAccount1"}]}}}) + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + dcr_url = f"{armendpoint}{dcr_resource_id}?api-version={DC_API}" + try: + headers = ['User-Agent=arc-azuremonitormetrics.create_dcr'] + send_raw_request(cmd.cli_ctx, "PUT", + dcr_url, body=dcr_creation_body, headers=headers) + return dcr_resource_id + except CLIError as error: + raise error diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcra_api.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcra_api.py new file mode 100644 index 00000000000..66e4688cf33 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/dcra_api.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from knack.util import CLIError +from ..constants import DC_API +from .defaults import get_default_dcra_name + + +# pylint: disable=line-too-long +def create_dcra(cmd, cluster_region, cluster_subscription, cluster_resource_group_name, cluster_name, dcr_resource_id): + from azure.cli.core.util import send_raw_request + cluster_resource_id = f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/providers/Microsoft.Kubernetes/connectedClusters/{cluster_name}" + dcra_name = get_default_dcra_name(cmd, cluster_region, cluster_name) + dcra_resource_id = f"/subscriptions/{cluster_subscription}/resourceGroups/{cluster_resource_group_name}/providers/Microsoft.Insights/dataCollectionRuleAssociations/{dcra_name}" + description_str = "Promtheus data collection association between DCR, DCE and target AKS resource" + # only create or delete the association between the DCR and cluster + association_body = json.dumps({"location": cluster_region, + "properties": { + "dataCollectionRuleId": dcr_resource_id, + "description": description_str + }}) + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/{dcra_name}?api-version={DC_API}" + try: + headers = ['User-Agent=arc-azuremonitormetrics.create_dcra'] + send_raw_request(cmd.cli_ctx, "PUT", association_url, + body=association_body, headers=headers) + return dcra_resource_id + except CLIError as error: + raise error diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/defaults.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/defaults.py new file mode 100644 index 00000000000..8dd7260377e --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/defaults.py @@ -0,0 +1,41 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from ..constants import ( + DC_TYPE, + MapToClosestMACRegion +) +from ..deaults import get_default_region + + +# DCR = 64, DCE = 44, DCRA = 64 +# All DC* object names should end only in alpha numeric (after `length` trim) +# DCE remove underscore from cluster name +def sanitize_name(name, objtype, length): + length = length - 1 + if objtype == DC_TYPE.DCE: + name = name.replace("_", "") + name = name[0:length] + lastIndexAlphaNumeric = len(name) - 1 + while ((name[lastIndexAlphaNumeric].isalnum() is False) and lastIndexAlphaNumeric > -1): + lastIndexAlphaNumeric = lastIndexAlphaNumeric - 1 + if lastIndexAlphaNumeric < 0: + return "" + return name[0:lastIndexAlphaNumeric + 1] + + +def get_default_dce_name(cmd, mac_region, cluster_name): + region = get_default_region(cmd) + if dict.get(MapToClosestMACRegion, mac_region): + region = MapToClosestMACRegion[mac_region] + default_dce_name = "MSProm-" + region + "-" + cluster_name + return sanitize_name(default_dce_name, DC_TYPE.DCE, 44) + + +def get_default_dcra_name(cmd, cluster_region, cluster_name): + region = get_default_region(cmd) + if dict.get(MapToClosestMACRegion, cluster_region): + region = MapToClosestMACRegion[cluster_region] + default_dcra_name = "ContainerInsightsMetricsExtension-" + region + "-" + cluster_name + return sanitize_name(default_dcra_name, DC_TYPE.DCRA, 64) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/delete.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/delete.py new file mode 100644 index 00000000000..40d92ac7933 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/dc/delete.py @@ -0,0 +1,79 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from ..constants import DC_API +from knack.util import CLIError + + +def get_dce_from_dcr(cmd, dcrId): + from azure.cli.core.util import send_raw_request + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}{dcrId}?api-version={DC_API}" + headers = ['User-Agent=arc-azuremonitormetrics.get_dce_from_dcr'] + r = send_raw_request(cmd.cli_ctx, "GET", association_url, headers=headers) + data = json.loads(r.text) + if 'dataCollectionEndpointId' in data['properties']: + return str(data['properties']['dataCollectionEndpointId']) + return "" + + +# pylint: disable=line-too-long +def get_dc_objects_list(cmd, cluster_subscription, cluster_resource_group_name, cluster_name): + try: + from azure.cli.core.util import send_raw_request + cluster_resource_id = \ + "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes/connectedClusters/{2}".format( + cluster_subscription, + cluster_resource_group_name, + cluster_name + ) + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations?api-version={DC_API}" + headers = ['User-Agent=arc-azuremonitormetrics.get_dcra'] + r = send_raw_request(cmd.cli_ctx, "GET", association_url, headers=headers) + data = json.loads(r.text) + dc_object_array = [] + for item in data['value']: + if 'properties' in item and 'dataCollectionRuleId' in item['properties']: + dce_id = get_dce_from_dcr(cmd, item['properties']['dataCollectionRuleId']) + dc_object_array.append({'name': item['name'], 'dataCollectionRuleId': item['properties']['dataCollectionRuleId'], 'dceId': dce_id}) + return dc_object_array + except CLIError as e: + error = e + raise CLIError(error) + + +# pylint: disable=line-too-long +def delete_dc_objects_if_prometheus_enabled(cmd, dc_objects_list, cluster_subscription, cluster_resource_group_name, cluster_name): + from azure.cli.core.util import send_raw_request + cluster_resource_id = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes/connectedClusters/{2}".format( + cluster_subscription, + cluster_resource_group_name, + cluster_name + ) + for item in dc_objects_list: + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + association_url = f"{armendpoint}{item['dataCollectionRuleId']}?api-version={DC_API}" + try: + headers = ['User-Agent=arc-azuremonitormetrics.get_dcr_if_prometheus_enabled'] + r = send_raw_request(cmd.cli_ctx, "GET", association_url, headers=headers) + data = json.loads(r.text) + if 'microsoft-prometheusmetrics' in [stream.lower() for stream in data['properties']['dataFlows'][0]['streams']]: + # delete DCRA + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + url = f"{armendpoint}{cluster_resource_id}/providers/Microsoft.Insights/dataCollectionRuleAssociations/{item['name']}?api-version={DC_API}" + headers = ['User-Agent=arc-azuremonitormetrics.delete_dcra'] + send_raw_request(cmd.cli_ctx, "DELETE", url, headers=headers) + # delete DCR + url = f"{armendpoint}{item['dataCollectionRuleId']}?api-version={DC_API}" + headers = ['User-Agent=arc-azuremonitormetrics.delete_dcr'] + send_raw_request(cmd.cli_ctx, "DELETE", url, headers=headers) + # delete DCE + url = f"{armendpoint}{item['dceId']}?api-version={DC_API}" + headers = ['User-Agent=arc-azuremonitormetrics.delete_dce'] + send_raw_request(cmd.cli_ctx, "DELETE", url, headers=headers) + except CLIError as e: + error = e + raise CLIError(error) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/deaults.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/deaults.py new file mode 100644 index 00000000000..2e338217c2a --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/deaults.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from knack.util import CLIError + + +def get_default_region(cmd): + cloud_name = cmd.cli_ctx.cloud.name + if cloud_name.lower() == 'azurechinacloud': + raise CLIError("Azure China Cloud is not supported for the Azure Monitor Metrics addon") + if cloud_name.lower() == 'azureusgovernment': + return "usgovvirginia" + return "eastus" diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/helper.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/helper.py new file mode 100644 index 00000000000..2169ac0a3b3 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/helper.py @@ -0,0 +1,119 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from knack.util import CLIError +from azure.cli.core.azclierror import ( + UnknownError +) +from .constants import ( + RP_API, ClUSTER_RESOURCE_API, CLUSTER_RESOURCE_ID +) +from ..._client_factory import ( + cf_resources, cf_resource_groups, cf_log_analytics) +from azure.core.exceptions import HttpResponseError +from ... import consts + + +def sanitize_resource_id(resource_id): + resource_id = resource_id.strip() + if not resource_id.startswith("/"): + resource_id = "/" + resource_id + if resource_id.endswith("/"): + resource_id = resource_id.rstrip("/") + return resource_id.lower() + + +def post_request(cmd, subscription_id, rp_name, headers): + from azure.cli.core.util import send_raw_request + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + customUrl = "{0}/subscriptions/{1}/providers/{2}/register?api-version={3}".format( + armendpoint, + subscription_id, + rp_name, + RP_API, + ) + try: + send_raw_request(cmd.cli_ctx, "POST", customUrl, headers=headers) + except CLIError as e: + raise CLIError(e) + + +# pylint: disable=line-too-long +def rp_registrations(cmd, subscription_id): + from azure.cli.core.util import send_raw_request + # Get list of RP's for RP's subscription + try: + headers = ['User-Agent=arc-azuremonitormetrics.get_mac_sub_list'] + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + customUrl = "{0}/subscriptions/{1}/providers?api-version={2}&$select=namespace,registrationstate".format( + armendpoint, + subscription_id, + RP_API + ) + r = send_raw_request(cmd.cli_ctx, "GET", customUrl, headers=headers) + except CLIError as e: + raise CLIError(e) + isInsightsRpRegistered = False + isAlertsManagementRpRegistered = False + isMoniotrRpRegistered = False + isDashboardRpRegistered = False + json_response = json.loads(r.text) + values_array = json_response["value"] + for value in values_array: + if value["namespace"].lower() == "microsoft.insights" and value["registrationState"].lower() == "registered": + isInsightsRpRegistered = True + if value["namespace"].lower() == "microsoft.alertsmanagement" and value["registrationState"].lower() == "registered": + isAlertsManagementRpRegistered = True + if value["namespace"].lower() == "microsoft.monitor" and value["registrationState"].lower() == "registered": + isAlertsManagementRpRegistered = True + if value["namespace"].lower() == "microsoft.dashboard" and value["registrationState"].lower() == "registered": + isAlertsManagementRpRegistered = True + if isInsightsRpRegistered is False: + print(f"Registering microsoft.insights RP for the subscription {subscription_id}") + headers = ['User-Agent=arc-azuremonitormetrics.register_insights_rp'] + post_request(cmd, subscription_id, "microsoft.insights", headers) + if isAlertsManagementRpRegistered is False: + print(f"Registering microsoft.alertsmanagement RP for the subscription {subscription_id}") + headers = ['User-Agent=arc-azuremonitormetrics.register_alertsmanagement_rp'] + post_request(cmd, subscription_id, "microsoft.alertsmanagement", headers) + if isMoniotrRpRegistered is False: + print(f"Registering microsoft.monitor RP for the subscription {subscription_id}") + headers = ['User-Agent=arc-azuremonitormetrics.register_monitor_rp'] + post_request(cmd, subscription_id, "microsoft.monitor", headers) + if isDashboardRpRegistered is False: + print(f"Registering microsoft.dashboard RP for the subscription {subscription_id}") + headers = ['User-Agent=arc-azuremonitormetrics.register_dashboard_rp'] + post_request(cmd, subscription_id, "microsoft.dashboard", headers) + + +def get_cluster_region(cmd, cluster_rp, subscription_id, cluster_resource_group_name, cluster_name, cluster_type): + cluster_region = '' + resources = cf_resources(cmd.cli_ctx, subscription_id) + cluster_resource_id = CLUSTER_RESOURCE_ID.format( + subscription_id, cluster_resource_group_name, cluster_rp, cluster_type, cluster_name) + try: + if cluster_rp.lower() == consts.HYBRIDCONTAINERSERVICE_RP: + resource = resources.get_by_id(cluster_resource_id, consts.HYBRIDCONTAINERSERVICE_API_VERSION) + else: + resource = resources.get_by_id(cluster_resource_id, ClUSTER_RESOURCE_API) + cluster_region = resource.location.lower() + except HttpResponseError as ex: + raise ex + return cluster_region + + +def safe_key_check(key_to_check, strStrDict): + if strStrDict is None or key_to_check is None: + return False + if key_to_check.lower() in [key.lower() for key in strStrDict.keys()]: + return True + return False + + +def safe_value_get(key_to_find, strStrDict): + for key in strStrDict: + if key.lower() == key_to_find.lower(): + return strStrDict[key] + return "" diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/create.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/create.py new file mode 100644 index 00000000000..5f022b6bb88 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/create.py @@ -0,0 +1,78 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import json +from ..constants import ALERTS_API, RULES_API +from knack.util import CLIError + + +# pylint: disable=line-too-long +def get_recording_rules_template(cmd, azure_monitor_workspace_resource_id): + from azure.cli.core.util import send_raw_request + headers = ['User-Agent=arc-azuremonitormetrics.get_recording_rules_template'] + armendpoint = cmd.cli_ctx.cloud.endpoints.resource_manager + url = f"{armendpoint}{azure_monitor_workspace_resource_id}/providers/microsoft.alertsManagement/alertRuleRecommendations?api-version={ALERTS_API}" + r = send_raw_request(cmd.cli_ctx, "GET", url, headers=headers) + data = json.loads(r.text) + return data['value'] + + +# pylint: disable=line-too-long +def put_rules(cmd, default_rule_group_id, default_rule_group_name, mac_region, azure_monitor_workspace_resource_id, cluster_name, default_rules_template, url, enable_rules, i): + from azure.cli.core.util import send_raw_request + body = json.dumps({ + "id": default_rule_group_id, + "name": default_rule_group_name, + "type": "Microsoft.AlertsManagement/prometheusRuleGroups", + "location": mac_region, + "properties": { + "scopes": [ + azure_monitor_workspace_resource_id + ], + "enabled": enable_rules, + "clusterName": cluster_name, + "interval": "PT1M", + "rules": default_rules_template[i]["properties"]["rulesArmTemplate"]["resources"][0]["properties"]["rules"] + } + }) + for _ in range(3): + try: + headers = ['User-Agent=arc-azuremonitormetrics.put_rules.' + default_rule_group_name] + send_raw_request(cmd.cli_ctx, "PUT", url, + body=body, headers=headers) + break + except CLIError as e: + error = e + else: + raise error + + +# pylint: disable=line-too-long +def create_rules(cmd, cluster_subscription, cluster_resource_group_name, cluster_name, azure_monitor_workspace_resource_id, mac_region): + default_rules_template = get_recording_rules_template(cmd, azure_monitor_workspace_resource_id) + default_rule_group_name = "NodeRecordingRulesRuleGroup-{0}".format(cluster_name) + default_rule_group_id = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.AlertsManagement/prometheusRuleGroups/{2}".format( + cluster_subscription, + cluster_resource_group_name, + default_rule_group_name + ) + url = "{0}{1}?api-version={2}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + default_rule_group_id, + RULES_API + ) + put_rules(cmd, default_rule_group_id, default_rule_group_name, mac_region, azure_monitor_workspace_resource_id, cluster_name, default_rules_template, url, True, 0) + + default_rule_group_name = "KubernetesRecordingRulesRuleGroup-{0}".format(cluster_name) + default_rule_group_id = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.AlertsManagement/prometheusRuleGroups/{2}".format( + cluster_subscription, + cluster_resource_group_name, + default_rule_group_name + ) + url = "{0}{1}?api-version={2}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + default_rule_group_id, + RULES_API + ) + put_rules(cmd, default_rule_group_id, default_rule_group_name, mac_region, azure_monitor_workspace_resource_id, cluster_name, default_rules_template, url, True, 1) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/delete.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/delete.py new file mode 100644 index 00000000000..ac945fbaed8 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/recordingrules/delete.py @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from ..constants import RULES_API + + +def delete_rule(cmd, cluster_subscription, cluster_resource_group_name, default_rule_group_name): + from azure.cli.core.util import send_raw_request + default_rule_group_id = \ + "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.AlertsManagement/prometheusRuleGroups/{2}".format( + cluster_subscription, + cluster_resource_group_name, + default_rule_group_name + ) + headers = ['User-Agent=arc-azuremonitormetrics.delete_rule.' + default_rule_group_name] + url = "{0}{1}?api-version={2}".format( + cmd.cli_ctx.cloud.endpoints.resource_manager, + default_rule_group_id, + RULES_API + ) + send_raw_request(cmd.cli_ctx, "DELETE", url, headers=headers) + + +def delete_rules(cmd, cluster_subscription, cluster_resource_group_name, cluster_name): + delete_rule( + cmd, + cluster_subscription, + cluster_resource_group_name, + "NodeRecordingRulesRuleGroup-{0}".format(cluster_name) + ) + delete_rule( + cmd, + cluster_subscription, + cluster_resource_group_name, + "KubernetesRecordingRulesRuleGroup-{0}".format(cluster_name) + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/responseparsers/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/responseparsers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/responseparsers/amwlocationresponseparser.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/responseparsers/amwlocationresponseparser.py new file mode 100644 index 00000000000..ab0fbe9df8e --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/azuremonitormetrics/responseparsers/amwlocationresponseparser.py @@ -0,0 +1,29 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from typing import List + + +def parseResourceProviderResponseForLocations(resourceProviderResponse): + supportedLocationMap = {} + if not resourceProviderResponse.get('resourceTypes'): + return supportedLocationMap + resourceTypesRawArr = resourceProviderResponse['resourceTypes'] + for resourceTypeResponse in resourceTypesRawArr: + if resourceTypeResponse['resourceType'] == 'accounts': + supportedLocationMap = parseLocations(resourceTypeResponse['locations']) + return supportedLocationMap + + +def parseLocations(locations: List[str]) -> List[str]: + if not locations or len(locations) == 0: + return [] + return [reduceLocation(location) for location in locations] + + +def reduceLocation(location: str) -> str: + if not location: + return location + location = location.replace(' ', '').lower() + return location diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 0fcc313500e..f129b4e4aae 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -33,7 +33,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.4.0" +VERSION = "1.4.1" with open("README.rst", "r", encoding="utf-8") as f: README = f.read()