|
| 1 | +# -------------------------------------------------------------------------------------------- |
| 2 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | +# Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | +# -------------------------------------------------------------------------------------------- |
| 5 | + |
| 6 | +# pylint: disable=unused-argument |
| 7 | +from knack.log import get_logger |
| 8 | +from azure.cli.core.commands.client_factory import get_subscription_id |
| 9 | +from azure.cli.core.azclierror import RequiredArgumentMissingError, InvalidArgumentValueError |
| 10 | + |
| 11 | +from .DefaultExtension import DefaultExtension |
| 12 | +from .._client_factory import cf_storage, cf_managed_clusters |
| 13 | +from ..vendored_sdks.models import (Extension, PatchExtension, Scope, ScopeCluster) |
| 14 | + |
| 15 | +logger = get_logger(__name__) |
| 16 | + |
| 17 | + |
| 18 | +class DataProtectionKubernetes(DefaultExtension): |
| 19 | + def __init__(self): |
| 20 | + """Constants for configuration settings |
| 21 | + - Tenant Id (required) |
| 22 | + - Backup storage location (required) |
| 23 | + - Resource Limits (optional) |
| 24 | + """ |
| 25 | + self.TENANT_ID = "credentials.tenantId" |
| 26 | + self.BACKUP_STORAGE_ACCOUNT_CONTAINER = "configuration.backupStorageLocation.bucket" |
| 27 | + self.BACKUP_STORAGE_ACCOUNT_NAME = "configuration.backupStorageLocation.config.storageAccount" |
| 28 | + self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP = "configuration.backupStorageLocation.config.resourceGroup" |
| 29 | + self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION = "configuration.backupStorageLocation.config.subscriptionId" |
| 30 | + self.RESOURCE_LIMIT_CPU = "resources.limits.cpu" |
| 31 | + self.RESOURCE_LIMIT_MEMORY = "resources.limits.memory" |
| 32 | + |
| 33 | + self.blob_container = "blobContainer" |
| 34 | + self.storage_account = "storageAccount" |
| 35 | + self.storage_account_resource_group = "storageAccountResourceGroup" |
| 36 | + self.storage_account_subsciption = "storageAccountSubscriptionId" |
| 37 | + self.cpu_limit = "cpuLimit" |
| 38 | + self.memory_limit = "memoryLimit" |
| 39 | + |
| 40 | + self.configuration_mapping = { |
| 41 | + self.blob_container.lower(): self.BACKUP_STORAGE_ACCOUNT_CONTAINER, |
| 42 | + self.storage_account.lower(): self.BACKUP_STORAGE_ACCOUNT_NAME, |
| 43 | + self.storage_account_resource_group.lower(): self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP, |
| 44 | + self.storage_account_subsciption.lower(): self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION, |
| 45 | + self.cpu_limit.lower(): self.RESOURCE_LIMIT_CPU, |
| 46 | + self.memory_limit.lower(): self.RESOURCE_LIMIT_MEMORY |
| 47 | + } |
| 48 | + |
| 49 | + self.bsl_configuration_settings = [ |
| 50 | + self.blob_container, |
| 51 | + self.storage_account, |
| 52 | + self.storage_account_resource_group, |
| 53 | + self.storage_account_subsciption |
| 54 | + ] |
| 55 | + |
| 56 | + def Create( |
| 57 | + self, |
| 58 | + cmd, |
| 59 | + client, |
| 60 | + resource_group_name, |
| 61 | + cluster_name, |
| 62 | + name, |
| 63 | + cluster_type, |
| 64 | + cluster_rp, |
| 65 | + extension_type, |
| 66 | + scope, |
| 67 | + auto_upgrade_minor_version, |
| 68 | + release_train, |
| 69 | + version, |
| 70 | + target_namespace, |
| 71 | + release_namespace, |
| 72 | + configuration_settings, |
| 73 | + configuration_protected_settings, |
| 74 | + configuration_settings_file, |
| 75 | + configuration_protected_settings_file |
| 76 | + ): |
| 77 | + # Current scope of DataProtection Kubernetes Backup extension is 'cluster' #TODO: add TSGs when they are in place |
| 78 | + if scope == 'namespace': |
| 79 | + raise InvalidArgumentValueError(f"Invalid scope '{scope}'. This extension can only be installed at 'cluster' scope.") |
| 80 | + |
| 81 | + scope_cluster = ScopeCluster(release_namespace=release_namespace) |
| 82 | + ext_scope = Scope(cluster=scope_cluster, namespace=None) |
| 83 | + |
| 84 | + if cluster_type.lower() != 'managedclusters': |
| 85 | + raise InvalidArgumentValueError(f"Invalid cluster type '{cluster_type}'. This extension can only be installed for managed clusters.") |
| 86 | + |
| 87 | + if release_namespace is not None: |
| 88 | + logger.warning(f"Ignoring 'release-namespace': {release_namespace}") |
| 89 | + |
| 90 | + tenant_id = self.__get_tenant_id(cmd.cli_ctx) |
| 91 | + if not tenant_id: |
| 92 | + raise SystemExit(logger.error("Unable to fetch TenantId. Please check your subscription or run 'az login' to login to Azure.")) |
| 93 | + |
| 94 | + self.__validate_and_map_config(configuration_settings) |
| 95 | + self.__validate_backup_storage_account(cmd.cli_ctx, resource_group_name, cluster_name, configuration_settings) |
| 96 | + |
| 97 | + configuration_settings[self.TENANT_ID] = tenant_id |
| 98 | + |
| 99 | + if release_train is None: |
| 100 | + release_train = 'stable' |
| 101 | + |
| 102 | + create_identity = True |
| 103 | + extension = Extension( |
| 104 | + extension_type=extension_type, |
| 105 | + auto_upgrade_minor_version=True, |
| 106 | + release_train=release_train, |
| 107 | + scope=ext_scope, |
| 108 | + configuration_settings=configuration_settings |
| 109 | + ) |
| 110 | + return extension, name, create_identity |
| 111 | + |
| 112 | + def Update( |
| 113 | + self, |
| 114 | + cmd, |
| 115 | + resource_group_name, |
| 116 | + cluster_name, |
| 117 | + auto_upgrade_minor_version, |
| 118 | + release_train, |
| 119 | + version, |
| 120 | + configuration_settings, |
| 121 | + configuration_protected_settings, |
| 122 | + original_extension, |
| 123 | + yes=False, |
| 124 | + ): |
| 125 | + if configuration_settings is None: |
| 126 | + configuration_settings = {} |
| 127 | + |
| 128 | + if len(configuration_settings) > 0: |
| 129 | + bsl_specified = self.__is_bsl_specified(configuration_settings) |
| 130 | + self.__validate_and_map_config(configuration_settings, validate_bsl=bsl_specified) |
| 131 | + if bsl_specified: |
| 132 | + self.__validate_backup_storage_account(cmd.cli_ctx, resource_group_name, cluster_name, configuration_settings) |
| 133 | + |
| 134 | + return PatchExtension( |
| 135 | + auto_upgrade_minor_version=True, |
| 136 | + release_train=release_train, |
| 137 | + configuration_settings=configuration_settings, |
| 138 | + ) |
| 139 | + |
| 140 | + def __get_tenant_id(self, cli_ctx): |
| 141 | + from azure.cli.core._profile import Profile |
| 142 | + if not cli_ctx.data.get('tenant_id'): |
| 143 | + cli_ctx.data['tenant_id'] = Profile(cli_ctx=cli_ctx).get_subscription()['tenantId'] |
| 144 | + return cli_ctx.data['tenant_id'] |
| 145 | + |
| 146 | + def __validate_and_map_config(self, configuration_settings, validate_bsl=True): |
| 147 | + """Validate and set configuration settings for Data Protection K8sBackup extension""" |
| 148 | + input_configuration_settings = dict(configuration_settings.items()) |
| 149 | + input_configuration_keys = [key.lower() for key in configuration_settings] |
| 150 | + |
| 151 | + if validate_bsl: |
| 152 | + for key in self.bsl_configuration_settings: |
| 153 | + if key.lower() not in input_configuration_keys: |
| 154 | + raise RequiredArgumentMissingError(f"Missing required configuration setting: {key}") |
| 155 | + |
| 156 | + for key in input_configuration_settings: |
| 157 | + _key = key.lower() |
| 158 | + if _key in self.configuration_mapping: |
| 159 | + configuration_settings[self.configuration_mapping[_key]] = configuration_settings.pop(key) |
| 160 | + else: |
| 161 | + configuration_settings.pop(key) |
| 162 | + logger.warning(f"Ignoring unrecognized configuration setting: {key}") |
| 163 | + |
| 164 | + def __validate_backup_storage_account(self, cli_ctx, resource_group_name, cluster_name, configuration_settings): |
| 165 | + """Validations performed on the backup storage account |
| 166 | + - Existance of the storage account |
| 167 | + - Cluster and storage account are in the same location |
| 168 | + """ |
| 169 | + sa_subscription_id = configuration_settings[self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION] |
| 170 | + storage_account_client = cf_storage(cli_ctx, sa_subscription_id).storage_accounts |
| 171 | + |
| 172 | + storage_account = storage_account_client.get_properties( |
| 173 | + configuration_settings[self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP], |
| 174 | + configuration_settings[self.BACKUP_STORAGE_ACCOUNT_NAME]) |
| 175 | + |
| 176 | + cluster_subscription_id = get_subscription_id(cli_ctx) |
| 177 | + managed_clusters_client = cf_managed_clusters(cli_ctx, cluster_subscription_id) |
| 178 | + managed_cluster = managed_clusters_client.get( |
| 179 | + resource_group_name, |
| 180 | + cluster_name) |
| 181 | + |
| 182 | + if managed_cluster.location != storage_account.location: |
| 183 | + error_message = f"The Kubernetes managed cluster '{cluster_name} ({managed_cluster.location})' and the backup storage account '{configuration_settings[self.BACKUP_STORAGE_ACCOUNT_NAME]} ({storage_account.location})' are not in the same location. Please make sure that the cluster and the storage account are in the same location." |
| 184 | + raise SystemExit(logger.error(error_message)) |
| 185 | + |
| 186 | + def __is_bsl_specified(self, configuration_settings): |
| 187 | + """Check if the backup storage account is specified in the input""" |
| 188 | + input_configuration_keys = [key.lower() for key in configuration_settings] |
| 189 | + for key in self.bsl_configuration_settings: |
| 190 | + if key.lower() in input_configuration_keys: |
| 191 | + return True |
| 192 | + return False |
0 commit comments