Skip to content

Commit 754ef33

Browse files
authored
[App Service] az appservice plan create/update: Add async scaling parameter --async-scaling-enabled (#32123)
1 parent 704326a commit 754ef33

File tree

6 files changed

+3942
-9
lines changed

6 files changed

+3942
-9
lines changed

src/azure-cli/azure/cli/command_modules/appservice/_params.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,15 @@ def load_arguments(self, _):
126126
'the App Service plan that hosts it.')
127127
c.argument('zone_redundant', options_list=['--zone-redundant', '-z'], help='Enable zone redundancy for high availability. Minimum instance count is 2.')
128128
c.argument('tags', arg_type=tags_type)
129+
c.argument('async_scaling_enabled', arg_type=get_three_state_flag(), help='Enables async scaling for the app service plan. Set to "true" to create an async operation if there are insufficient workers to scale synchronously. The SKU must be Dedicated.')
129130

130131
with self.argument_context('appservice plan update') as c:
131132
c.argument('sku', arg_type=sku_arg_type)
132133
c.argument('elastic_scale', arg_type=get_three_state_flag(), is_preview=True, help='Enable or disable automatic scaling. Set to "true" to enable elastic scale for this plan, or "false" to disable elastic scale for this plan. The SKU must be a Premium V2 SKU (P1V2, P2V2, P3V2) or a Premium V3 SKU (P1V3, P2V3, P3V3)')
133134
c.argument('max_elastic_worker_count', options_list=['--max-elastic-worker-count', '-m'], type=int, is_preview=True, help='Maximum number of instances that the plan can scale out to. The plan must be an elastic scale plan.')
134135
c.argument('number_of_workers', type=int, help='Number of workers to be allocated.')
135136
c.ignore('allow_pending_state')
137+
c.argument('async_scaling_enabled', arg_type=get_three_state_flag(), help='Enables async scaling for the app service plan. Set to "true" to create an async operation if there are insufficient workers to scale synchronously. The SKU must be Dedicated.')
136138

137139
with self.argument_context('appservice plan delete') as c:
138140
c.argument('name', arg_type=name_arg_type, help='The name of the app service plan',

src/azure-cli/azure/cli/command_modules/appservice/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ def load_command_table(self, _):
314314
g.command('delete', 'delete', confirmation=True)
315315
g.custom_command('list', 'list_app_service_plans')
316316
g.custom_show_command('show', 'show_plan')
317-
g.generic_update_command('update', setter_name='begin_create_or_update', custom_func_name='update_app_service_plan',
318-
setter_arg_name='app_service_plan', supports_no_wait=True,
317+
g.generic_update_command('update', setter_name='update_app_service_plan_with_progress', custom_func_name='update_app_service_plan',
318+
setter_arg_name='app_service_plan', setter_type=appservice_custom, supports_no_wait=True,
319319
exception_handler=ex_handler_factory())
320320

321321
with self.command_group('appservice') as g:

src/azure-cli/azure/cli/command_modules/appservice/custom.py

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
from azure.cli.core.commands.client_factory import get_mgmt_service_client
4343
from azure.cli.core.commands import LongRunningOperation
44+
from azure.cli.core.commands.progress import IndeterminateProgressBar
4445
from azure.cli.core.util import shell_safe_json_parse, open_page_in_browser, get_json_object, \
4546
ConfiguredDefaultSetter, sdk_no_wait
4647
from azure.cli.core.util import get_az_user_agent, send_raw_request, get_file_json
@@ -3979,9 +3980,76 @@ def _enable_zone_redundant(plan_def, sku_def, number_of_workers):
39793980
sku_def.capacity = max(3, number_of_workers)
39803981

39813982

3983+
# Progress bar for serverfarm async scaling operations
3984+
class PlanProgressBar(IndeterminateProgressBar):
3985+
STATUS_CHECK_INTERVAL_SEC = 60
3986+
3987+
def __init__(self, cli_ctx, resource_group_name, plan_name):
3988+
self.client = web_client_factory(cli_ctx).app_service_plans
3989+
self.rg = resource_group_name
3990+
self.plan_name = plan_name
3991+
self._last_msg = None
3992+
self._last_status_check = None
3993+
super().__init__(cli_ctx)
3994+
3995+
def _emit(self, msg):
3996+
if msg != self._last_msg:
3997+
logger.warning(msg)
3998+
self._last_msg = msg
3999+
4000+
def begin(self):
4001+
self._emit(f"Starting to scale App Service plan {self.plan_name}...")
4002+
super().begin()
4003+
4004+
def update_progress_with_msg(self, message):
4005+
self._safe_update_progress_message(message)
4006+
super().update_progress_with_msg(message)
4007+
4008+
def end(self):
4009+
plan = self.client.get(self.rg, self.plan_name)
4010+
capacity = None
4011+
sku_name = None
4012+
4013+
if getattr(plan, 'sku', None):
4014+
capacity = getattr(plan.sku, 'capacity', None)
4015+
sku_name = getattr(plan.sku, 'name', None)
4016+
4017+
if capacity is not None and sku_name is not None:
4018+
self._emit(f"Successfully scaled to {capacity} workers in pricing tier {sku_name}.")
4019+
4020+
super().end()
4021+
4022+
def stop(self):
4023+
logger.error("Operation wait cancelled. The async scaling operation is still in progress. "
4024+
"Please update the plan to stop scaling.")
4025+
super().stop()
4026+
4027+
def _safe_update_progress_message(self, message):
4028+
# Only check real status periodically to avoid hammering API
4029+
now = time.monotonic()
4030+
if (self._last_status_check is not None and
4031+
now - self._last_status_check < PlanProgressBar.STATUS_CHECK_INTERVAL_SEC):
4032+
return
4033+
4034+
try:
4035+
plan = self.client.get(self.rg, self.plan_name)
4036+
capacity = None
4037+
skuName = None
4038+
if getattr(plan, 'sku', None):
4039+
capacity = getattr(plan.sku, 'capacity', None)
4040+
skuName = getattr(plan.sku, 'name', None)
4041+
4042+
status = message or "InProgress"
4043+
details = f"Status: {status} — Scaled to {capacity} workers of pricing tier {skuName}."
4044+
self._last_status_check = now
4045+
self._emit(details)
4046+
except Exception: # pylint: disable=broad-except
4047+
self._emit("Scaling in progress...")
4048+
4049+
39824050
def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, per_site_scaling=False,
39834051
app_service_environment=None, sku='B1', number_of_workers=None, location=None,
3984-
tags=None, no_wait=False, zone_redundant=False):
4052+
tags=None, no_wait=False, zone_redundant=False, async_scaling_enabled=None):
39854053
HostingEnvironmentProfile, SkuDescription, AppServicePlan = cmd.get_models(
39864054
'HostingEnvironmentProfile', 'SkuDescription', 'AppServicePlan')
39874055

@@ -4020,7 +4088,8 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p
40204088
sku_def = SkuDescription(tier=get_sku_tier(sku), name=_normalize_sku(sku), capacity=number_of_workers)
40214089
plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def,
40224090
reserved=(is_linux or None), hyper_v=(hyper_v or None),
4023-
per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def)
4091+
per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def,
4092+
async_scaling_enabled=async_scaling_enabled)
40244093

40254094
if sku.upper() in ['WS1', 'WS2', 'WS3']:
40264095
existing_plan = get_resource_if_exists(client.app_service_plans,
@@ -4033,16 +4102,49 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p
40334102
if zone_redundant:
40344103
_enable_zone_redundant(plan_def, sku_def, number_of_workers)
40354104

4036-
return sdk_no_wait(no_wait, client.app_service_plans.begin_create_or_update, name=name,
4037-
resource_group_name=resource_group_name, app_service_plan=plan_def)
4105+
if no_wait:
4106+
return sdk_no_wait(no_wait, client.app_service_plans.begin_create_or_update, name=name,
4107+
resource_group_name=resource_group_name, app_service_plan=plan_def)
4108+
4109+
poller = client.app_service_plans.begin_create_or_update(resource_group_name, name, plan_def)
4110+
4111+
# Only use progress bar for actual long-running operations (async scaling)
4112+
# Check if the operation is actually async by looking at the poller status
4113+
if poller.done():
4114+
# This completed synchronously (200 response), no need for progress bar
4115+
return poller.result()
4116+
4117+
# Asynchronous operation (202 response), use custom progress bar
4118+
progress_bar = PlanProgressBar(cmd.cli_ctx, resource_group_name, name)
4119+
return LongRunningOperation(cmd.cli_ctx, progress_bar=progress_bar)(poller)
4120+
4121+
4122+
def update_app_service_plan_with_progress(cmd, resource_group_name, name, app_service_plan):
4123+
client = web_client_factory(cmd.cli_ctx)
4124+
4125+
# For regular execution, apply conditional progress logic
4126+
poller = client.app_service_plans.begin_create_or_update(resource_group_name, name, app_service_plan)
4127+
4128+
if poller.done():
4129+
# Synchronous operation (200 response), return result directly
4130+
return poller.result()
4131+
4132+
# Asynchronous operation (202 response), use custom progress bar
4133+
progress_bar = PlanProgressBar(cmd.cli_ctx, resource_group_name, name)
4134+
return LongRunningOperation(cmd.cli_ctx, progress_bar=progress_bar)(poller)
40384135

40394136

40404137
def update_app_service_plan(cmd, instance, sku=None, number_of_workers=None, elastic_scale=None,
4041-
max_elastic_worker_count=None):
4042-
if number_of_workers is None and sku is None and elastic_scale is None and max_elastic_worker_count is None:
4138+
max_elastic_worker_count=None, async_scaling_enabled=None):
4139+
if (number_of_workers is None and sku is None and
4140+
elastic_scale is None and max_elastic_worker_count is None and async_scaling_enabled is None):
40434141
safe_params = cmd.cli_ctx.data['safe_params']
40444142
if '--set' not in safe_params:
4045-
args = ["--number-of-workers", "--sku", "--elastic-scale", "--max-elastic-worker-count"]
4143+
args = ["--number-of-workers",
4144+
"--sku",
4145+
"--elastic-scale",
4146+
"--max-elastic-worker-count",
4147+
"--async-scaling-enabled"]
40464148
logger.warning('Nothing to update. Set one of the following parameters to make an update: %s', str(args))
40474149
sku_def = instance.sku
40484150
if sku is not None:
@@ -4078,6 +4180,9 @@ def update_app_service_plan(cmd, instance, sku=None, number_of_workers=None, ela
40784180
use_additional_properties(instance)
40794181
instance.additional_properties["properties"]["maximumElasticWorkerCount"] = max_elastic_worker_count
40804182

4183+
if async_scaling_enabled is not None:
4184+
instance.async_scaling_enabled = async_scaling_enabled
4185+
40814186
instance.sku = sku_def
40824187
return instance
40834188

0 commit comments

Comments
 (0)