From 1ec2cc4de4f1e09a68f19978ee3ecec61177dadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Fri, 11 Dec 2020 14:20:54 +0100 Subject: [PATCH 1/9] ASE v3 base changes --- .../cli/command_modules/appservice/_help.py | 21 ++- .../cli/command_modules/appservice/_params.py | 7 +- .../appservice/appservice_environment.py | 166 ++++++++++++++++-- .../cli/command_modules/appservice/custom.py | 6 +- .../cli/command_modules/appservice/utils.py | 2 + 5 files changed, 177 insertions(+), 25 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index cd5519fff1d..16aa58e0319 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -2148,7 +2148,7 @@ type: command short-summary: Create app service environment. examples: - - name: Create Resource Group, vNet and app service environment with default values. + - name: Create Resource Group, vNet and app service environment v2 with default values. text: | az group create -g MyResourceGroup --location westeurope @@ -2157,27 +2157,40 @@ az appservice ase create -n MyAseName -g MyResourceGroup --vnet-name MyVirtualNetwork \\ --subnet MyAseSubnet - - name: Create External app service environments with large front-ends and scale factor of 10 in existing resource group and vNet. + - name: Create External app service environments v2 with large front-ends and scale factor of 10 in existing resource group and vNet. text: | az appservice ase create -n MyAseName -g MyResourceGroup --vnet-name MyVirtualNetwork \\ --subnet MyAseSubnet --front-end-sku I3 --front-end-scale-factor 10 \\ --virtual-ip-type External - - name: Create vNet and app service environment, but do not create network security group and route table in existing resource group. + - name: Create vNet and app service environment v2, but do not create network security group and route table in existing resource group. text: | az network vnet create -g MyResourceGroup -n MyVirtualNetwork \\ --address-prefixes 10.0.0.0/16 --subnet-name MyAseSubnet --subnet-prefixes 10.0.0.0/24 az appservice ase create -n MyAseName -g MyResourceGroup --vnet-name MyVirtualNetwork \\ --subnet MyAseSubnet --ignore-network-security-group --ignore-route-table - - name: Create vNet and app service environment in a smaller than recommended subnet in existing resource group. + - name: Create vNet and app service environment v2 in a smaller than recommended subnet in existing resource group. text: | az network vnet create -g MyResourceGroup -n MyVirtualNetwork \\ --address-prefixes 10.0.0.0/16 --subnet-name MyAseSubnet --subnet-prefixes 10.0.0.0/26 az appservice ase create -n MyAseName -g MyResourceGroup --vnet-name MyVirtualNetwork \\ --subnet MyAseSubnet --ignore-subnet-size-validation + - name: Create Resource Group, vNet and app service environment v3 with default values. + text: | + az group create -g ASEv3ResourceGroup --location westeurope + + az network vnet create -g ASEv3ResourceGroup -n MyASEv3VirtualNetwork \\ + --address-prefixes 10.0.0.0/16 --subnet-name Inbound --subnet-prefixes 10.0.0.0/24 + + az network vnet subnet create -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\ + --name Outbound --address-prefixes 10.0.1.0/24 + + az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\ + --subnet Inbound --outbound-subnet Outbound --kind asev3 """ + helps['appservice ase update'] = """ type: command short-summary: Update app service environment. 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 b2279200c68..1b2150e90a9 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -35,6 +35,7 @@ WINDOWS_RUNTIMES = ['dotnet', 'node', 'java', 'powershell'] ACCESS_RESTRICTION_ACTION_TYPES = ['Allow', 'Deny'] ASE_LOADBALANCER_MODES = ['Internal', 'External'] +ASE_KINDS = ['asev2', 'asev3'] # pylint: disable=too-many-statements, too-many-lines @@ -45,10 +46,10 @@ 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), P1V3(Premium V3 Small), P2V3(Premium V3 Medium), P3V3(Premium V3 Large), PC2 (Premium Container Small), PC3 (Premium Container Medium), PC4 (Premium Container Large), I1 (Isolated Small), I2 (Isolated Medium), I3 (Isolated Large)', + 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), P1V3(Premium V3 Small), P2V3(Premium V3 Medium), P3V3(Premium V3 Large), PC2 (Premium Container Small), PC3 (Premium Container Medium), PC4 (Premium Container Large), I1 (Isolated Small), I2 (Isolated Medium), I3 (Isolated Large), I1v2 (Isolated V2 Small), I2v2 (Isolated V2 Medium), I3v2 (Isolated V2 Large)', arg_type=get_enum_type( ['F1', 'FREE', 'D1', 'SHARED', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1V2', 'P2V2', 'P3V2', 'P1V3', 'P2V3', 'P3V3', 'PC2', 'PC3', - 'PC4', 'I1', 'I2', 'I3'])) + 'PC4', 'I1', 'I2', 'I3', 'I1v2', 'I2v2', 'I3v2'])) 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', @@ -869,6 +870,8 @@ def load_arguments(self, _): help='Name of the app service environment', local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.SET], scopes=['appservice'])) + c.argument('kind', options_list=['--kind', '-k'], arg_type=get_enum_type(ASE_KINDS), + default='asev2', help="Specify App Service Environment version") c.argument('subnet', help='Name or ID of existing subnet. To create vnet and/or subnet \ use `az network vnet [subnet] create`') c.argument('vnet_name', help='Name of the vNet. Mandatory if only subnet name is specified.') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index 70bfed8cfbe..f4432e58208 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -9,7 +9,7 @@ from azure.mgmt.resource import ResourceManagementClient # Models -from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule) +from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation) from azure.mgmt.resource.resources.models import (DeploymentProperties, Deployment) # Utils @@ -41,7 +41,8 @@ def show_appserviceenvironment(cmd, name, resource_group_name=None): return ase_client.get(resource_group_name, name) -def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, vnet_name=None, ignore_route_table=False, +def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='asev2', outbound_subnet=None, + vnet_name=None, ignore_route_table=False, ignore_network_security_group=False, virtual_ip_type='Internal', front_end_scale_factor=None, front_end_sku=None, force_route_table=False, force_network_security_group=False, ignore_subnet_size_validation=False, @@ -53,22 +54,38 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, vne # Therefore the current method use direct ARM. location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name) - - _validate_subnet_empty(cmd.cli_ctx, subnet_id) - if not ignore_subnet_size_validation: - _validate_subnet_size(cmd.cli_ctx, subnet_id) - if not ignore_route_table: - _ensure_route_table(cmd.cli_ctx, resource_group_name, name, location, subnet_id, force_route_table) - if not ignore_network_security_group: - _ensure_network_security_group(cmd.cli_ctx, resource_group_name, name, location, - subnet_id, force_network_security_group) + outbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, outbound_subnet, vnet_name, resource_group_name) + + if kind == 'asev2': + _validate_subnet_empty(cmd.cli_ctx, subnet_id) + if not ignore_subnet_size_validation: + _validate_subnet_size(cmd.cli_ctx, subnet_id) + if not ignore_route_table: + _ensure_route_table(cmd.cli_ctx, resource_group_name, name, location, subnet_id, force_route_table) + if not ignore_network_security_group: + _ensure_network_security_group(cmd.cli_ctx, resource_group_name, name, location, + subnet_id, force_network_security_group) + elif kind == 'asev3': + if not ignore_subnet_size_validation: + _validate_subnet_size(cmd.cli_ctx, outbound_subnet_id) + _ensure_subnets_asev3(cmd.cli_ctx, subnet_id, outbound_subnet_id) logger.info('Create App Service Environment...') + return deployment_name = _get_unique_deployment_name('cli_ase_deploy_') - ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, - subnet_id=subnet_id, virtual_ip_type=virtual_ip_type, - front_end_scale_factor=front_end_scale_factor, - front_end_sku=front_end_sku, tags=None) + + if kind == 'asev2': + ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, + subnet_id=subnet_id, + virtual_ip_type=virtual_ip_type, + front_end_scale_factor=front_end_scale_factor, + front_end_sku=front_end_sku, tags=None) + elif kind == 'asev3': + ase_deployment_properties = _build_asev3_deployment_properties(name=name, location=location, + inbound_subnet_id=subnet_id, + outbound_subnet_id=outbound_subnet_id, + front_end_scale_factor=front_end_scale_factor, + front_end_sku=front_end_sku, tags=None) deployment_client = _get_resource_client_factory(cmd.cli_ctx).deployments return sdk_no_wait(no_wait, deployment_client.create_or_update, @@ -90,6 +107,9 @@ def update_appserviceenvironment(cmd, name, resource_group_name=None, front_end_ if resource_group_name is None: resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase_def = ase_client.get(resource_group_name, name) + if ase_def.kind.lower() != 'asev2': + raise CLIError('Only ASE v2 currently supports updates') + worker_sku = _map_worker_sku(front_end_sku) ase_def.worker_pools = ase_def.worker_pools or [] # v1 feature, but cannot be null ase_def.internal_load_balancing_mode = None # Workaround issue with flag enums in Swagger @@ -106,6 +126,9 @@ def list_appserviceenvironment_addresses(cmd, name, resource_group_name=None): ase_client = _get_ase_client_factory(cmd.cli_ctx) if resource_group_name is None: resource_group_name = _get_resource_group_name_from_ase(ase_client, name) + ase = ase_client.get(resource_group_name, name) + if ase.kind.lower() == 'asev3': + raise CLIError('list-addresses is currently not supported for ASEv3. Inbound IP is associated with the private endpoint.') return ase_client.get_vip_info(resource_group_name, name) @@ -205,7 +228,57 @@ def _validate_subnet_size(cli_ctx, subnet_id): address = subnet_obj.address_prefix size = int(address[address.index('/') + 1:]) if size > 24: - raise CLIError('Subnet is too small. Should be at least /24.') + raise CLIError('Subnet size could cause scaling issues. Recommended size is at least /24. ' + 'Use --ignore-subnet-size-validation to skip size test.') + + +def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): + network_client = _get_network_client_factory(cli_ctx) + + # Inbound + in_subnet_id_parts = parse_resource_id(inbound_subnet_id) + in_vnet_resource_group = in_subnet_id_parts['resource_group'] + in_vnet_name = in_subnet_id_parts['name'] + in_subnet_name = in_subnet_id_parts['resource_name'] + in_subnet_obj = network_client.subnets.get(in_vnet_resource_group, in_vnet_name, in_subnet_name) + if in_subnet_obj.private_endpoint_network_policies == 'Enabled': + logger.warning('Disabling inbound subnet Private Endpoint Network Policy setting.') + in_subnet_obj.private_endpoint_network_policies = 'Disabled' + try: + poller = network_client.subnets.create_or_update( + in_vnet_resource_group, in_vnet_name, + in_subnet_name, subnet_parameters=in_subnet_obj) + LongRunningOperation(cli_ctx)(poller) + except Exception: + raise CLIError('Inbound subnet must have Private Endpoint Network Policy disabled.' + 'Use: az network vnet subnet update --disable-private-endpoint-network-policies') + + # Outbound + out_subnet_id_parts = parse_resource_id(outbound_subnet_id) + out_vnet_resource_group = out_subnet_id_parts['resource_group'] + out_vnet_name = out_subnet_id_parts['name'] + out_subnet_name = out_subnet_id_parts['resource_name'] + out_subnet_obj = network_client.subnets.get(out_vnet_resource_group, out_vnet_name, out_subnet_name) + if out_subnet_obj.resource_navigation_links: + raise CLIError('Outbound subnet is not empty.') + + delegations = out_subnet_obj.delegations + delegated = False + for d in delegations: + if d.service_name.lower() == "microsoft.web/hostingenvironments": + delegated = True + + if not delegated: + logger.warning('Adding delegation for hostingEnvironments to outbound subnet.') + out_subnet_obj.delegations = [Delegation(name="delegation", service_name="Microsoft.Web/hostingEnvironments")] + try: + poller = network_client.subnets.create_or_update( + out_vnet_resource_group, out_vnet_name, + out_subnet_name, subnet_parameters=out_subnet_obj) + LongRunningOperation(cli_ctx)(poller) + except Exception: + raise CLIError('Outbound subnet must be delegated to Microsoft.Web/hostingEnvironments. ' + 'Use: az network vnet subnet update --delegations "Microsoft.Web/hostingEnvironments"') def _ensure_route_table(cli_ctx, resource_group_name, ase_name, location, subnet_id, force): @@ -362,7 +435,7 @@ def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type, 'location': location, 'InternalLoadBalancingMode': ilb_mode, 'virtualNetwork': { - 'Id': subnet_id + 'id': subnet_id } } if front_end_scale_factor: @@ -391,6 +464,65 @@ def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type, return deployment +def _build_asev3_deployment_properties(name, location, inbound_subnet_id, outbound_subnet_id, tags=None): + ase_properties = { + "location": location, + "virtualNetwork": { + "id": outbound_subnet_id + } + } + + ase_resource = { + "name": name, + "type": "Microsoft.Web/hostingEnvironments", + "location": location, + "apiVersion": "2019-08-01", + "kind": "ASEV3", + "tags": tags, + "properties": ase_properties + } + + privateLinkConnectionName = '{}-privateLink'.format(name) + pe_properties = { + "subnet": { + "id": inbound_subnet_id + }, + "privateLinkServiceConnections": [ + { + "name": privateLinkConnectionName, + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.Web/hostingEnvironments', '{}')]".format(name), + "groupIds": [ + "hostingEnvironments" + ] + } + } + ] + } + + privateEndpointName = '{}-privateEndpoint'.format(name) + pe_resource = { + "name": privateEndpointName, + "type": "Microsoft.Network/privateEndpoints", + "location": location, + "apiVersion": "2019-04-01", + "dependsOn": [ + "[resourceId('Microsoft.Web/hostingEnvironments', '{}')]".format(name) + ], + "properties": pe_properties + } + + deployment_template = ArmTemplateBuilder() + deployment_template.add_resource(ase_resource) + deployment_template.add_resource(pe_resource) + template = deployment_template.build() + parameters = deployment_template.build_parameters() + + deploymentProperties = DeploymentProperties(template=template, parameters=parameters, mode='Incremental') + deployment = Deployment(properties=deploymentProperties) + return deployment + + def _create_nsg_rule(cli_ctx, resource_group_name, network_security_group_name, security_rule_name, priority, description=None, protocol=None, access=None, direction=None, source_port_range='*', source_address_prefix='*', 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 c2b42e5801a..5b4096d30ee 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1717,12 +1717,14 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p ase_def = None if location is None: location = _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) - + logger.warning('create') # the api is odd on parameter naming, have to live with it for now sku_def = SkuDescription(tier=get_sku_name(sku), name=sku, capacity=number_of_workers) + logger.warning(sku_def) plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def, reserved=(is_linux or None), hyper_v=(hyper_v or None), name=name, per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def) + logger.warning(plan_def) return sdk_no_wait(no_wait, client.app_service_plans.create_or_update, name=name, resource_group_name=resource_group_name, app_service_plan=plan_def) @@ -4086,7 +4088,7 @@ def _validate_app_service_environment_id(cli_ctx, ase, resource_group_name): def _validate_asp_sku(app_service_environment, sku): # Isolated SKU is supported only for ASE - if sku in ['I1', 'I2', 'I3']: + if sku.upper() in ['I1', 'I2', 'I3', 'I1V2', 'I2V2', 'I3V2']: if not app_service_environment: raise CLIError("The pricing tier 'Isolated' is not allowed for this app service plan. Use this link to " "learn more: https://docs.microsoft.com/en-us/azure/app-service/overview-hosting-plans") 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 33c5840dc1e..5fc8eceeca1 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -48,6 +48,8 @@ def get_sku_name(tier): # pylint: disable=too-many-return-statements return 'ElasticPremium' if tier in ['I1', 'I2', 'I3']: return 'Isolated' + if tier in ['I1V2', 'I2V2', 'I3V2']: + return 'IsolatedV2' raise CLIError("Invalid sku(pricing tier), please refer to command help for valid values") From 8c857aca7476a2bc4613c52a80e825b033b691dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Mon, 14 Dec 2020 13:49:44 +0100 Subject: [PATCH 2/9] Code optimization --- .../appservice/appservice_environment.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index f4432e58208..78550a910aa 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -41,7 +41,7 @@ def show_appserviceenvironment(cmd, name, resource_group_name=None): return ase_client.get(resource_group_name, name) -def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='asev2', outbound_subnet=None, +def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='asev2', inbound_subnet=None, vnet_name=None, ignore_route_table=False, ignore_network_security_group=False, virtual_ip_type='Internal', front_end_scale_factor=None, front_end_sku=None, force_route_table=False, @@ -54,7 +54,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin # Therefore the current method use direct ARM. location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name) - outbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, outbound_subnet, vnet_name, resource_group_name) + deployment_name = _get_unique_deployment_name('cli_ase_deploy_') if kind == 'asev2': _validate_subnet_empty(cmd.cli_ctx, subnet_id) @@ -65,28 +65,26 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin if not ignore_network_security_group: _ensure_network_security_group(cmd.cli_ctx, resource_group_name, name, location, subnet_id, force_network_security_group) - elif kind == 'asev3': - if not ignore_subnet_size_validation: - _validate_subnet_size(cmd.cli_ctx, outbound_subnet_id) - _ensure_subnets_asev3(cmd.cli_ctx, subnet_id, outbound_subnet_id) - - logger.info('Create App Service Environment...') - return - deployment_name = _get_unique_deployment_name('cli_ase_deploy_') - if kind == 'asev2': ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, subnet_id=subnet_id, virtual_ip_type=virtual_ip_type, front_end_scale_factor=front_end_scale_factor, front_end_sku=front_end_sku, tags=None) + elif kind == 'asev3': + inbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, inbound_subnet, vnet_name, resource_group_name) + if not ignore_subnet_size_validation: + _validate_subnet_size(cmd.cli_ctx, subnet_id) + _ensure_subnets_asev3(cmd.cli_ctx, inbound_subnet_id, subnet_id) + ase_deployment_properties = _build_asev3_deployment_properties(name=name, location=location, - inbound_subnet_id=subnet_id, - outbound_subnet_id=outbound_subnet_id, + inbound_subnet_id=inbound_subnet_id, + outbound_subnet_id=subnet_id, front_end_scale_factor=front_end_scale_factor, front_end_sku=front_end_sku, tags=None) + logger.info('Create App Service Environment...') deployment_client = _get_resource_client_factory(cmd.cli_ctx).deployments return sdk_no_wait(no_wait, deployment_client.create_or_update, resource_group_name, deployment_name, ase_deployment_properties) @@ -108,7 +106,7 @@ def update_appserviceenvironment(cmd, name, resource_group_name=None, front_end_ resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase_def = ase_client.get(resource_group_name, name) if ase_def.kind.lower() != 'asev2': - raise CLIError('Only ASE v2 currently supports updates') + raise CLIError('Only ASE v2 currently supports update') worker_sku = _map_worker_sku(front_end_sku) ase_def.worker_pools = ase_def.worker_pools or [] # v1 feature, but cannot be null @@ -128,7 +126,8 @@ def list_appserviceenvironment_addresses(cmd, name, resource_group_name=None): resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase = ase_client.get(resource_group_name, name) if ase.kind.lower() == 'asev3': - raise CLIError('list-addresses is currently not supported for ASEv3. Inbound IP is associated with the private endpoint.') + raise CLIError('list-addresses is currently not supported for ASEv3. \ + Inbound IP is associated with the private endpoint.') return ase_client.get_vip_info(resource_group_name, name) From 7185edbcfe3c158ff7f642ace6fcded81c906219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Mon, 21 Dec 2020 12:49:40 +0100 Subject: [PATCH 3/9] Additional check for empty subnet --- .../cli/command_modules/appservice/appservice_environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index 78550a910aa..adb5125c1be 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -213,7 +213,7 @@ def _validate_subnet_empty(cli_ctx, subnet_id): subnet_name = subnet_id_parts['resource_name'] network_client = _get_network_client_factory(cli_ctx) subnet_obj = network_client.subnets.get(vnet_resource_group, vnet_name, subnet_name) - if subnet_obj.resource_navigation_links: + if subnet_obj.resource_navigation_links or subnet_obj.service_association_links: raise CLIError('Subnet is not empty.') From c2d3dd44baaa3977e5d76256afc13d230d29b679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Tue, 12 Jan 2021 21:51:52 +0100 Subject: [PATCH 4/9] Help, test and param changes --- .../cli/command_modules/appservice/_help.py | 2 +- .../cli/command_modules/appservice/_params.py | 6 ++- .../appservice/appservice_environment.py | 12 ++--- ..._service_environment_commands_thru_mock.py | 48 +++++++++++++++++-- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index 16aa58e0319..8f389ac4fb1 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -2187,7 +2187,7 @@ --name Outbound --address-prefixes 10.0.1.0/24 az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\ - --subnet Inbound --outbound-subnet Outbound --kind asev3 + --subnet Outbound --inbound-subnet Inbound --kind asev3 """ 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 1b2150e90a9..929f7b60741 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -35,7 +35,7 @@ WINDOWS_RUNTIMES = ['dotnet', 'node', 'java', 'powershell'] ACCESS_RESTRICTION_ACTION_TYPES = ['Allow', 'Deny'] ASE_LOADBALANCER_MODES = ['Internal', 'External'] -ASE_KINDS = ['asev2', 'asev3'] +ASE_KINDS = ['ASEv2', 'ASEv3'] # pylint: disable=too-many-statements, too-many-lines @@ -871,7 +871,7 @@ def load_arguments(self, _): local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.SET], scopes=['appservice'])) c.argument('kind', options_list=['--kind', '-k'], arg_type=get_enum_type(ASE_KINDS), - default='asev2', help="Specify App Service Environment version") + default='ASEv2', help="Specify App Service Environment version") c.argument('subnet', help='Name or ID of existing subnet. To create vnet and/or subnet \ use `az network vnet [subnet] create`') c.argument('vnet_name', help='Name of the vNet. Mandatory if only subnet name is specified.') @@ -891,6 +891,8 @@ def load_arguments(self, _): help='Scale of front ends to app service plan instance ratio.', default=15) c.argument('front_end_sku', arg_type=isolated_sku_arg_type, default='I1', help='Size of front end servers.') + c.argument('inbound_subnet', help='Required for ASEv3. Name or ID of existing subnet. \ + To create vnet and/or subnet use `az network vnet [subnet] create`') with self.argument_context('appservice ase delete') as c: c.argument('name', options_list=['--name', '-n'], help='Name of the app service environment') with self.argument_context('appservice ase update') as c: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index adb5125c1be..ad6caf12fa9 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -41,7 +41,7 @@ def show_appserviceenvironment(cmd, name, resource_group_name=None): return ase_client.get(resource_group_name, name) -def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='asev2', inbound_subnet=None, +def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='ASEv2', inbound_subnet=None, vnet_name=None, ignore_route_table=False, ignore_network_security_group=False, virtual_ip_type='Internal', front_end_scale_factor=None, front_end_sku=None, force_route_table=False, @@ -56,7 +56,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name) deployment_name = _get_unique_deployment_name('cli_ase_deploy_') - if kind == 'asev2': + if kind == 'ASEv2': _validate_subnet_empty(cmd.cli_ctx, subnet_id) if not ignore_subnet_size_validation: _validate_subnet_size(cmd.cli_ctx, subnet_id) @@ -72,7 +72,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin front_end_scale_factor=front_end_scale_factor, front_end_sku=front_end_sku, tags=None) - elif kind == 'asev3': + elif kind == 'ASEv3': inbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, inbound_subnet, vnet_name, resource_group_name) if not ignore_subnet_size_validation: _validate_subnet_size(cmd.cli_ctx, subnet_id) @@ -80,9 +80,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin ase_deployment_properties = _build_asev3_deployment_properties(name=name, location=location, inbound_subnet_id=inbound_subnet_id, - outbound_subnet_id=subnet_id, - front_end_scale_factor=front_end_scale_factor, - front_end_sku=front_end_sku, tags=None) + outbound_subnet_id=subnet_id, tags=None) logger.info('Create App Service Environment...') deployment_client = _get_resource_client_factory(cmd.cli_ctx).deployments @@ -106,7 +104,7 @@ def update_appserviceenvironment(cmd, name, resource_group_name=None, front_end_ resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase_def = ase_client.get(resource_group_name, name) if ase_def.kind.lower() != 'asev2': - raise CLIError('Only ASE v2 currently supports update') + raise CLIError('Only ASEv2 currently supports update') worker_sku = _map_worker_sku(front_end_sku) ase_def.worker_pools = ase_def.worker_pools or [] # v1 feature, but cannot be null diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py index 49f4e07e6b9..1038feebe0c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py @@ -13,7 +13,7 @@ from azure.mgmt.web import WebSiteManagementClient from azure.mgmt.web.models import HostingEnvironmentProfile -from azure.mgmt.network.models import (Subnet, RouteTable, Route, NetworkSecurityGroup, SecurityRule) +from azure.mgmt.network.models import (Subnet, RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation) from azure.cli.core.adal_authentication import AdalAuthentication from azure.cli.command_modules.appservice.appservice_environment import (show_appserviceenvironment, @@ -105,13 +105,15 @@ def test_app_service_environment_create(self, ase_client_factory_mock, network_c # assert that CLIError raised when called with small subnet with self.assertRaises(CLIError): - create_appserviceenvironment_arm(self.mock_cmd, rg_name, ase_name, subnet_name, vnet_name, + create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, + subnet=subnet_name, vnet_name=vnet_name, ignore_network_security_group=True, ignore_route_table=True, location='westeurope') subnet = Subnet(id=1, address_prefix='10.10.10.10/24') network_client.subnets.get.return_value = subnet - create_appserviceenvironment_arm(self.mock_cmd, rg_name, ase_name, subnet_name, vnet_name, + create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, + subnet=subnet_name, vnet_name=vnet_name, ignore_network_security_group=True, ignore_route_table=True, location='westeurope') @@ -133,6 +135,7 @@ def test_app_service_environment_update(self, ase_client_factory_mock, resource_ host_env = HostingEnvironmentProfile(id='id1') host_env.name = ase_name + host_env.kind = 'ASEv2' host_env.resource_group = rg_name host_env.worker_pools = [] ase_client.get.return_value = host_env @@ -143,11 +146,12 @@ def test_app_service_environment_update(self, ase_client_factory_mock, resource_ # Assert create_or_update is called with correct properties assert_host_env = HostingEnvironmentProfile(id='id1') assert_host_env.name = ase_name + assert_host_env.kind = 'ASEv2' assert_host_env.resource_group = rg_name assert_host_env.worker_pools = [] assert_host_env.internal_load_balancing_mode = None assert_host_env.front_end_scale_factor = 10 - ase_client.create_or_update.assert_called_once_with(name=ase_name, resource_group_name=rg_name, + ase_client.create_or_update.assert_called_once_with(resource_group_name=rg_name, name=ase_name, hosting_environment_envelope=assert_host_env) @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_location_from_resource_group', autospec=True) @@ -176,6 +180,42 @@ def test_app_service_environment_delete(self, ase_client_factory_mock, resource_ assert_host_env.resource_group = rg_name ase_client.delete.assert_called_once_with(name=ase_name, resource_group_name=rg_name) + @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_unique_deployment_name', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_resource_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_network_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_ase_client_factory', autospec=True) + def test_app_service_environment_v3_create(self, ase_client_factory_mock, network_client_factory_mock, + resource_client_factory_mock, deployment_name_mock): + ase_name = 'mock_ase_name' + rg_name = 'mock_rg_name' + vnet_name = 'mock_vnet_name' + subnet_name = 'mock_subnet_name' + inbound_subnet_name = 'mock_inbound_subnet_name' + deployment_name = 'mock_deployment_name' + + ase_client = mock.MagicMock() + ase_client_factory_mock.return_value = ase_client + + resource_client_mock = mock.MagicMock() + resource_client_factory_mock.return_value = resource_client_mock + + deployment_name_mock.return_value = deployment_name + + network_client = mock.MagicMock() + network_client_factory_mock.return_value = network_client + + subnet = Subnet(id=1, address_prefix='10.10.10.10/24') + hosting_delegation = Delegation(id=1, service_name='Microsoft.Web/hostingEnvironment') + subnet.delegations = [hosting_delegation] + network_client.subnets.get.return_value = subnet + create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, + subnet=subnet_name, vnet_name=vnet_name, kind='asev3', + inbound_subnet=inbound_subnet_name, location='westeurope') + + # Assert create_or_update is called with correct rg and deployment name + resource_client_mock.deployments.create_or_update.assert_called_once() + self.assertEqual(resource_client_mock.deployments.create_or_update.call_args[0][0], rg_name) + self.assertEqual(resource_client_mock.deployments.create_or_update.call_args[0][1], deployment_name) if __name__ == '__main__': unittest.main() From 9667112055b3cb1437a64ebb6374e4487a31b03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Wed, 13 Jan 2021 11:56:09 +0100 Subject: [PATCH 5/9] Remove debug info and fix style --- .../command_modules/appservice/appservice_environment.py | 4 ++-- .../azure/cli/command_modules/appservice/custom.py | 6 +++--- .../test_app_service_environment_commands_thru_mock.py | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index ad6caf12fa9..1144567eb1d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -242,7 +242,7 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): logger.warning('Disabling inbound subnet Private Endpoint Network Policy setting.') in_subnet_obj.private_endpoint_network_policies = 'Disabled' try: - poller = network_client.subnets.create_or_update( + poller = network_client.subnets.begin_create_or_update( in_vnet_resource_group, in_vnet_name, in_subnet_name, subnet_parameters=in_subnet_obj) LongRunningOperation(cli_ctx)(poller) @@ -269,7 +269,7 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): logger.warning('Adding delegation for hostingEnvironments to outbound subnet.') out_subnet_obj.delegations = [Delegation(name="delegation", service_name="Microsoft.Web/hostingEnvironments")] try: - poller = network_client.subnets.create_or_update( + poller = network_client.subnets.begin_create_or_update( out_vnet_resource_group, out_vnet_name, out_subnet_name, subnet_parameters=out_subnet_obj) LongRunningOperation(cli_ctx)(poller) 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 5b4096d30ee..c04fc81f248 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1717,14 +1717,14 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p ase_def = None if location is None: location = _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) - logger.warning('create') + # the api is odd on parameter naming, have to live with it for now sku_def = SkuDescription(tier=get_sku_name(sku), name=sku, capacity=number_of_workers) - logger.warning(sku_def) + plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def, reserved=(is_linux or None), hyper_v=(hyper_v or None), name=name, per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def) - logger.warning(plan_def) + return sdk_no_wait(no_wait, client.app_service_plans.create_or_update, name=name, resource_group_name=resource_group_name, app_service_plan=plan_def) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py index 1038feebe0c..520c498ea14 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py @@ -185,7 +185,7 @@ def test_app_service_environment_delete(self, ase_client_factory_mock, resource_ @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_network_client_factory', autospec=True) @mock.patch('azure.cli.command_modules.appservice.appservice_environment._get_ase_client_factory', autospec=True) def test_app_service_environment_v3_create(self, ase_client_factory_mock, network_client_factory_mock, - resource_client_factory_mock, deployment_name_mock): + resource_client_factory_mock, deployment_name_mock): ase_name = 'mock_ase_name' rg_name = 'mock_rg_name' vnet_name = 'mock_vnet_name' @@ -209,13 +209,14 @@ def test_app_service_environment_v3_create(self, ase_client_factory_mock, networ subnet.delegations = [hosting_delegation] network_client.subnets.get.return_value = subnet create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, - subnet=subnet_name, vnet_name=vnet_name, kind='asev3', - inbound_subnet=inbound_subnet_name, location='westeurope') + subnet=subnet_name, vnet_name=vnet_name, kind='ASEv3', + inbound_subnet=inbound_subnet_name, location='westeurope') # Assert create_or_update is called with correct rg and deployment name resource_client_mock.deployments.create_or_update.assert_called_once() self.assertEqual(resource_client_mock.deployments.create_or_update.call_args[0][0], rg_name) self.assertEqual(resource_client_mock.deployments.create_or_update.call_args[0][1], deployment_name) + if __name__ == '__main__': unittest.main() From 58559ef8fc1a1c5054fa4f5660a2186b0d59ed8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Wed, 13 Jan 2021 14:08:21 +0100 Subject: [PATCH 6/9] Align with new Error guidelines --- .../appservice/appservice_environment.py | 58 ++++++++++++------- .../cli/command_modules/appservice/custom.py | 2 - ..._service_environment_commands_thru_mock.py | 8 +-- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index 1144567eb1d..4d6efd44c8f 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -17,8 +17,9 @@ from azure.cli.core.commands.client_factory import (get_mgmt_service_client, get_subscription_id) from azure.cli.core.commands.arm import ArmTemplateBuilder from azure.cli.core.util import (sdk_no_wait, random_string) +from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, CommandNotFoundError, + MutuallyExclusiveArgumentError) from knack.log import get_logger -from knack.util import CLIError from msrestazure.tools import (parse_resource_id, is_valid_resource_id, resource_id) VERSION_2019_10_01 = "2019-10-01" @@ -104,7 +105,7 @@ def update_appserviceenvironment(cmd, name, resource_group_name=None, front_end_ resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase_def = ase_client.get(resource_group_name, name) if ase_def.kind.lower() != 'asev2': - raise CLIError('Only ASEv2 currently supports update') + raise CommandNotFoundError('Only ASEv2 currently supports update') worker_sku = _map_worker_sku(front_end_sku) ase_def.worker_pools = ase_def.worker_pools or [] # v1 feature, but cannot be null @@ -124,8 +125,8 @@ def list_appserviceenvironment_addresses(cmd, name, resource_group_name=None): resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase = ase_client.get(resource_group_name, name) if ase.kind.lower() == 'asev3': - raise CLIError('list-addresses is currently not supported for ASEv3. \ - Inbound IP is associated with the private endpoint.') + raise CommandNotFoundError('list-addresses is currently not supported for ASEv3. ' \ + 'Inbound IP is associated with the private endpoint.') return ase_client.get_vip_info(resource_group_name, name) @@ -175,7 +176,7 @@ def _get_resource_group_name_from_ase(ase_client, ase_name): ase_found = True break if not ase_found: - raise CLIError("App service environment '{}' not found in subscription.".format(ase_name)) + raise ResourceNotFoundError("App service environment '{}' not found in subscription.".format(ase_name)) return resource_group @@ -192,7 +193,7 @@ def _validate_subnet_id(cli_ctx, subnet, vnet_name, resource_group_name): name=vnet_name, child_type_1='subnets', child_name_1=subnet) - raise CLIError('Usage error: --subnet ID | --subnet NAME --vnet-name NAME') + raise MutuallyExclusiveArgumentError('Please specify either: --subnet ID or (--subnet NAME and --vnet-name NAME)') def _map_worker_sku(sku_name): @@ -212,7 +213,7 @@ def _validate_subnet_empty(cli_ctx, subnet_id): network_client = _get_network_client_factory(cli_ctx) subnet_obj = network_client.subnets.get(vnet_resource_group, vnet_name, subnet_name) if subnet_obj.resource_navigation_links or subnet_obj.service_association_links: - raise CLIError('Subnet is not empty.') + raise ValidationError('Subnet is not empty.') def _validate_subnet_size(cli_ctx, subnet_id): @@ -225,8 +226,11 @@ def _validate_subnet_size(cli_ctx, subnet_id): address = subnet_obj.address_prefix size = int(address[address.index('/') + 1:]) if size > 24: - raise CLIError('Subnet size could cause scaling issues. Recommended size is at least /24. ' - 'Use --ignore-subnet-size-validation to skip size test.') + err_msg = 'Subnet size could cause scaling issues. Recommended size is at least /24.' + rec_msg = 'Use --ignore-subnet-size-validation to skip size test.' + validation_error = ValidationError(err_msg) + validation_error.set_recommendation(rec_msg) + raise validation_error def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): @@ -247,8 +251,11 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): in_subnet_name, subnet_parameters=in_subnet_obj) LongRunningOperation(cli_ctx)(poller) except Exception: - raise CLIError('Inbound subnet must have Private Endpoint Network Policy disabled.' - 'Use: az network vnet subnet update --disable-private-endpoint-network-policies') + err_msg = 'Inbound subnet must have Private Endpoint Network Policy disabled.' + rec_msg = 'Use: az network vnet subnet update --disable-private-endpoint-network-policies' + validation_error = ValidationError(err_msg) + validation_error.set_recommendation(rec_msg) + raise validation_error # Outbound out_subnet_id_parts = parse_resource_id(outbound_subnet_id) @@ -257,7 +264,7 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): out_subnet_name = out_subnet_id_parts['resource_name'] out_subnet_obj = network_client.subnets.get(out_vnet_resource_group, out_vnet_name, out_subnet_name) if out_subnet_obj.resource_navigation_links: - raise CLIError('Outbound subnet is not empty.') + raise ValidationError('Outbound subnet is not empty.') delegations = out_subnet_obj.delegations delegated = False @@ -266,7 +273,7 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): delegated = True if not delegated: - logger.warning('Adding delegation for hostingEnvironments to outbound subnet.') + logger.warning('Adding delegation for Microsoft.Web/hostingEnvironments to outbound subnet.') out_subnet_obj.delegations = [Delegation(name="delegation", service_name="Microsoft.Web/hostingEnvironments")] try: poller = network_client.subnets.begin_create_or_update( @@ -274,8 +281,11 @@ def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): out_subnet_name, subnet_parameters=out_subnet_obj) LongRunningOperation(cli_ctx)(poller) except Exception: - raise CLIError('Outbound subnet must be delegated to Microsoft.Web/hostingEnvironments. ' - 'Use: az network vnet subnet update --delegations "Microsoft.Web/hostingEnvironments"') + err_msg = 'Outbound subnet must be delegated to Microsoft.Web/hostingEnvironments.' + rec_msg = 'Use: az network vnet subnet update --delegations "Microsoft.Web/hostingEnvironments"' + validation_error = ValidationError(err_msg) + validation_error.set_recommendation(rec_msg) + raise validation_error def _ensure_route_table(cli_ctx, resource_group_name, ase_name, location, subnet_id, force): @@ -321,9 +331,12 @@ def _ensure_route_table(cli_ctx, resource_group_name, ase_name, location, subnet route_table_id_parts = parse_resource_id(subnet_obj.route_table.id) rt_name = route_table_id_parts['name'] if rt_name.lower() != ase_route_table_name.lower(): - raise CLIError('Route table already exists. \ - Use --ignore-route-table to use existing route table. \ - Use --force-route-table to replace existing route table') + err_msg = 'Route table already exists.' + rec_msg = 'Use --ignore-route-table to use existing route table ' \ + 'or --force-route-table to replace existing route table' + validation_error = ValidationError(err_msg) + validation_error.set_recommendation(rec_msg) + raise validation_error def _ensure_network_security_group(cli_ctx, resource_group_name, ase_name, location, subnet_id, force): @@ -413,9 +426,12 @@ def _ensure_network_security_group(cli_ctx, resource_group_name, ase_name, locat nsg_id_parts = parse_resource_id(subnet_obj.network_security_group.id) nsg_name = nsg_id_parts['name'] if nsg_name.lower() != ase_nsg_name.lower(): - raise CLIError('Network Security Group already exists. \ - Use --ignore-network-security-group to use existing NSG. \ - Use --force-network-security-group to replace existing NSG') + err_msg = 'Network Security Group already exists.' + rec_msg = 'Use --ignore-network-security-group to use existing NSG ' \ + 'or --force-network-security-group to replace existing NSG' + validation_error = ValidationError(err_msg) + validation_error.set_recommendation(rec_msg) + raise validation_error def _get_unique_deployment_name(prefix): 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 c04fc81f248..b908844c926 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1720,11 +1720,9 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p # the api is odd on parameter naming, have to live with it for now sku_def = SkuDescription(tier=get_sku_name(sku), name=sku, capacity=number_of_workers) - plan_def = AppServicePlan(location=location, tags=tags, sku=sku_def, reserved=(is_linux or None), hyper_v=(hyper_v or None), name=name, per_site_scaling=per_site_scaling, hosting_environment_profile=ase_def) - return sdk_no_wait(no_wait, client.app_service_plans.create_or_update, name=name, resource_group_name=resource_group_name, app_service_plan=plan_def) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py index 520c498ea14..954caada7d9 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py @@ -8,7 +8,7 @@ import unittest import mock -from knack.util import CLIError +from azure.cli.core.azclierror import ValidationError from azure.mgmt.web import WebSiteManagementClient @@ -103,8 +103,8 @@ def test_app_service_environment_create(self, ase_client_factory_mock, network_c subnet = Subnet(id=1, address_prefix='10.10.10.10/25') network_client.subnets.get.return_value = subnet - # assert that CLIError raised when called with small subnet - with self.assertRaises(CLIError): + # assert that ValidationError raised when called with small subnet + with self.assertRaises(ValidationError): create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, subnet=subnet_name, vnet_name=vnet_name, ignore_network_security_group=True, ignore_route_table=True, @@ -205,7 +205,7 @@ def test_app_service_environment_v3_create(self, ase_client_factory_mock, networ network_client_factory_mock.return_value = network_client subnet = Subnet(id=1, address_prefix='10.10.10.10/24') - hosting_delegation = Delegation(id=1, service_name='Microsoft.Web/hostingEnvironment') + hosting_delegation = Delegation(id=1, service_name='Microsoft.Web/hostingEnvironments') subnet.delegations = [hosting_delegation] network_client.subnets.get.return_value = subnet create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, From 8697310928cdf8d84729d06aa275a212902b1e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Thu, 14 Jan 2021 00:24:45 +0100 Subject: [PATCH 7/9] Restructure to isolate private endpoint logic --- .../cli/command_modules/appservice/_help.py | 14 +- .../cli/command_modules/appservice/_params.py | 8 +- .../appservice/appservice_environment.py | 330 +++++++++--------- .../command_modules/appservice/commands.py | 1 + .../cli/command_modules/appservice/custom.py | 12 +- ..._service_environment_commands_thru_mock.py | 3 +- 6 files changed, 195 insertions(+), 173 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index 8f389ac4fb1..058812200c3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -2186,8 +2186,18 @@ az network vnet subnet create -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\ --name Outbound --address-prefixes 10.0.1.0/24 - az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup --vnet-name MyASEv3VirtualNetwork \\ - --subnet Outbound --inbound-subnet Inbound --kind asev3 + az appservice ase create -n MyASEv3Name -g ASEv3ResourceGroup \\ + --vnet-name MyASEv3VirtualNetwork --subnet Outbound --kind asev3 +""" + +helps['appservice ase create-inbound-services'] = """ + type: command + short-summary: Creates the inbound services needed in preview for ASEv3 (private endpoint and dns). + examples: + - name: Create private endpoint, Private DNS Zone, A records and ensure subnet network policy. + text: | + az appservice ase create-inbound-services -n MyASEv3Name -g ASEv3ResourceGroup \\ + --vnet-name MyASEv3VirtualNetwork --subnet Inbound """ 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 929f7b60741..228841d5b5e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -891,8 +891,6 @@ def load_arguments(self, _): help='Scale of front ends to app service plan instance ratio.', default=15) c.argument('front_end_sku', arg_type=isolated_sku_arg_type, default='I1', help='Size of front end servers.') - c.argument('inbound_subnet', help='Required for ASEv3. Name or ID of existing subnet. \ - To create vnet and/or subnet use `az network vnet [subnet] create`') with self.argument_context('appservice ase delete') as c: c.argument('name', options_list=['--name', '-n'], help='Name of the app service environment') with self.argument_context('appservice ase update') as c: @@ -907,6 +905,12 @@ def load_arguments(self, _): with self.argument_context('appservice ase list-plans') as c: c.argument('name', options_list=['--name', '-n'], help='Name of the app service environment', local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.GET])) + with self.argument_context('appservice ase create-inbound-services') as c: + c.argument('name', options_list=['--name', '-n'], help='Name of the app service environment', + local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.GET])) + c.argument('subnet', help='Name or ID of existing subnet for inbound traffic to ASEv3. \ + To create vnet and/or subnet use `az network vnet [subnet] create`') + c.argument('vnet_name', help='Name of the vNet. Mandatory if only subnet name is specified.') # App Service Domain Commands with self.argument_context('appservice domain create') as c: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index 4d6efd44c8f..6261e03ce45 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -7,10 +7,13 @@ from azure.mgmt.network import NetworkManagementClient from azure.mgmt.web import WebSiteManagementClient from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.privatedns import PrivateDnsManagementClient # Models -from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation) -from azure.mgmt.resource.resources.models import (DeploymentProperties, Deployment) +from azure.mgmt.network.models import (RouteTable, Route, NetworkSecurityGroup, SecurityRule, Delegation, + PrivateEndpoint, Subnet, PrivateLinkServiceConnection) +from azure.mgmt.resource.resources.models import (DeploymentProperties, Deployment, SubResource) +from azure.mgmt.privatedns.models import (PrivateZone, VirtualNetworkLink, RecordSet, ARecord) # Utils from azure.cli.core.commands import LongRunningOperation @@ -42,7 +45,7 @@ def show_appserviceenvironment(cmd, name, resource_group_name=None): return ase_client.get(resource_group_name, name) -def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='ASEv2', inbound_subnet=None, +def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kind='ASEv2', vnet_name=None, ignore_route_table=False, ignore_network_security_group=False, virtual_ip_type='Internal', front_end_scale_factor=None, front_end_sku=None, force_route_table=False, @@ -56,33 +59,25 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name) deployment_name = _get_unique_deployment_name('cli_ase_deploy_') + _validate_subnet_empty(cmd.cli_ctx, subnet_id) + if not ignore_subnet_size_validation: + _validate_subnet_size(cmd.cli_ctx, subnet_id) if kind == 'ASEv2': - _validate_subnet_empty(cmd.cli_ctx, subnet_id) - if not ignore_subnet_size_validation: - _validate_subnet_size(cmd.cli_ctx, subnet_id) if not ignore_route_table: _ensure_route_table(cmd.cli_ctx, resource_group_name, name, location, subnet_id, force_route_table) if not ignore_network_security_group: _ensure_network_security_group(cmd.cli_ctx, resource_group_name, name, location, subnet_id, force_network_security_group) - ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, subnet_id=subnet_id, virtual_ip_type=virtual_ip_type, front_end_scale_factor=front_end_scale_factor, - front_end_sku=front_end_sku, tags=None) + front_end_sku=front_end_sku) elif kind == 'ASEv3': - inbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, inbound_subnet, vnet_name, resource_group_name) - if not ignore_subnet_size_validation: - _validate_subnet_size(cmd.cli_ctx, subnet_id) - _ensure_subnets_asev3(cmd.cli_ctx, inbound_subnet_id, subnet_id) - - ase_deployment_properties = _build_asev3_deployment_properties(name=name, location=location, - inbound_subnet_id=inbound_subnet_id, - outbound_subnet_id=subnet_id, tags=None) - + ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, + subnet_id=subnet_id, kind='ASEv3') logger.info('Create App Service Environment...') deployment_client = _get_resource_client_factory(cmd.cli_ctx).deployments return sdk_no_wait(no_wait, deployment_client.create_or_update, @@ -125,7 +120,7 @@ def list_appserviceenvironment_addresses(cmd, name, resource_group_name=None): resource_group_name = _get_resource_group_name_from_ase(ase_client, name) ase = ase_client.get(resource_group_name, name) if ase.kind.lower() == 'asev3': - raise CommandNotFoundError('list-addresses is currently not supported for ASEv3. ' \ + raise CommandNotFoundError('list-addresses is currently not supported for ASEv3. ' 'Inbound IP is associated with the private endpoint.') return ase_client.get_vip_info(resource_group_name, name) @@ -137,6 +132,61 @@ def list_appserviceenvironment_plans(cmd, name, resource_group_name=None): return ase_client.list_app_service_plans(resource_group_name, name) +def create_asev3_inbound_services(cmd, resource_group_name, name, subnet, vnet_name=None): + ase_client = _get_ase_client_factory(cmd.cli_ctx) + ase = ase_client.get(resource_group_name, name) + if not ase: + raise ResourceNotFoundError("App Service Environment '{}' not found.".format(name)) + if ase.kind.lower() != 'asev3': + raise CommandNotFoundError('Only ASEv3 support private endpoint connections') + + inbound_subnet_id = _validate_subnet_id(cmd.cli_ctx, subnet, vnet_name, resource_group_name) + inbound_vnet_id = _get_vnet_id_from_subnet(cmd.cli_ctx, inbound_subnet_id) + _ensure_subnet_private_endpoint_network_policy(cmd.cli_ctx, inbound_subnet_id, False) + + # Private Endpoint + network_client = _get_network_client_factory(cmd.cli_ctx) + pls_connection = PrivateLinkServiceConnection(private_link_service_id=ase.id, + group_ids=['hostingEnvironments'], + request_message='Link from CLI', + name='{}-private-connection'.format(name)) + private_endpoint = PrivateEndpoint( + location=ase.location, + tags=None, + subnet=Subnet(id=inbound_subnet_id) + ) + + private_endpoint.private_link_service_connections = [pls_connection] + + poller = network_client.private_endpoints.begin_create_or_update(resource_group_name, + '{}-private-endpoint'.format(name), + private_endpoint) + LongRunningOperation(cmd.cli_ctx)(poller) + ase_pe = poller.result() + nic_name = parse_resource_id(ase_pe.network_interfaces[0].id)['name'] + nic = network_client.network_interfaces.get(resource_group_name, nic_name) + + # Private DNS Zone + private_dns_client = _get_private_dns_client_factory(cmd.cli_ctx) + zone_name = '{}.appserviceenvironment.net'.format(name) + zone = PrivateZone(location='global', tags=None) + poller = private_dns_client.private_zones.create_or_update(resource_group_name, zone_name, zone, if_none_match='*') + LongRunningOperation(cmd.cli_ctx)(poller) + + link_name = '{}_link'.format(name) + link = VirtualNetworkLink(location='global', tags=None) + link.virtual_network = SubResource(id=inbound_vnet_id) + link.registration_enabled = False + private_dns_client.virtual_network_links.create_or_update(resource_group_name, zone_name, + link_name, link, if_none_match='*') + ase_record = ARecord(ipv4_address=nic.ip_configurations[0].private_ip_address) + record_set = RecordSet(ttl=3600) + record_set.a_records = [ase_record] + private_dns_client.record_sets.create_or_update(resource_group_name, zone_name, 'a', '*', record_set) + private_dns_client.record_sets.create_or_update(resource_group_name, zone_name, 'a', '@', record_set) + private_dns_client.record_sets.create_or_update(resource_group_name, zone_name, 'a', '*.scm', record_set) + + def _get_ase_client_factory(cli_ctx): client = get_mgmt_service_client(cli_ctx, WebSiteManagementClient).app_service_environments return client @@ -160,6 +210,11 @@ def _get_network_client_factory(cli_ctx, api_version=None): return client +def _get_private_dns_client_factory(cli_ctx): + client = get_mgmt_service_client(cli_ctx, PrivateDnsManagementClient) + return client + + def _get_location_from_resource_group(cli_ctx, resource_group_name): resource_group_client = _get_resource_client_factory(cli_ctx).resource_groups group = resource_group_client.get(resource_group_name) @@ -176,7 +231,7 @@ def _get_resource_group_name_from_ase(ase_client, ase_name): ase_found = True break if not ase_found: - raise ResourceNotFoundError("App service environment '{}' not found in subscription.".format(ase_name)) + raise ResourceNotFoundError("App Service Environment '{}' not found in subscription.".format(ase_name)) return resource_group @@ -196,6 +251,18 @@ def _validate_subnet_id(cli_ctx, subnet, vnet_name, resource_group_name): raise MutuallyExclusiveArgumentError('Please specify either: --subnet ID or (--subnet NAME and --vnet-name NAME)') +def _get_vnet_id_from_subnet(cli_ctx, subnet_id): + subnet_id_parts = parse_resource_id(subnet_id) + vnet_resource_group = subnet_id_parts['resource_group'] + vnet_name = subnet_id_parts['name'] + return resource_id( + subscription=get_subscription_id(cli_ctx), + resource_group=vnet_resource_group, + namespace='Microsoft.Network', + type='virtualNetworks', + name=vnet_name) + + def _map_worker_sku(sku_name): switcher = { 'I1': 'Standard_D1_V2', @@ -233,56 +300,52 @@ def _validate_subnet_size(cli_ctx, subnet_id): raise validation_error -def _ensure_subnets_asev3(cli_ctx, inbound_subnet_id, outbound_subnet_id): +def _ensure_subnet_private_endpoint_network_policy(cli_ctx, subnet_id, network_policy_enabled): network_client = _get_network_client_factory(cli_ctx) + subnet_id_parts = parse_resource_id(subnet_id) + vnet_resource_group = subnet_id_parts['resource_group'] + vnet_name = subnet_id_parts['name'] + subnet_name = subnet_id_parts['resource_name'] + subnet_obj = network_client.subnets.get(vnet_resource_group, vnet_name, subnet_name) + target_state = 'Enabled' if network_policy_enabled else 'Disabled' - # Inbound - in_subnet_id_parts = parse_resource_id(inbound_subnet_id) - in_vnet_resource_group = in_subnet_id_parts['resource_group'] - in_vnet_name = in_subnet_id_parts['name'] - in_subnet_name = in_subnet_id_parts['resource_name'] - in_subnet_obj = network_client.subnets.get(in_vnet_resource_group, in_vnet_name, in_subnet_name) - if in_subnet_obj.private_endpoint_network_policies == 'Enabled': - logger.warning('Disabling inbound subnet Private Endpoint Network Policy setting.') - in_subnet_obj.private_endpoint_network_policies = 'Disabled' + if subnet_obj.private_endpoint_network_policies != target_state: + subnet_obj.private_endpoint_network_policies = target_state try: poller = network_client.subnets.begin_create_or_update( - in_vnet_resource_group, in_vnet_name, - in_subnet_name, subnet_parameters=in_subnet_obj) + vnet_resource_group, vnet_name, subnet_name, subnet_parameters=subnet_obj) LongRunningOperation(cli_ctx)(poller) except Exception: - err_msg = 'Inbound subnet must have Private Endpoint Network Policy disabled.' + err_msg = 'Subnet must have Private Endpoint Network Policy {}.'.format(target_state) rec_msg = 'Use: az network vnet subnet update --disable-private-endpoint-network-policies' validation_error = ValidationError(err_msg) validation_error.set_recommendation(rec_msg) raise validation_error - # Outbound - out_subnet_id_parts = parse_resource_id(outbound_subnet_id) - out_vnet_resource_group = out_subnet_id_parts['resource_group'] - out_vnet_name = out_subnet_id_parts['name'] - out_subnet_name = out_subnet_id_parts['resource_name'] - out_subnet_obj = network_client.subnets.get(out_vnet_resource_group, out_vnet_name, out_subnet_name) - if out_subnet_obj.resource_navigation_links: - raise ValidationError('Outbound subnet is not empty.') - delegations = out_subnet_obj.delegations +def _ensure_subnet_delegation(cli_ctx, subnet_id, delegation_service_name): + network_client = _get_network_client_factory(cli_ctx) + subnet_id_parts = parse_resource_id(subnet_id) + vnet_resource_group = subnet_id_parts['resource_group'] + vnet_name = subnet_id_parts['name'] + subnet_name = subnet_id_parts['resource_name'] + subnet_obj = network_client.subnets.get(vnet_resource_group, vnet_name, subnet_name) + + delegations = subnet_obj.delegations delegated = False for d in delegations: - if d.service_name.lower() == "microsoft.web/hostingenvironments": + if d.service_name.lower() == delegation_service_name.lower(): delegated = True if not delegated: - logger.warning('Adding delegation for Microsoft.Web/hostingEnvironments to outbound subnet.') - out_subnet_obj.delegations = [Delegation(name="delegation", service_name="Microsoft.Web/hostingEnvironments")] + subnet_obj.delegations = [Delegation(name="delegation", service_name=delegation_service_name)] try: poller = network_client.subnets.begin_create_or_update( - out_vnet_resource_group, out_vnet_name, - out_subnet_name, subnet_parameters=out_subnet_obj) + vnet_resource_group, vnet_name, subnet_name, subnet_parameters=subnet_obj) LongRunningOperation(cli_ctx)(poller) except Exception: - err_msg = 'Outbound subnet must be delegated to Microsoft.Web/hostingEnvironments.' - rec_msg = 'Use: az network vnet subnet update --delegations "Microsoft.Web/hostingEnvironments"' + err_msg = 'Subnet must be delegated to {}.'.format(delegation_service_name) + rec_msg = 'Use: az network vnet subnet update --delegations "{}"'.format(delegation_service_name) validation_error = ValidationError(err_msg) validation_error.set_recommendation(rec_msg) raise validation_error @@ -364,63 +427,14 @@ def _ensure_network_security_group(cli_ctx, resource_group_name, ase_name, locat ase_nsg_name, ase_nsg) LongRunningOperation(cli_ctx)(poller) - logger.info('Ensure Network Security Group Rules...') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-management', - 100, 'Used to manage ASE from public VIP', '*', 'Allow', 'Inbound', - '*', 'AppServiceManagement', '454-455', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-load-balancer-keep-alive', - 105, 'Allow communication to ASE from Load Balancer', '*', 'Allow', 'Inbound', - '*', 'AzureLoadBalancer', '16001', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'ASE-internal-inbound', - 110, 'ASE-internal-inbound', '*', 'Allow', 'Inbound', - '*', subnet_address_prefix, '*', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-HTTP', - 120, 'Allow HTTP', '*', 'Allow', 'Inbound', - '*', '*', '80', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-HTTPS', - 130, 'Allow HTTPS', '*', 'Allow', 'Inbound', - '*', '*', '443', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTP', - 140, 'Allow FTP', '*', 'Allow', 'Inbound', - '*', '*', '21', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTPS', - 150, 'Allow FTPS', '*', 'Allow', 'Inbound', - '*', '*', '990', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTP-Data', - 160, 'Allow FTP Data', '*', 'Allow', 'Inbound', - '*', '*', '10001-10020', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-Remote-Debugging', - 170, 'Visual Studio remote debugging', '*', 'Allow', 'Inbound', - '*', '*', '4016-4022', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-443', - 100, 'Azure Storage blob', '*', 'Allow', 'Outbound', - '*', '*', '443', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-DB', - 110, 'Database', '*', 'Allow', 'Outbound', - '*', '*', '1433', 'Sql') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-DNS', - 120, 'DNS', '*', 'Allow', 'Outbound', - '*', '*', '53', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'ASE-internal-outbound', - 130, 'Azure Storage queue', '*', 'Allow', 'Outbound', - '*', '*', '*', subnet_address_prefix) - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-80', - 140, 'Outbound 80', '*', 'Allow', 'Outbound', - '*', '*', '80', '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-monitor', - 150, 'Azure Monitor', '*', 'Allow', 'Outbound', - '*', '*', 12000, '*') - _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-NTP', - 160, 'Clock', '*', 'Allow', 'Outbound', - '*', '*', '123', '*') + _create_asev2_nsg_rules(cli_ctx, resource_group_name, ase_nsg_name, subnet_address_prefix) nsg = network_client.network_security_groups.get(resource_group_name, ase_nsg_name) if not subnet_obj.network_security_group or subnet_obj.network_security_group.id != nsg.id: logger.info('Associate Network Security Group with Subnet...') subnet_obj.network_security_group = NetworkSecurityGroup(id=nsg.id) poller = network_client.subnets.begin_create_or_update( - vnet_resource_group, vnet_name, - subnet_name, subnet_parameters=subnet_obj) + vnet_resource_group, vnet_name, subnet_name, subnet_parameters=subnet_obj) LongRunningOperation(cli_ctx)(poller) else: nsg_id_parts = parse_resource_id(subnet_obj.network_security_group.id) @@ -429,8 +443,7 @@ def _ensure_network_security_group(cli_ctx, resource_group_name, ase_name, locat err_msg = 'Network Security Group already exists.' rec_msg = 'Use --ignore-network-security-group to use existing NSG ' \ 'or --force-network-security-group to replace existing NSG' - validation_error = ValidationError(err_msg) - validation_error.set_recommendation(rec_msg) + validation_error = ValidationError(err_msg, rec_msg) raise validation_error @@ -438,8 +451,8 @@ def _get_unique_deployment_name(prefix): return prefix + random_string(16) -def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type, - front_end_scale_factor=None, front_end_sku=None, tags=None): +def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type=None, + front_end_scale_factor=None, front_end_sku=None, tags=None, kind='ASEv2'): # InternalLoadBalancingMode Enum: None 0, Web 1, Publishing 2. # External: 0 (None), Internal: 3 (Web + Publishing) ilb_mode = 3 if virtual_ip_type == 'Internal' else 0 @@ -461,8 +474,8 @@ def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type, 'name': name, 'type': 'Microsoft.Web/hostingEnvironments', 'location': location, - 'apiVersion': '2019-02-01', - 'kind': 'ASEV2', + 'apiVersion': '2019-08-01', + 'kind': kind, 'tags': tags, 'properties': ase_properties } @@ -477,63 +490,56 @@ def _build_ase_deployment_properties(name, location, subnet_id, virtual_ip_type, return deployment -def _build_asev3_deployment_properties(name, location, inbound_subnet_id, outbound_subnet_id, tags=None): - ase_properties = { - "location": location, - "virtualNetwork": { - "id": outbound_subnet_id - } - } - - ase_resource = { - "name": name, - "type": "Microsoft.Web/hostingEnvironments", - "location": location, - "apiVersion": "2019-08-01", - "kind": "ASEV3", - "tags": tags, - "properties": ase_properties - } - - privateLinkConnectionName = '{}-privateLink'.format(name) - pe_properties = { - "subnet": { - "id": inbound_subnet_id - }, - "privateLinkServiceConnections": [ - { - "name": privateLinkConnectionName, - "properties": { - "privateLinkServiceId": "[resourceId('Microsoft.Web/hostingEnvironments', '{}')]".format(name), - "groupIds": [ - "hostingEnvironments" - ] - } - } - ] - } - - privateEndpointName = '{}-privateEndpoint'.format(name) - pe_resource = { - "name": privateEndpointName, - "type": "Microsoft.Network/privateEndpoints", - "location": location, - "apiVersion": "2019-04-01", - "dependsOn": [ - "[resourceId('Microsoft.Web/hostingEnvironments', '{}')]".format(name) - ], - "properties": pe_properties - } - - deployment_template = ArmTemplateBuilder() - deployment_template.add_resource(ase_resource) - deployment_template.add_resource(pe_resource) - template = deployment_template.build() - parameters = deployment_template.build_parameters() - - deploymentProperties = DeploymentProperties(template=template, parameters=parameters, mode='Incremental') - deployment = Deployment(properties=deploymentProperties) - return deployment +def _create_asev2_nsg_rules(cli_ctx, resource_group_name, ase_nsg_name, subnet_address_prefix): + logger.info('Ensure Network Security Group Rules...') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-management', + 100, 'Used to manage ASE from public VIP', '*', 'Allow', 'Inbound', + '*', 'AppServiceManagement', '454-455', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-load-balancer-keep-alive', + 105, 'Allow communication to ASE from Load Balancer', '*', 'Allow', 'Inbound', + '*', 'AzureLoadBalancer', '16001', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'ASE-internal-inbound', + 110, 'ASE-internal-inbound', '*', 'Allow', 'Inbound', + '*', subnet_address_prefix, '*', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-HTTP', + 120, 'Allow HTTP', '*', 'Allow', 'Inbound', + '*', '*', '80', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-HTTPS', + 130, 'Allow HTTPS', '*', 'Allow', 'Inbound', + '*', '*', '443', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTP', + 140, 'Allow FTP', '*', 'Allow', 'Inbound', + '*', '*', '21', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTPS', + 150, 'Allow FTPS', '*', 'Allow', 'Inbound', + '*', '*', '990', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-FTP-Data', + 160, 'Allow FTP Data', '*', 'Allow', 'Inbound', + '*', '*', '10001-10020', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Inbound-Remote-Debugging', + 170, 'Visual Studio remote debugging', '*', 'Allow', 'Inbound', + '*', '*', '4016-4022', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-443', + 100, 'Azure Storage blob', '*', 'Allow', 'Outbound', + '*', '*', '443', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-DB', + 110, 'Database', '*', 'Allow', 'Outbound', + '*', '*', '1433', 'Sql') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-DNS', + 120, 'DNS', '*', 'Allow', 'Outbound', + '*', '*', '53', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'ASE-internal-outbound', + 130, 'Azure Storage queue', '*', 'Allow', 'Outbound', + '*', '*', '*', subnet_address_prefix) + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-80', + 140, 'Outbound 80', '*', 'Allow', 'Outbound', + '*', '*', '80', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-monitor', + 150, 'Azure Monitor', '*', 'Allow', 'Outbound', + '*', '*', '12000', '*') + _create_nsg_rule(cli_ctx, resource_group_name, ase_nsg_name, 'Outbound-NTP', + 160, 'Clock', '*', 'Allow', 'Outbound', + '*', '*', '123', '*') def _create_nsg_rule(cli_ctx, resource_group_name, network_security_group_name, security_rule_name, diff --git a/src/azure-cli/azure/cli/command_modules/appservice/commands.py b/src/azure-cli/azure/cli/command_modules/appservice/commands.py index 1c51f187c8b..94a13ae7341 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -416,6 +416,7 @@ def load_command_table(self, _): g.custom_command('create', 'create_appserviceenvironment_arm', supports_no_wait=True) g.custom_command('update', 'update_appserviceenvironment', supports_no_wait=True) g.custom_command('delete', 'delete_appserviceenvironment', supports_no_wait=True, confirmation=True) + g.custom_command('create-inbound-services', 'create_asev3_inbound_services', is_preview=True) with self.command_group('appservice domain', custom_command_type=appservice_domains, is_preview=True) as g: g.custom_command('create', 'create_domain') 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 b908844c926..edc156da3b7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -47,7 +47,8 @@ from azure.cli.core.util import get_az_user_agent, send_raw_request from azure.cli.core.profiles import ResourceType, get_sdk from azure.cli.core.azclierror import (ResourceNotFoundError, RequiredArgumentMissingError, ValidationError, - CLIInternalError, UnclassifiedUserFault, AzureResponseError) + CLIInternalError, UnclassifiedUserFault, AzureResponseError, + ArgumentUsageError, MutuallyExclusiveArgumentError) from .tunnel import TunnelServer @@ -1696,23 +1697,24 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p sku = _normalize_sku(sku) _validate_asp_sku(app_service_environment, sku) if is_linux and hyper_v: - raise CLIError('usage error: --is-linux | --hyper-v') + raise MutuallyExclusiveArgumentError('Usage error: --is-linux and --hyper-v cannot be used together.') client = web_client_factory(cmd.cli_ctx) if app_service_environment: if hyper_v: - raise CLIError('Windows containers is not yet supported in app service environment') + raise ArgumentUsageError('Windows containers is not yet supported in app service environment') ase_list = client.app_service_environments.list() ase_found = False ase = None for ase in ase_list: - if ase.name.lower() == app_service_environment.lower(): + if ase.name.lower() == app_service_environment.lower() or ase.id.lower() == app_service_environment.lower(): ase_def = HostingEnvironmentProfile(id=ase.id) location = ase.location ase_found = True break if not ase_found: - raise CLIError("App service environment '{}' not found in subscription.".format(ase.id)) + err_msg = "App service environment '{}' not found in subscription.".format(app_service_environment) + raise ResourceNotFoundError(err_msg) else: # Non-ASE ase_def = None if location is None: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py index 954caada7d9..af03dfae026 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_app_service_environment_commands_thru_mock.py @@ -190,7 +190,6 @@ def test_app_service_environment_v3_create(self, ase_client_factory_mock, networ rg_name = 'mock_rg_name' vnet_name = 'mock_vnet_name' subnet_name = 'mock_subnet_name' - inbound_subnet_name = 'mock_inbound_subnet_name' deployment_name = 'mock_deployment_name' ase_client = mock.MagicMock() @@ -210,7 +209,7 @@ def test_app_service_environment_v3_create(self, ase_client_factory_mock, networ network_client.subnets.get.return_value = subnet create_appserviceenvironment_arm(self.mock_cmd, resource_group_name=rg_name, name=ase_name, subnet=subnet_name, vnet_name=vnet_name, kind='ASEv3', - inbound_subnet=inbound_subnet_name, location='westeurope') + location='westeurope') # Assert create_or_update is called with correct rg and deployment name resource_client_mock.deployments.create_or_update.assert_called_once() From 3b28b2715bfd4d6c38ee688eca7438c4c036fc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Fri, 15 Jan 2021 07:11:02 +0100 Subject: [PATCH 8/9] Update src/azure-cli/azure/cli/command_modules/appservice/_help.py Co-authored-by: Feiyue Yu --- src/azure-cli/azure/cli/command_modules/appservice/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index 058812200c3..d22ecaad137 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -2192,7 +2192,7 @@ helps['appservice ase create-inbound-services'] = """ type: command - short-summary: Creates the inbound services needed in preview for ASEv3 (private endpoint and dns). + short-summary: Create the inbound services needed in preview for ASEv3 (private endpoint and DNS). examples: - name: Create private endpoint, Private DNS Zone, A records and ensure subnet network policy. text: | From 36b200268d446a32c7e4cb405fe000d6d758eb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20Damg=C3=A5rd?= Date: Fri, 15 Jan 2021 12:13:59 +0100 Subject: [PATCH 9/9] Add service delegation --- .../cli/command_modules/appservice/appservice_environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py index 6261e03ce45..a87d089342b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/appservice_environment.py @@ -76,6 +76,7 @@ def create_appserviceenvironment_arm(cmd, resource_group_name, name, subnet, kin front_end_sku=front_end_sku) elif kind == 'ASEv3': + _ensure_subnet_delegation(cmd.cli_ctx, subnet_id, 'Microsoft.Web/hostingEnvironments') ase_deployment_properties = _build_ase_deployment_properties(name=name, location=location, subnet_id=subnet_id, kind='ASEv3') logger.info('Create App Service Environment...')