From a54dce23d9877b067b7347bdbf3f522a831bd913 Mon Sep 17 00:00:00 2001 From: Khuram Khan <64149947+khkh-ms@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:18:02 -0500 Subject: [PATCH] [App Service] `az functionapp create`: Add `--zone-redundant` parameter to support zone redundant for Functions Flex SKU (#29984) --- .../cli/command_modules/appservice/_params.py | 6 +++ .../cli/command_modules/appservice/custom.py | 40 +++++++++++++++++-- .../tests/latest/test_functionapp_commands.py | 32 +++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) 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 d67e3f29112..523b78f39dc 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -194,6 +194,10 @@ def load_arguments(self, _): c.argument('location', arg_type=get_location_type(self.cli_ctx), help="limit the output to just the runtimes available in the specified location") c.argument('runtime', help="limit the output to just the specified runtime") + with self.argument_context('functionapp list-flexconsumption-locations') as c: + c.argument('zone_redundant', arg_type=get_three_state_flag(), + help='Filter the list to return only locations which support zone redundancy.', is_preview=True) + with self.argument_context('webapp deleted list') as c: c.argument('name', arg_type=webapp_name_arg_type, id_part=None) c.argument('slot', options_list=['--slot', '-s'], help='Name of the deleted web app slot.') @@ -822,6 +826,8 @@ def load_arguments(self, _): c.argument('deployment_storage_auth_value', options_list=['--deployment-storage-auth-value', '--dsav'], help="The deployment storage account authentication value. For the user-assigned managed identity authentication type, " "this should be the user assigned identity resource id. For the storage account connection string authentication type, this should be the name of the app setting that will contain the storage account connection " "string. For the system assigned managed-identity authentication type, this parameter is not applicable and should be left empty.", is_preview=True) + c.argument('zone_redundant', arg_type=get_three_state_flag(), + help='Enable zone redundancy for high availability. Applies to Flex Consumption SKU only.', is_preview=True) with self.argument_context('functionapp deployment config set') as c: c.argument('deployment_storage_name', options_list=['--deployment-storage-name', '--dsn'], help="The deployment storage account name.", is_preview=True) 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 990991355dc..39c310bafa3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -4581,7 +4581,7 @@ def get_app_insights_connection_string(cli_ctx, resource_group, name): return appinsights.connection_string -def create_flex_app_service_plan(cmd, resource_group_name, name, location): +def create_flex_app_service_plan(cmd, resource_group_name, name, location, zone_redundant): SkuDescription, AppServicePlan = cmd.get_models('SkuDescription', 'AppServicePlan') client = web_client_factory(cmd.cli_ctx) sku_def = SkuDescription(tier="FlexConsumption", name="FC1", size="FC", family="FC") @@ -4592,6 +4592,10 @@ def create_flex_app_service_plan(cmd, resource_group_name, name, location): kind="functionapp", name=name ) + + if zone_redundant: + _enable_zone_redundant(plan_def, sku_def, None) + poller = client.app_service_plans.begin_create_or_update(resource_group_name, name, plan_def) return LongRunningOperation(cmd.cli_ctx)(poller) @@ -4757,6 +4761,13 @@ def is_exactly_one_true(*args): return found +def list_flexconsumption_zone_redundant_locations(cmd): + client = web_client_factory(cmd.cli_ctx) + regions = client.list_geo_regions(sku="FlexConsumption") + regions = [x for x in regions if "FCZONEREDUNDANCY" in x.org_domain] + return [{'name': x.name.lower().replace(' ', '')} for x in regions] + + def create_functionapp(cmd, resource_group_name, name, storage_account, plan=None, os_type=None, functions_version=None, runtime=None, runtime_version=None, consumption_plan_location=None, app_insights=None, app_insights_key=None, @@ -4772,8 +4783,9 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non always_ready_instances=None, maximum_instance_count=None, instance_memory=None, flexconsumption_location=None, deployment_storage_name=None, deployment_storage_container_name=None, deployment_storage_auth_type=None, - deployment_storage_auth_value=None): + deployment_storage_auth_value=None, zone_redundant=False): # pylint: disable=too-many-statements, too-many-branches + if functions_version is None and flexconsumption_location is None: logger.warning("No functions version specified so defaulting to 4.") functions_version = '4' @@ -4812,6 +4824,12 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non SiteConfig, NameValuePair, DaprConfig, ResourceConfig = cmd.get_models('SiteConfig', 'NameValuePair', 'DaprConfig', 'ResourceConfig') + if flexconsumption_location is None: + if zone_redundant: + raise ArgumentUsageError( + '--zone-redundant is only valid for the Flex Consumption plan. ' + 'Please try again without the --zone-redundant parameter.') + if flexconsumption_location is not None: if image is not None: raise ArgumentUsageError( @@ -5138,6 +5156,17 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non create_app_insights = True if flexconsumption_location is not None: + if zone_redundant: + zone_redundant_locations = list_flexconsumption_zone_redundant_locations(cmd) + zone_redundant_location = next((loc for loc in zone_redundant_locations + if loc['name'].lower() == flexconsumption_location.lower()), None) + if zone_redundant_location is None: + raise ValidationError("The specified location '{0}' " + "doesn't support zone redundancy in Flex Consumption. " + "Use: az functionapp list-flexconsumption-locations --zone-redundant " + "for the list of locations that support zone redundancy." + .format(flexconsumption_location)) + site_config.net_framework_version = None functionapp_def.reserved = None functionapp_def.is_xenon = None @@ -5145,7 +5174,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non try: plan_name = generatePlanName(resource_group_name) plan_info = create_flex_app_service_plan( - cmd, resource_group_name, plan_name, flexconsumption_location) + cmd, resource_group_name, plan_name, flexconsumption_location, zone_redundant) functionapp_def.server_farm_id = plan_info.id functionapp_def.location = flexconsumption_location @@ -5690,7 +5719,10 @@ def list_consumption_locations(cmd): return [{'name': x.name.lower().replace(' ', '')} for x in regions] -def list_flexconsumption_locations(cmd): +def list_flexconsumption_locations(cmd, zone_redundant=False): + if zone_redundant: + return list_flexconsumption_zone_redundant_locations(cmd) + from azure.cli.core.commands.client_factory import get_subscription_id sub_id = get_subscription_id(cmd.cli_ctx) geo_regions_api = 'subscriptions/{}/providers/Microsoft.Web/geoRegions?sku=FlexConsumption&api-version=2023-01-01' diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py index 0847ce493c7..6dc81edcbd1 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py @@ -779,10 +779,42 @@ def test_functionapp_list_flexconsumption_locations(self): locations = self.cmd('functionapp list-flexconsumption-locations').get_output_in_json() self.assertTrue(len(locations) == 13) + def test_functionapp_list_flexconsumption_locations_zone_redundant(self): + locations = self.cmd('functionapp list-flexconsumption-locations --zone-redundant').get_output_in_json() + self.assertTrue(len(locations) > 0) + def test_functionapp_list_flexconsumption_runtimes(self): runtimes = self.cmd('functionapp list-flexconsumption-runtimes -l eastasia --runtime python').get_output_in_json() self.assertTrue(len(runtimes) == 2) + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) + @StorageAccountPreparer() + def test_functionapp_flex_zone_redundant_active(self, resource_group, storage_account): + functionapp_name = self.create_random_name( + 'functionapp', 40) + + functionapp = self.cmd('functionapp create -g {} -n {} -f {} -s {} --runtime python --runtime-version 3.11 --zone-redundant' + .format(resource_group, functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)).get_output_in_json() + + server_farm_id =functionapp['properties']['serverFarmId'] + function_plan = self.cmd('az functionapp plan show --ids {}' + .format(server_farm_id)).get_output_in_json() + self.assertTrue(function_plan['zoneRedundant'] == True) + + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) + @StorageAccountPreparer() + def test_functionapp_flex_zone_redundant_not_active(self, resource_group, storage_account): + functionapp_name = self.create_random_name( + 'functionapp', 40) + + functionapp = self.cmd('functionapp create -g {} -n {} -f {} -s {} --runtime python --runtime-version 3.11' + .format(resource_group, functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)).get_output_in_json() + + server_farm_id =functionapp['properties']['serverFarmId'] + function_plan = self.cmd('az functionapp plan show --ids {}' + .format(server_farm_id)).get_output_in_json() + self.assertTrue(function_plan['zoneRedundant'] == False) + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) @StorageAccountPreparer() def test_functionapp_flex_scale_config(self, resource_group, storage_account):