Skip to content

Commit 6c1b085

Browse files
authored
[App Service] az appservice plan update/az webapp update : Add elastic scale support (#20748)
* WIP * start elastic scale implementation * add elastic scale params to 'az appservice plan update' and add tests; start webapp elastic scale impl * Change allowed SKUs for elastic scale * fix webapp minimum_elastic_instance_count bug * add/record tests and fix ASP update setting worker count to 1 when not specified * rerecord old tests * fix style and linter issues * rerecord test_webapp_e2e test * make elastic scale parameters preview * update recordings * fix style
1 parent 80cf4d4 commit 6c1b085

33 files changed

Lines changed: 7454 additions & 3972 deletions

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ def load_arguments(self, _):
106106
completer=get_resource_name_completion_list('Microsoft.Web/serverFarms'),
107107
configured_default='appserviceplan', id_part='name',
108108
local_context_attribute=LocalContextAttribute(name='plan_name', actions=[LocalContextAction.GET]))
109-
c.argument('number_of_workers', help='Number of workers to be allocated.', type=int, default=1)
110109
c.argument('admin_site_name', help='The name of the admin web app.',
111110
deprecate_info=c.deprecate(expiration='0.2.17'))
112111
c.ignore('max_burst')
@@ -116,6 +115,7 @@ def load_arguments(self, _):
116115
validator=validate_asp_create,
117116
local_context_attribute=LocalContextAttribute(name='plan_name', actions=[LocalContextAction.SET],
118117
scopes=['appservice', 'webapp', 'functionapp']))
118+
c.argument('number_of_workers', help='Number of workers to be allocated.', type=int, default=1)
119119
c.argument('app_service_environment', options_list=['--app-service-environment', '-e'],
120120
help="Name or ID of the app service environment",
121121
local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.GET]))
@@ -131,6 +131,9 @@ def load_arguments(self, _):
131131

132132
with self.argument_context('appservice plan update') as c:
133133
c.argument('sku', arg_type=sku_arg_type)
134+
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)')
135+
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.')
136+
c.argument('number_of_workers', type=int, help='Number of workers to be allocated.')
134137
c.ignore('allow_pending_state')
135138

136139
with self.argument_context('appservice plan delete') as c:
@@ -198,6 +201,8 @@ def load_arguments(self, _):
198201
arg_type=get_three_state_flag(return_label=True), deprecate_info=c.deprecate(expiration='3.0.0'))
199202
c.argument('skip_dns_registration', help="If true web app hostname is not registered with DNS on creation",
200203
arg_type=get_three_state_flag(return_label=True), deprecate_info=c.deprecate(expiration='3.0.0'))
204+
c.argument('minimum_elastic_instance_count', options_list=["--minimum-elastic-instance-count", "-i"], type=int, is_preview=True, help="Minimum number of instances. App must be in an elastic scale App Service Plan.")
205+
c.argument('prewarmed_instance_count', options_list=["--prewarmed-instance-count", "-w"], type=int, is_preview=True, help="Number of preWarmed instances. App must be in an elastic scale App Service Plan.")
201206

202207
with self.argument_context('webapp browse') as c:
203208
c.argument('logs', options_list=['--logs', '-l'], action='store_true',

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55

66
import ipaddress
77

8-
from azure.cli.core.azclierror import (InvalidArgumentValueError, ArgumentUsageError, ValidationError,
9-
MutuallyExclusiveArgumentError)
8+
9+
from azure.cli.core.azclierror import (InvalidArgumentValueError, ArgumentUsageError, RequiredArgumentMissingError,
10+
ResourceNotFoundError, ValidationError, MutuallyExclusiveArgumentError)
1011
from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id
1112
from azure.cli.core.profiles import ResourceType
1213
from azure.cli.core.commands.validators import validate_tags
1314

1415
from knack.log import get_logger
15-
from knack.util import CLIError
1616
from msrestazure.tools import is_valid_resource_id, parse_resource_id
1717

1818
from ._appservice_utils import _generic_site_operation
1919
from ._client_factory import web_client_factory
20-
from .utils import _normalize_sku, get_sku_name, _normalize_location
20+
from .utils import _normalize_sku, get_sku_tier, _normalize_location
2121

2222
logger = get_logger(__name__)
2323

@@ -59,7 +59,8 @@ def validate_site_create(cmd, namespace):
5959
else:
6060
plan_info = client.app_service_plans.get(resource_group_name, plan)
6161
if not plan_info:
62-
raise CLIError("The plan '{}' doesn't exist in the resource group '{}'".format(plan, resource_group_name))
62+
raise ResourceNotFoundError("The plan '{}' doesn't exist in the resource group '{}'".format(
63+
plan, resource_group_name))
6364
# verify that the name is available for create
6465
validation_payload = {
6566
"name": namespace.name,
@@ -71,7 +72,7 @@ def validate_site_create(cmd, namespace):
7172
}
7273
validation = client.validate(resource_group_name, validation_payload)
7374
if validation.status.lower() == "failure" and validation.error.code != 'SiteAlreadyExists':
74-
raise CLIError(validation.error.message)
75+
raise ValidationError(validation.error.message)
7576

7677

7778
def validate_ase_create(cmd, namespace):
@@ -81,11 +82,11 @@ def validate_ase_create(cmd, namespace):
8182
if isinstance(namespace.name, str):
8283
name_validation = client.check_name_availability(namespace.name, resource_type)
8384
if not name_validation.name_available:
84-
raise CLIError(name_validation.message)
85+
raise ValidationError(name_validation.message)
8586

8687

8788
def _validate_asp_sku(sku, app_service_environment, zone_redundant):
88-
if zone_redundant and get_sku_name(sku.upper()) not in ['PREMIUMV2', 'PREMIUMV3']:
89+
if zone_redundant and get_sku_tier(sku.upper()) not in ['PREMIUMV2', 'PREMIUMV3']:
8990
raise ValidationError("Zone redundancy cannot be enabled for sku {}".format(sku))
9091
# Isolated SKU is supported only for ASE
9192
if sku.upper() in ['I1', 'I2', 'I3', 'I1V2', 'I2V2', 'I3V2']:
@@ -110,7 +111,7 @@ def validate_asp_create(namespace):
110111
def validate_functionapp_asp_create(namespace):
111112
validate_tags(namespace)
112113
sku = _normalize_sku(namespace.sku)
113-
tier = get_sku_name(sku)
114+
tier = get_sku_tier(sku)
114115
_validate_asp_sku(sku=sku, app_service_environment=None, zone_redundant=namespace.zone_redundant)
115116
if namespace.max_burst is not None:
116117
if tier.lower() != "elasticpremium":
@@ -131,7 +132,7 @@ def validate_app_or_slot_exists_in_rg(cmd, namespace):
131132
else:
132133
app = client.web_apps.get(resource_group_name, webapp, None, raw=True)
133134
if app.response.status_code != 200:
134-
raise CLIError(app.response.text)
135+
raise ResourceNotFoundError(app.response.text)
135136

136137

137138
def validate_app_exists_in_rg(cmd, namespace):
@@ -140,11 +141,11 @@ def validate_app_exists_in_rg(cmd, namespace):
140141
resource_group_name = namespace.resource_group_name
141142
app = client.web_apps.get(resource_group_name, webapp, None, raw=True)
142143
if app.response.status_code != 200:
143-
raise CLIError(app.response.text)
144+
raise ResourceNotFoundError(app.response.text)
144145

145146

146147
def validate_add_vnet(cmd, namespace):
147-
from azure.core.exceptions import ResourceNotFoundError
148+
from azure.core.exceptions import ResourceNotFoundError as ResNotFoundError
148149

149150
resource_group_name = namespace.resource_group_name
150151
from azure.cli.command_modules.network._client_factory import network_client_factory
@@ -172,7 +173,7 @@ def validate_add_vnet(cmd, namespace):
172173
try:
173174
vnet_loc = vnet_client.virtual_networks.get(resource_group_name=namespace.resource_group_name,
174175
virtual_network_name=vnet_identifier).location
175-
except ResourceNotFoundError:
176+
except ResNotFoundError:
176177
vnets = vnet_client.virtual_networks.list_all()
177178
vnet_loc = ''
178179
for v in vnets:
@@ -202,7 +203,7 @@ def validate_front_end_scale_factor(namespace):
202203
scale_error_text = "Frontend Scale Factor '{}' is invalid. Must be between {} and {}"
203204
scale_factor = namespace.front_end_scale_factor
204205
if scale_factor < min_scale_factor or scale_factor > max_scale_factor:
205-
raise CLIError(scale_error_text.format(scale_factor, min_scale_factor, max_scale_factor))
206+
raise ValidationError(scale_error_text.format(scale_factor, min_scale_factor, max_scale_factor))
206207

207208

208209
def validate_ip_address(cmd, namespace):
@@ -215,13 +216,13 @@ def validate_ip_address(cmd, namespace):
215216

216217
def validate_onedeploy_params(namespace):
217218
if namespace.src_path and namespace.src_url:
218-
raise CLIError('Only one of --src-path and --src-url can be specified')
219+
raise MutuallyExclusiveArgumentError('Only one of --src-path and --src-url can be specified')
219220

220221
if not namespace.src_path and not namespace.src_url:
221-
raise CLIError('Either of --src-path or --src-url must be specified')
222+
raise RequiredArgumentMissingError('Either of --src-path or --src-url must be specified')
222223

223224
if namespace.src_url and not namespace.artifact_type:
224-
raise CLIError('Deployment type is mandatory when deploying from URLs. Use --type')
225+
raise RequiredArgumentMissingError('Deployment type is mandatory when deploying from URLs. Use --type')
225226

226227

227228
def _validate_ip_address_format(namespace):
@@ -304,7 +305,7 @@ def _validate_service_tag_existence(cmd, namespace):
304305
def validate_public_cloud(cmd):
305306
from azure.cli.core.cloud import AZURE_PUBLIC_CLOUD
306307
if cmd.cli_ctx.cloud.name != AZURE_PUBLIC_CLOUD.name:
307-
raise CLIError('This command is not yet supported on soveriegn clouds.')
308+
raise ValidationError('This command is not yet supported on soveriegn clouds.')
308309

309310

310311
def validate_staticsite_sku(cmd, namespace):
@@ -352,7 +353,7 @@ def validate_vnet_integration(cmd, namespace):
352353

353354
sku_name = plan_info.sku.name
354355
disallowed_skus = {'FREE', 'SHARED', 'BASIC', 'ElasticPremium', 'PremiumContainer', 'Isolated', 'IsolatedV2'}
355-
if get_sku_name(sku_name) in disallowed_skus:
356+
if get_sku_tier(sku_name) in disallowed_skus:
356357
raise ArgumentUsageError("App Service Plan has invalid sku for vnet integration: {}."
357358
"Plan sku cannot be one of: {}. "
358359
"Please run 'az appservice plan create -h' "

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ def load_command_table(self, _):
133133
g.custom_command('identity remove', 'remove_identity')
134134
g.custom_command('create-remote-connection', 'create_tunnel', exception_handler=ex_handler_factory())
135135
g.custom_command('deploy', 'perform_onedeploy', validator=validate_onedeploy_params, is_preview=True)
136-
g.generic_update_command('update', getter_name='get_webapp', setter_name='set_webapp', custom_func_name='update_webapp', command_type=appservice_custom)
136+
g.generic_update_command('update', getter_name='get_webapp', setter_name='set_webapp',
137+
custom_func_name='update_webapp', command_type=appservice_custom)
137138

138139
with self.command_group('webapp traffic-routing') as g:
139140
g.custom_command('set', 'set_traffic_routing')

0 commit comments

Comments
 (0)