diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py index 0c0184a1966..98fcae8db44 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -15,6 +15,10 @@ WINDOWS_OS_NAME = "windows" STATIC_RUNTIME_NAME = "static" # not an official supported runtime but used for CLI logic LINUX_SKU_DEFAULT = "P1V2" +ISOLATED_V4_SKUS = [ + 'I1V4', 'I2V4', 'I3V4', 'I4V4', 'I5V4', 'I6V4', + 'I1MV4', 'I2MV4', 'I3MV4', 'I4MV4', 'I5MV4' +] FUNCTIONS_VERSIONS = ['4'] LOGICAPPS_NODE_RUNTIME_VERSIONS = ['~14', '~16', '~18'] FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX = r"^.*\|(.*)$" diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 775fe497149..cdbc5d9f29e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -19,7 +19,7 @@ from ._completers import get_hostname_completion_list from ._constants import (FUNCTIONS_VERSIONS, LOGICAPPS_NODE_RUNTIME_VERSIONS, WINDOWS_OS_NAME, LINUX_OS_NAME, - DEPLOYMENT_STORAGE_AUTH_TYPES, UPDATE_STRATEGY_TYPES) + DEPLOYMENT_STORAGE_AUTH_TYPES, UPDATE_STRATEGY_TYPES, ISOLATED_V4_SKUS) from ._validators import (validate_timeout_value, validate_site_create, validate_asp_create, validate_ase_create, validate_ip_address, @@ -56,9 +56,9 @@ def load_arguments(self, _): # PARAMETER REGISTRATION name_arg_type = CLIArgumentType(options_list=['--name', '-n'], metavar='NAME') sku_arg_type = CLIArgumentType( - help='The pricing tiers, e.g., F1(Free), D1(Shared), B1(Basic Small), B2(Basic Medium), B3(Basic Large), S1(Standard Small), P1V2(Premium V2 Small), P2V2(Premium V2 Medium), P3V2(Premium V2 Large), P0V3(Premium V3 Extra Small), P1V3(Premium V3 Small), P2V3(Premium V3 Medium), P3V3(Premium V3 Large), P1MV3(Premium Memory Optimized V3 Small), P2MV3(Premium Memory Optimized V3 Medium), P3MV3(Premium Memory Optimized V3 Large), P4MV3(Premium Memory Optimized V3 Extra Large), P5MV3(Premium Memory Optimized V3 Extra Extra Large), P0V4(Premium V4 Extra Small), P1V4(Premium V4 Small), P2V4(Premium V4 Medium), P3V4(Premium V4 Large), P1MV4(Premium Memory Optimized V4 Small), P2MV4(Premium Memory Optimized V4 Medium), P3MV4(Premium Memory Optimized V4 Large), P4MV4(Premium Memory Optimized V4 Extra Large), P5MV4(Premium Memory Optimized V4 Extra Extra Large), I1V2 (Isolated V2 I1V2), I2V2 (Isolated V2 I2V2), I3V2 (Isolated V2 I3V2), I4V2 (Isolated V2 I4V2), I5V2 (Isolated V2 I5V2), I6V2 (Isolated V2 I6V2), I1MV2 (Isolated Memory Optimized V2 I1MV2), I2MV2 (Isolated Memory Optimized V2 I2MV2), I3MV2 (Isolated Memory Optimized V2 I3MV2), I4MV2 (Isolated Memory Optimized V2 I4MV2), I5MV2 (Isolated Memory Optimized V2 I5MV2), WS1 (Logic Apps Workflow Standard 1), WS2 (Logic Apps Workflow Standard 2), WS3 (Logic Apps Workflow Standard 3)', + help='The pricing tiers, e.g., F1(Free), D1(Shared), B1(Basic Small), B2(Basic Medium), B3(Basic Large), S1(Standard Small), P1V2(Premium V2 Small), P2V2(Premium V2 Medium), P3V2(Premium V2 Large), P0V3(Premium V3 Extra Small), P1V3(Premium V3 Small), P2V3(Premium V3 Medium), P3V3(Premium V3 Large), P1MV3(Premium Memory Optimized V3 Small), P2MV3(Premium Memory Optimized V3 Medium), P3MV3(Premium Memory Optimized V3 Large), P4MV3(Premium Memory Optimized V3 Extra Large), P5MV3(Premium Memory Optimized V3 Extra Extra Large), P0V4(Premium V4 Extra Small), P1V4(Premium V4 Small), P2V4(Premium V4 Medium), P3V4(Premium V4 Large), P1MV4(Premium Memory Optimized V4 Small), P2MV4(Premium Memory Optimized V4 Medium), P3MV4(Premium Memory Optimized V4 Large), P4MV4(Premium Memory Optimized V4 Extra Large), P5MV4(Premium Memory Optimized V4 Extra Extra Large), I1V2 (Isolated V2 I1V2), I2V2 (Isolated V2 I2V2), I3V2 (Isolated V2 I3V2), I4V2 (Isolated V2 I4V2), I5V2 (Isolated V2 I5V2), I6V2 (Isolated V2 I6V2), I1MV2 (Isolated Memory Optimized V2 I1MV2), I2MV2 (Isolated Memory Optimized V2 I2MV2), I3MV2 (Isolated Memory Optimized V2 I3MV2), I4MV2 (Isolated Memory Optimized V2 I4MV2), I5MV2 (Isolated Memory Optimized V2 I5MV2), I1V4-I6V4 (Isolated V4), I1MV4-I5MV4 (Isolated Memory Optimized V4), WS1 (Logic Apps Workflow Standard 1), WS2 (Logic Apps Workflow Standard 2), WS3 (Logic Apps Workflow Standard 3)', arg_type=get_enum_type( - ['F1', 'FREE', 'D1', 'SHARED', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1V2', 'P2V2', 'P3V2', 'P0V3', 'P1V3', 'P2V3', 'P3V3', 'P1MV3', 'P2MV3', 'P3MV3', 'P4MV3', 'P5MV3', 'P0V4', 'P1V4', 'P2V4', 'P3V4', 'P1MV4', 'P2MV4', 'P3MV4', 'P4MV4', 'P5MV4', 'I1V2', 'I2V2', 'I3V2', 'I4V2', 'I5V2', 'I6V2', 'I1MV2', 'I2MV2', 'I3MV2', 'I4MV2', 'I5MV2', 'WS1', 'WS2', 'WS3'])) + ['F1', 'FREE', 'D1', 'SHARED', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1V2', 'P2V2', 'P3V2', 'P0V3', 'P1V3', 'P2V3', 'P3V3', 'P1MV3', 'P2MV3', 'P3MV3', 'P4MV3', 'P5MV3', 'P0V4', 'P1V4', 'P2V4', 'P3V4', 'P1MV4', 'P2MV4', 'P3MV4', 'P4MV4', 'P5MV4', 'I1V2', 'I2V2', 'I3V2', 'I4V2', 'I5V2', 'I6V2', 'I1MV2', 'I2MV2', 'I3MV2', 'I4MV2', 'I5MV2', 'WS1', 'WS2', 'WS3'] + ISOLATED_V4_SKUS)) webapp_name_arg_type = CLIArgumentType(configured_default='web', options_list=['--name', '-n'], metavar='NAME', completer=get_resource_name_completion_list('Microsoft.Web/sites'), id_part='name', @@ -1057,7 +1057,7 @@ def load_arguments(self, _): help="Configure default logging required to enable viewing log stream immediately after launching the webapp", default=False, action='store_true') c.argument('html', help="Ignore app detection and deploy as an html app", default=False, action='store_true') - c.argument('app_service_environment', options_list=['--app-service-environment', '-e'], help='name or resource ID of the (pre-existing) App Service Environment to deploy to. Requires an Isolated V2 sku') + c.argument('app_service_environment', options_list=['--app-service-environment', '-e'], help='name or resource ID of the (pre-existing) App Service Environment to deploy to. Requires an Isolated V2 or Isolated V4 sku') c.argument('basic_auth', help='Enable or disable basic auth.', arg_type=get_enum_type(BASIC_AUTH_TYPES)) c.argument('track_status', help="If true, web app startup status during deployment will be tracked for linux web apps.", arg_type=get_three_state_flag()) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py index 26d363d8425..ba623cbb0c0 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py @@ -22,7 +22,7 @@ from .aaz.latest.network import ListServiceTags from .aaz.latest.network.vnet import List as VNetList, Show as VNetShow -from ._constants import ACR_IMAGE_SUFFIX +from ._constants import ACR_IMAGE_SUFFIX, ISOLATED_V4_SKUS logger = get_logger(__name__) @@ -93,19 +93,20 @@ def validate_ase_create(cmd, namespace): def _validate_asp_sku(sku, app_service_environment, zone_redundant): - supported_skus = ['PREMIUMV2', 'PREMIUMV3', 'PREMIUMMV3', 'PREMIUM0V3', 'PREMIUMV4', 'PREMIUMMV4', 'ISOLATEDV2', 'ISOLATEDMV2', 'ELASTICPREMIUM'] # pylint: disable=line-too-long + supported_skus = ['PREMIUMV2', 'PREMIUMV3', 'PREMIUMMV3', 'PREMIUM0V3', 'PREMIUMV4', 'PREMIUMMV4', 'ISOLATEDV2', 'ISOLATEDMV2', 'ISOLATEDV4', 'ELASTICPREMIUM'] # pylint: disable=line-too-long if zone_redundant and get_sku_tier(sku).upper() not in supported_skus: raise ValidationError("Zone redundancy cannot be enabled for sku {}".format(sku)) # Isolated SKU is supported only for ASE - if sku.upper() in ['I1V2', 'I2V2', 'I3V2', 'I4V2', 'I5V2', 'I6V2', 'I1MV2', 'I2MV2', 'I3MV2', 'I4MV2', 'I5MV2']: + if sku.upper() in ['I1V2', 'I2V2', 'I3V2', 'I4V2', 'I5V2', 'I6V2', + 'I1MV2', 'I2MV2', 'I3MV2', 'I4MV2', 'I5MV2'] + ISOLATED_V4_SKUS: if not app_service_environment: raise ValidationError("The pricing tier 'Isolated' is not allowed for this app service plan. " "Use this link to learn more: " "https://learn.microsoft.com/azure/app-service/overview-hosting-plans") else: if app_service_environment: - raise ValidationError("Only pricing tier 'IsolatedV2' and 'IsolatedMV2' is allowed in this " - "app service plan. Use this link to " + raise ValidationError("Only pricing tiers 'IsolatedV2', 'IsolatedMV2', and 'IsolatedV4' are allowed " + "in this app service plan. Use this link to " "learn more: https://learn.microsoft.com/azure/app-service/overview-hosting-plans") diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 0f247b322e2..597b9c7c3c8 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -98,7 +98,7 @@ RUNTIME_STATUS_TEXT_MAP, LANGUAGE_EOL_DEPRECATION_NOTICES, STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_ID) from ._github_oauth import (get_github_access_token, cache_github_token) -from ._validators import validate_and_convert_to_int, validate_range_of_int_flag +from ._validators import validate_and_convert_to_int, validate_range_of_int_flag, _validate_asp_sku from .aaz.latest.network.vnet import List as VNetList, Show as VNetShow from .aaz.latest.network.vnet.subnet import Show as SubnetShow, Update as SubnetUpdate @@ -5257,6 +5257,9 @@ def update_app_service_plan(cmd, instance, sku=None, number_of_workers=None, ela sku_def = instance.sku if sku is not None: sku = _normalize_sku(sku) + _validate_asp_sku(sku, + getattr(instance, 'hosting_environment_profile', None), + getattr(instance, 'zone_redundant', False)) sku_def.tier = get_sku_tier(sku) sku_def.name = sku diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py index 1f0bfd960fc..b1ad47f6eb8 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py @@ -6,14 +6,36 @@ import unittest from unittest import mock -from azure.cli.core.azclierror import ArgumentUsageError +from azure.cli.core.azclierror import ArgumentUsageError, ValidationError +from azure.cli.command_modules.appservice._constants import ISOLATED_V4_SKUS from azure.cli.command_modules.appservice._validators import ( _normalize_http_headers, _normalize_ip_address_list, + _validate_asp_sku, _validate_ip_address_existence, _validate_service_tag_existence, ) +from azure.cli.command_modules.appservice.utils import get_sku_tier + + +class ValidateAppServicePlanSkuTest(unittest.TestCase): + def test_isolated_v4_skus_are_supported_for_ase_plans(self): + for sku in ISOLATED_V4_SKUS: + with self.subTest(sku=sku): + self.assertEqual(get_sku_tier(sku), 'IsolatedV4') + _validate_asp_sku(sku, app_service_environment='ase', zone_redundant=False) + + def test_isolated_v4_skus_support_zone_redundancy(self): + for sku in ISOLATED_V4_SKUS: + with self.subTest(sku=sku): + _validate_asp_sku(sku, app_service_environment='ase', zone_redundant=True) + + def test_isolated_v4_skus_require_ase(self): + for sku in ISOLATED_V4_SKUS: + with self.subTest(sku=sku): + with self.assertRaises(ValidationError): + _validate_asp_sku(sku, app_service_environment=None, zone_redundant=False) class _StubRule: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 697496f307c..5404d74cc9a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -16,7 +16,8 @@ MutuallyExclusiveArgumentError, ArgumentUsageError, AzureResponseError, - ResourceNotFoundError) + ResourceNotFoundError, + ValidationError) from azure.cli.command_modules.appservice.custom import (set_deployment_user, update_git_token, add_hostname, update_site_configs, @@ -1438,6 +1439,28 @@ def test_default_sku_is_p0v3_when_not_specified(self, mock_location, mock_client # The sku name should be normalized P0V3 self.assertIn('P0V3', str(call_kwargs)) + def test_update_to_isolated_v4_sku_requires_ase(self): + from azure.cli.command_modules.appservice.custom import update_app_service_plan + instance = mock.MagicMock() + instance.hosting_environment_profile = None + instance.zone_redundant = False + + with self.assertRaises(ValidationError): + update_app_service_plan(mock.MagicMock(), instance, sku='I1V4') + + @mock.patch('azure.cli.command_modules.appservice.custom._enable_managed_instance_properties') + def test_update_to_isolated_v4_sku_on_ase(self, _): + from azure.cli.command_modules.appservice.custom import update_app_service_plan + instance = mock.MagicMock() + instance.hosting_environment_profile = mock.MagicMock() + instance.zone_redundant = False + instance.sku.capacity = 1 + + result = update_app_service_plan(mock.MagicMock(), instance, sku='I1MV4') + + self.assertEqual(result.sku.name, 'I1MV4') + self.assertEqual(result.sku.tier, 'IsolatedV4') + class TestOneDeployScmCache(unittest.TestCase): """Tests for the per-invocation SCM URL / SCM headers cache on OneDeployParams. diff --git a/src/azure-cli/azure/cli/command_modules/appservice/utils.py b/src/azure-cli/azure/cli/command_modules/appservice/utils.py index 1d6e10431bc..8f22eb008ca 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -21,7 +21,7 @@ from azure.mgmt.core.tools import parse_resource_id, is_valid_resource_id, resource_id from ._client_factory import web_client_factory, providers_client_factory -from ._constants import LOGICAPP_KIND, FUNCTIONAPP_KIND, LINUXAPP_KIND +from ._constants import LOGICAPP_KIND, FUNCTIONAPP_KIND, LINUXAPP_KIND, ISOLATED_V4_SKUS logger = get_logger(__name__) @@ -113,6 +113,8 @@ def get_sku_tier(name): # pylint: disable=too-many-return-statements return 'IsolatedV2' if name in ['I1MV2', 'I2MV2', 'I3MV2', 'I4MV2', 'I5MV2']: return 'IsolatedMV2' + if name in ISOLATED_V4_SKUS: + return 'IsolatedV4' if name in ['WS1', 'WS2', 'WS3']: return 'WorkflowStandard' raise ValidationError("Invalid sku(pricing tier), please refer to command help for valid values")