Skip to content

Commit 77f07ec

Browse files
authored
[App Service] az appservice ase create: Command changes for ASE v3 GA (#18748)
* ga changes - draft * exclude missing v3 apis * remove debug line * Add zone support * Add test for zone redundancy * Update help
1 parent 28e4d2d commit 77f07ec

File tree

4 files changed

+82
-77
lines changed

4 files changed

+82
-77
lines changed

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

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,7 +2175,7 @@
21752175

21762176
helps['appservice ase'] = """
21772177
type: group
2178-
short-summary: Manage App Service Environments v2
2178+
short-summary: Manage App Service Environments
21792179
"""
21802180

21812181
helps['appservice ase list'] = """
@@ -2198,7 +2198,7 @@
21982198

21992199
helps['appservice ase list-addresses'] = """
22002200
type: command
2201-
short-summary: List VIPs associated with an app service environment.
2201+
short-summary: List VIPs associated with an app service environment v2.
22022202
examples:
22032203
- name: List VIPs for an app service environments.
22042204
text: az appservice ase list-addresses --name MyAseName
@@ -2228,8 +2228,7 @@
22282228
- name: Create External app service environments v2 with large front-ends and scale factor of 10 in existing resource group and vNet.
22292229
text: |
22302230
az appservice ase create -n MyAseName -g MyResourceGroup --vnet-name MyVirtualNetwork \\
2231-
--subnet MyAseSubnet --front-end-sku I3 --front-end-scale-factor 10 \\
2232-
--virtual-ip-type External
2231+
--subnet MyAseSubnet --front-end-sku I3 --front-end-scale-factor 10 --virtual-ip-type External
22332232
- name: Create vNet and app service environment v2, but do not create network security group and route table in existing resource group.
22342233
text: |
22352234
az network vnet create -g MyResourceGroup -n MyVirtualNetwork \\
@@ -2249,33 +2248,31 @@
22492248
az group create -g ASEv3ResourceGroup --location westeurope
22502249
22512250
az network vnet create -g ASEv3ResourceGroup -n MyASEv3VirtualNetwork \\
2252-
--address-prefixes 10.0.0.0/16 --subnet-name Inbound --subnet-prefixes 10.0.0.0/24
2253-
2254-
az network vnet subnet create -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\
2255-
--name Outbound --address-prefixes 10.0.1.0/24
2251+
--address-prefixes 10.0.0.0/16 --subnet-name MyASEv3Subnet --subnet-prefixes 10.0.0.0/24
22562252
22572253
az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup \\
2258-
--vnet-name MyASEv3VirtualNetwork --subnet Outbound --kind asev3
2254+
--vnet-name MyASEv3VirtualNetwork --subnet MyASEv3Subnet --kind asev3
2255+
- name: Create External zone redundant app service environment v3 with default values.
2256+
text: |
2257+
az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup \\
2258+
--vnet-name MyASEv3VirtualNetwork --subnet MyASEv3Subnet --kind asev3 \\
2259+
--zone-redundant --virtual-ip-type External
22592260
"""
22602261

22612262
helps['appservice ase create-inbound-services'] = """
22622263
type: command
2263-
short-summary: Create the inbound services needed in preview for ASEv3 (private endpoint and DNS) or Private DNS Zone for Internal ASEv2.
2264+
short-summary: Private DNS Zone for Internal ASEv2.
22642265
examples:
2265-
- name: Create private endpoint, Private DNS Zone, A records and ensure subnet network policy.
2266+
- name: Create Private DNS Zone and A records.
22662267
text: |
22672268
az appservice ase create-inbound-services -n MyASEName -g ASEResourceGroup \\
22682269
--vnet-name MyASEVirtualNetwork --subnet MyAseSubnet
2269-
- name: Create private endpoint and ensure subnet network policy (ASEv3), but do not create DNS Zone and records.
2270-
text: |
2271-
az appservice ase create-inbound-services -n MyASEv3Name -g ASEv3ResourceGroup \\
2272-
--vnet-name MyASEv3VirtualNetwork --subnet Inbound --skip-dns
22732270
"""
22742271

22752272

22762273
helps['appservice ase update'] = """
22772274
type: command
2278-
short-summary: Update app service environment.
2275+
short-summary: Update app service environment v2.
22792276
examples:
22802277
- name: Update app service environment with medium front-ends and scale factor of 10.
22812278
text: |

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -914,19 +914,21 @@ def load_arguments(self, _):
914914
c.argument('ignore_subnet_size_validation', arg_type=get_three_state_flag(),
915915
help='Do not check if subnet is sized according to recommendations.')
916916
c.argument('ignore_route_table', arg_type=get_three_state_flag(),
917-
help='Configure route table manually.')
917+
help='Configure route table manually. Applies to ASEv2 only.')
918918
c.argument('ignore_network_security_group', arg_type=get_three_state_flag(),
919-
help='Configure network security group manually.')
919+
help='Configure network security group manually. Applies to ASEv2 only.')
920920
c.argument('force_route_table', arg_type=get_three_state_flag(),
921-
help='Override route table for subnet')
921+
help='Override route table for subnet. Applies to ASEv2 only.')
922922
c.argument('force_network_security_group', arg_type=get_three_state_flag(),
923-
help='Override network security group for subnet')
923+
help='Override network security group for subnet. Applies to ASEv2 only.')
924924
c.argument('front_end_scale_factor', type=int, validator=validate_front_end_scale_factor,
925-
help='Scale of front ends to app service plan instance ratio.', default=15)
925+
help='Scale of front ends to app service plan instance ratio. Applies to ASEv2 only.', default=15)
926926
c.argument('front_end_sku', arg_type=isolated_sku_arg_type, default='I1',
927-
help='Size of front end servers.')
927+
help='Size of front end servers. Applies to ASEv2 only.')
928928
c.argument('os_preference', arg_type=get_enum_type(ASE_OS_PREFERENCE_TYPES),
929929
help='Determine if app service environment should start with Linux workers. Applies to ASEv2 only.')
930+
c.argument('zone_redundant', arg_type=get_three_state_flag(),
931+
help='Configure App Service Environment as Zone Redundant. Applies to ASEv3 only.')
930932
with self.argument_context('appservice ase delete') as c:
931933
c.argument('name', options_list=['--name', '-n'], help='Name of the app service environment')
932934
with self.argument_context('appservice ase update') as c:

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

Lines changed: 24 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
from azure.mgmt.privatedns import PrivateDnsManagementClient
1111

1212
# Models
13-
from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation,
14-
PrivateEndpoint, Subnet, PrivateLinkServiceConnection)
13+
from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation)
1514
from azure.mgmt.resource.resources.models import (DeploymentProperties, Deployment, SubResource)
1615
from azure.mgmt.privatedns.models import (PrivateZone, VirtualNetworkLink, RecordSet, ARecord)
1716

@@ -50,7 +49,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin
5049
ignore_network_security_group=False, virtual_ip_type='Internal',
5150
front_end_scale_factor=None, front_end_sku=None, force_route_table=False,
5251
force_network_security_group=False, ignore_subnet_size_validation=False,
53-
location=None, no_wait=False, os_preference=None):
52+
location=None, no_wait=False, os_preference=None, zone_redundant=None):
5453
# The current SDK has a couple of challenges creating ASE. The current swagger version used,
5554
# did not have 201 as valid response code, and thus will fail with polling operations.
5655
# The Load Balancer Type is an Enum Flag, that is expressed as a simple string enum in swagger,
@@ -79,7 +78,9 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin
7978
elif kind == 'ASEv3':
8079
_ensure_subnet_delegation(cmd.cli_ctx, subnet_id, 'Microsoft.Web/hostingEnvironments')
8180
ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location,
82-
subnet_id=subnet_id, kind='ASEv3')
81+
subnet_id=subnet_id, kind='ASEv3',
82+
virtual_ip_type=virtual_ip_type,
83+
zone_redundant=zone_redundant)
8384
logger.info('Create App Service Environment...')
8485
deployment_client = _get_resource_client_factory(cmd.cli_ctx).deployments
8586
return sdk_no_wait(no_wait, deployment_client.begin_create_or_update,
@@ -122,8 +123,8 @@ def list_appserviceenvironment_addresses(cmd, name, resource_group_name=None):
122123
resource_group_name = _get_resource_group_name_from_ase(ase_client, name)
123124
ase = ase_client.get(resource_group_name, name)
124125
if ase.kind.lower() == 'asev3':
125-
raise CommandNotFoundError('list-addresses is currently not supported for ASEv3. '
126-
'Inbound IP is associated with the private endpoint.')
126+
# return ase_client.get_ase_v3_networking_configuration(resource_group_name, name) # pending SDK update
127+
raise CommandNotFoundError('list-addresses is currently not supported for ASEv3.')
127128
return ase_client.get_vip_info(resource_group_name, name)
128129

129130

@@ -140,34 +141,23 @@ def create_ase_inbound_services(cmd, resource_group_name, name, subnet, vnet_nam
140141
if not ase:
141142
raise ResourceNotFoundError("App Service Environment '{}' not found.".format(name))
142143

144+
if ase.internal_load_balancing_mode == 'None':
145+
raise ValidationError('Private DNS Zone is not relevant for External ASE.')
146+
147+
if ase.kind.lower() == 'asev3':
148+
# pending SDK update (ase_client.get_ase_v3_networking_configuration(resource_group_name, name))
149+
raise CommandNotFoundError('create-inbound-services is currently not supported for ASEv3.')
150+
151+
ase_vip_info = ase_client.get_vip_info(resource_group_name, name)
152+
inbound_ip_address = ase_vip_info.internal_ip_address
143153
inbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name)
144154
inbound_vnet_id = _get_vnet_id_from_subnet(cmd.cli_ctx, inbound_subnet_id)
145-
if ase.kind.lower() == 'asev3':
146-
_ensure_subnet_private_endpoint_network_policy(cmd.cli_ctx, inbound_subnet_id, False)
147-
network_client = _get_network_client_factory(cmd.cli_ctx)
148-
pls_connection = PrivateLinkServiceConnection(private_link_service_id=ase.id,
149-
group_ids=['hostingEnvironments'],
150-
request_message='Link from CLI',
151-
name='{}-private-connection'.format(name))
152-
private_endpoint = PrivateEndpoint(location=ase.location, tags=None, subnet=Subnet(id=inbound_subnet_id))
153-
private_endpoint.private_link_service_connections = [pls_connection]
154-
poller = network_client.private_endpoints.begin_create_or_update(resource_group_name,
155-
'{}-private-endpoint'.format(name),
156-
private_endpoint)
157-
LongRunningOperation(cmd.cli_ctx)(poller)
158-
ase_pe = poller.result()
159-
nic_name = parse_resource_id(ase_pe.network_interfaces[0].id)['name']
160-
nic = network_client.network_interfaces.get(resource_group_name, nic_name)
161-
inbound_ip_address = nic.ip_configurations[0].private_ip_address
162-
elif ase.kind.lower() == 'asev2':
163-
if ase.internal_load_balancing_mode == 0:
164-
raise ValidationError('Private DNS Zone is not relevant for External ASEv2.')
165-
ase_vip_info = ase_client.get_vip_info(resource_group_name, name)
166-
inbound_ip_address = ase_vip_info.internal_ip_address
167155

168156
if not skip_dns:
169157
_ensure_ase_private_dns_zone(cmd.cli_ctx, resource_group_name=resource_group_name, name=name,
170158
inbound_vnet_id=inbound_vnet_id, inbound_ip_address=inbound_ip_address)
159+
else:
160+
logger.warning('Parameter --skip-dns is deprecated.')
171161

172162

173163
def _get_ase_client_factory(cli_ctx, api_version=None):
@@ -288,29 +278,6 @@ def _validate_subnet_size(cli_ctx, subnet_id):
288278
raise validation_error
289279

290280

291-
def _ensure_subnet_private_endpoint_network_policy(cli_ctx, subnet_id, network_policy_enabled):
292-
network_client = _get_network_client_factory(cli_ctx)
293-
subnet_id_parts = parse_resource_id(subnet_id)
294-
vnet_resource_group = subnet_id_parts['resource_group']
295-
vnet_name = subnet_id_parts['name']
296-
subnet_name = subnet_id_parts['resource_name']
297-
subnet_obj = network_client.subnets.get(vnet_resource_group, vnet_name, subnet_name)
298-
target_state = 'Enabled' if network_policy_enabled else 'Disabled'
299-
300-
if subnet_obj.private_endpoint_network_policies != target_state:
301-
subnet_obj.private_endpoint_network_policies = target_state
302-
try:
303-
poller = network_client.subnets.begin_create_or_update(
304-
vnet_resource_group, vnet_name, subnet_name, subnet_parameters=subnet_obj)
305-
LongRunningOperation(cli_ctx)(poller)
306-
except Exception:
307-
err_msg = 'Subnet must have Private Endpoint Network Policy {}.'.format(target_state)
308-
rec_msg = 'Use: az network vnet subnet update --disable-private-endpoint-network-policies'
309-
validation_error = ValidationError(err_msg)
310-
validation_error.set_recommendation(rec_msg)
311-
raise validation_error
312-
313-
314281
def _ensure_subnet_delegation(cli_ctx, subnet_id, delegation_service_name):
315282
network_client = _get_network_client_factory(cli_ctx)
316283
subnet_id_parts = parse_resource_id(subnet_id)
@@ -441,7 +408,7 @@ def _get_unique_deployment_name(prefix):
441408

442409
def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type=None,
443410
front_end_scale_factor=None, front_end_sku=None, tags=None,
444-
kind='ASEv2', os_preference=None):
411+
kind='ASEv2', os_preference=None, zone_redundant=None):
445412
# InternalLoadBalancingMode Enum: None 0, Web 1, Publishing 2.
446413
# External: 0 (None), Internal: 3 (Web + Publishing)
447414
ilb_mode = 3 if virtual_ip_type == 'Internal' else 0
@@ -460,6 +427,8 @@ def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type=
460427
ase_properties['multiSize'] = worker_sku
461428
if os_preference:
462429
ase_properties['osPreference'] = os_preference
430+
if zone_redundant:
431+
ase_properties['zoneRedundant'] = zone_redundant
463432

464433
ase_resource = {
465434
'name': name,
@@ -555,15 +524,15 @@ def _ensure_ase_private_dns_zone(cli_ctx, resource_group_name, name, inbound_vne
555524
private_dns_client = _get_private_dns_client_factory(cli_ctx)
556525
zone_name = '{}.appserviceenvironment.net'.format(name)
557526
zone = PrivateZone(location='global', tags=None)
558-
poller = private_dns_client.private_zones.create_or_update(resource_group_name, zone_name, zone)
527+
poller = private_dns_client.private_zones.begin_create_or_update(resource_group_name, zone_name, zone)
559528
LongRunningOperation(cli_ctx)(poller)
560529

561530
link_name = '{}_link'.format(name)
562531
link = VirtualNetworkLink(location='global', tags=None)
563532
link.virtual_network = SubResource(id=inbound_vnet_id)
564533
link.registration_enabled = False
565-
private_dns_client.virtual_network_links.create_or_update(resource_group_name, zone_name,
566-
link_name, link, if_none_match='*')
534+
private_dns_client.virtual_network_links.begin_create_or_update(resource_group_name, zone_name,
535+
link_name, link, if_none_match='*')
567536
ase_record = ARecord(ipv4_address=inbound_ip_address)
568537
record_set = RecordSet(ttl=3600)
569538
record_set.a_records = [ase_record]

src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,43 @@ def test_app_service_environment_v3_create(self, ase_client_factory_mock, networ
218218
self.assertEqual(call_args[0][0], rg_name)
219219
self.assertEqual(call_args[0][1], deployment_name)
220220

221+
@mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_unique_deployment_name', autospec=True)
222+
@mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_resource_client_factory', autospec=True)
223+
@mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_network_client_factory', autospec=True)
224+
@mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_ase_client_factory', autospec=True)
225+
def test_app_service_environment_v3_zone_create(self, ase_client_factory_mock, network_client_factory_mock,
226+
resource_client_factory_mock, deployment_name_mock):
227+
ase_name = 'mock_ase_name'
228+
rg_name = 'mock_rg_name'
229+
vnet_name = 'mock_vnet_name'
230+
subnet_name = 'mock_subnet_name'
231+
deployment_name = 'mock_deployment_name'
232+
233+
ase_client = mock.MagicMock()
234+
ase_client_factory_mock.return_value = ase_client
235+
236+
resource_client_mock = mock.MagicMock()
237+
resource_client_factory_mock.return_value = resource_client_mock
238+
239+
deployment_name_mock.return_value = deployment_name
240+
241+
network_client = mock.MagicMock()
242+
network_client_factory_mock.return_value = network_client
243+
244+
subnet = Subnet(id=1, address_prefix='10.10.10.10/24')
245+
hosting_delegation = Delegation(id=1, service_name='Microsoft.Web/hostingEnvironments')
246+
subnet.delegations = [hosting_delegation]
247+
network_client.subnets.get.return_value = subnet
248+
create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name,
249+
subnet=subnet_name, vnet_name=vnet_name, kind='ASEv3',
250+
location='westeurope', zone_redundant=True)
251+
252+
# Assert begin_create_or_update is called with correct rg and deployment name
253+
resource_client_mock.deployments.begin_create_or_update.assert_called_once()
254+
call_args = resource_client_mock.deployments.begin_create_or_update.call_args
255+
self.assertEqual(call_args[0][0], rg_name)
256+
self.assertEqual(call_args[0][1], deployment_name)
257+
221258

222259
if __name__ == '__main__':
223260
unittest.main()

0 commit comments

Comments
 (0)