From 8e36b7a8cc9be2e079cc41719a4373a76918a8eb Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 10 Feb 2026 14:40:08 +0530 Subject: [PATCH 01/24] {Site} Add quickstart command and ARM template for Site + Config deployment --- src/site/azext_site/_help.py | 10 + .../azext_site/aaz/latest/site/__init__.py | 1 + .../azext_site/aaz/latest/site/_quickstart.py | 103 ++++++++++ src/site/azext_site/templates/infra/main.json | 181 ++++++++++++++++++ src/site/setup.cfg | 6 +- 5 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/site/azext_site/aaz/latest/site/_quickstart.py create mode 100644 src/site/azext_site/templates/infra/main.json diff --git a/src/site/azext_site/_help.py b/src/site/azext_site/_help.py index 126d5d00714..e71f34058f2 100644 --- a/src/site/azext_site/_help.py +++ b/src/site/azext_site/_help.py @@ -9,3 +9,13 @@ # pylint: disable=too-many-lines from knack.help_files import helps # pylint: disable=unused-import + +helps['site quickstart'] = """ +type: command +short-summary: Quickstart deploy Site + Config + ConfigRef using an internal ARM template. +examples: + - name: Resource group scope + text: az site quickstart --name MySite01 --defaultconfiguration -g MyRG -l eastus + - name: Subscription scope + text: az site quickstart --name MySite01 --defaultconfiguration -l eastus +""" diff --git a/src/site/azext_site/aaz/latest/site/__init__.py b/src/site/azext_site/aaz/latest/site/__init__.py index c401f439385..e2ef26b7f37 100644 --- a/src/site/azext_site/aaz/latest/site/__init__.py +++ b/src/site/azext_site/aaz/latest/site/__init__.py @@ -14,3 +14,4 @@ from ._list import * from ._show import * from ._update import * +from ._quickstart import * \ No newline at end of file diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py new file mode 100644 index 00000000000..466da7b9d41 --- /dev/null +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -0,0 +1,103 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from pathlib import Path +from azure.cli.core.aaz import ( # type: ignore[import-unresolved] + AAZCommand, + AAZStrArg, + AAZBoolArg, + AAZResourceGroupNameArg, + has_value, + register_command, +) +from azure.cli.core.azclierror import ( # type: ignore[import-unresolved] + InvalidArgumentValueError, + FileOperationError, + CLIInternalError, +) +from azure.cli.core import get_default_cli # type: ignore[import-unresolved] + + +def _resolve_template_path() -> Path: + # ...\azext_site\aaz\latest\site\_quickstart.py -> ...\azext_site\templates\infra\main.json + azext_root = Path(__file__).resolve().parents[3] # ...\azext_site + return azext_root / "templates" / "infra" / "main.json" + + +@register_command("site quickstart") +class Quickstart(AAZCommand): + """Quickstart: deploy internal ARM template to create Site + Config + ConfigRef.""" + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + _args_schema = cls._args_schema + + _args_schema.name = AAZStrArg( + options=["-n", "--name"], + required=True, + help="Site name (siteName).", + ) + + _args_schema.defaultconfiguration = AAZBoolArg( + options=["--defaultconfiguration", "--default-configuration"], + help="Trigger the internal ARM template flow (Site + Config + ConfigRef).", + ) + + _args_schema.resource_group = AAZResourceGroupNameArg( + options=["-g", "--resource-group"], + required=True, + help="Resource group for deployment.", + ) + + _args_schema.config_name = AAZStrArg( + options=["--config-name"], + help="Optional configName override. Default in template: '-configuration'.", + ) + + return cls._args_schema + + def _handler(self, command_args): + super()._handler(command_args) + + if not self.ctx.args.defaultconfiguration: + raise InvalidArgumentValueError("Specify --defaultconfiguration to run quickstart.") + + return self.handle() + + def handle(self): + template = _resolve_template_path() + if not template.exists(): + raise FileOperationError(f"Internal ARM template not found: {template}") + + site_name = self.ctx.args.name.to_serialized_data() + rg = self.ctx.args.resource_group.to_serialized_data() + + deployment_name = f"site-quickstart-{site_name}" + + invoke_args = [ + "deployment", "group", "create", + "--name", deployment_name, + "--resource-group", rg, + "--template-file", str(template), + "--parameters", + f"siteName={site_name}", + ] + + if has_value(self.ctx.args.config_name): + cfg = self.ctx.args.config_name.to_serialized_data() + invoke_args.append(f"configName={cfg}") + + cli = get_default_cli() + rc = cli.invoke(invoke_args) + if rc != 0: + raise CLIInternalError("ARM deployment failed for site quickstart.") + + return cli.result.result diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json new file mode 100644 index 00000000000..1ca18606a3a --- /dev/null +++ b/src/site/azext_site/templates/infra/main.json @@ -0,0 +1,181 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteApiVersion": { + "type": "string", + "defaultValue": "2025-06-01" + }, + "configApiVersion": { + "type": "string", + "defaultValue": "2025-06-01" + }, + "configChildApiVersion": { + "type": "string", + "defaultValue": "2024-09-01-preview" + }, + "configRefApiVersion": { + "type": "string", + "defaultValue": "2025-06-01" + }, + "siteName": { + "type": "string" + }, + "description": { + "type": "string", + "defaultValue": "" + }, + "labels": { + "type": "object", + "defaultValue": {} + }, + "siteAddress": { + "type": "object", + "defaultValue": { + "streetAddress1": "", + "streetAddress2": "", + "city": "", + "stateOrProvince": "", + "country": "", + "postalCode": "" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "configName": { + "type": "string", + "defaultValue": "[concat(parameters('siteName'), '-configuration')]" + }, + "connectivityConfigName": { + "type": "string", + "defaultValue": "connectivityConfig1" + }, + "secretConfigName": { + "type": "string", + "defaultValue": "secretconfig1" + }, + "networkConfigName": { + "type": "string", + "defaultValue": "networkConfig1" + }, + "networkConfigurationKind": { + "type": "string", + "defaultValue": "LAN" + }, + "tsConfigName": { + "type": "string", + "defaultValue": "tsconfig1" + }, + "timeServerConfiguration": { + "type": "object", + "defaultValue": {} + }, + "connectivityConfiguration": { + "type": "object", + "defaultValue": {} + }, + "securityConfiguration": { + "type": "object", + "defaultValue": {} + }, + "networkConfiguration": { + "type": "object", + "defaultValue": { + "scenario": "Provisioning", + "ipAssignments": { + "ipAssignmentType": "Manual", + "ipv4": { + "addressRange": { + "startIp": "192.168.100.10", + "endIp": "192.168.100.50" + }, + "subnetMask": "255.255.255.0", + "defaultGateway": "192.168.100.1", + "dnsServers": [], + "vLanId": 0 + } + } + } + }, + "useArcGateway": { + "type": "bool", + "defaultValue": false + }, + "arcGatewayConfigName": { + "type": "string", + "defaultValue": "arcConfig1" + }, + "arcGatewayConfiguration": { + "type": "object", + "defaultValue": {} + } + }, + "variables": { + "siteId": "[resourceId('Microsoft.Edge/sites', parameters('siteName'))]", + "configId": "[resourceId('Microsoft.Edge/Configurations', parameters('configName'))]" + }, + "resources": [ + { + "type": "Microsoft.Edge/sites", + "apiVersion": "[parameters('siteApiVersion')]", + "name": "[parameters('siteName')]", + "properties": { + "displayName": "[parameters('siteName')]", + "description": "[parameters('description')]", + "siteAddress": { + "streetAddress1": "[parameters('siteAddress').streetAddress1]", + "streetAddress2": "[parameters('siteAddress').streetAddress2]", + "city": "[parameters('siteAddress').city]", + "stateOrProvince": "[parameters('siteAddress').stateOrProvince]", + "country": "[parameters('siteAddress').country]", + "postalCode": "[parameters('siteAddress').postalCode]" + }, + "labels": "[parameters('labels')]" + } + }, + { + "type": "Microsoft.Edge/Configurations", + "apiVersion": "[parameters('configApiVersion')]", + "name": "[parameters('configName')]", + "location": "[parameters('location')]", + "properties": {}, + "resources": [ + { + "type": "NetworkConfigurations", + "apiVersion": "[parameters('configChildApiVersion')]", + "name": "[parameters('networkConfigName')]", + "dependsOn": [ + "[resourceId('Microsoft.Edge/Configurations', parameters('configName'))]" + ], + "kind": "[parameters('networkConfigurationKind')]", + "properties": "[parameters('networkConfiguration')]" + } + ] + }, + { + "type": "Microsoft.Edge/configurationReferences", + "apiVersion": "[parameters('configRefApiVersion')]", + "name": "default", + "scope": "[variables('siteId')]", + "dependsOn": [ + "[variables('siteId')]", + "[variables('configId')]" + ], + "properties": { + "configurationResourceId": "[variables('configId')]" + } + } + ], + "outputs": { + "siteId": { + "type": "string", + "value": "[variables('siteId')]" + }, + "configId": { + "type": "string", + "value": "[variables('configId')]" + } + } +} \ No newline at end of file diff --git a/src/site/setup.cfg b/src/site/setup.cfg index 2fdd96e5d39..67b297a94f2 100644 --- a/src/site/setup.cfg +++ b/src/site/setup.cfg @@ -1 +1,5 @@ -#setup.cfg \ No newline at end of file +#setup.cfg + +[options.package_data] +azext_site = + templates\**\*.json \ No newline at end of file From 8c5a364b3c2bfd43e34d64a5a7b2c8feab241120 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 10 Feb 2026 15:01:52 +0530 Subject: [PATCH 02/24] {Site} Update help text for config-name argument in quickstart command --- src/site/azext_site/aaz/latest/site/_quickstart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 466da7b9d41..46f89c6dd7d 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -59,7 +59,7 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.config_name = AAZStrArg( options=["--config-name"], - help="Optional configName override. Default in template: '-configuration'.", + help="Optional configName override. Default in template: 'siteName-configuration'.", ) return cls._args_schema From 03b58cd8e0d6282acb779067383f5cd7c52e36f4 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 10 Feb 2026 15:14:06 +0530 Subject: [PATCH 03/24] {Site} Add location argument to quickstart command for deployment --- src/site/azext_site/aaz/latest/site/_quickstart.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 46f89c6dd7d..2c3257d4d83 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -53,9 +53,13 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.resource_group = AAZResourceGroupNameArg( options=["-g", "--resource-group"], - required=True, help="Resource group for deployment.", ) + + _args_schema.location = AAZResourceLocationArg( + options=["-l", "--location"], + help="Location for the deployment. Default: resource group location.", + ) _args_schema.config_name = AAZStrArg( options=["--config-name"], @@ -91,6 +95,10 @@ def handle(self): f"siteName={site_name}", ] + if has_value(self.ctx.args.location): + loc = self.ctx.args.location.to_serialized_data() + invoke_args.extend(["--parameters", f"location={loc}"]) + if has_value(self.ctx.args.config_name): cfg = self.ctx.args.config_name.to_serialized_data() invoke_args.append(f"configName={cfg}") From ca4783650f399fef783a854ca73bce5f8c56405d Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 10 Feb 2026 15:23:11 +0530 Subject: [PATCH 04/24] {Site} Add AAZResourceLocationArg import for quickstart command --- src/site/azext_site/aaz/latest/site/_quickstart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 2c3257d4d83..866a2fc76b7 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -11,6 +11,7 @@ AAZResourceGroupNameArg, has_value, register_command, + AAZResourceLocationArg ) from azure.cli.core.azclierror import ( # type: ignore[import-unresolved] InvalidArgumentValueError, From 5fa677c1055524c29161e4efd4ea0f1f173667f6 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 10 Feb 2026 15:27:02 +0530 Subject: [PATCH 05/24] {Site} Fix formatting in quickstart command argument definitions --- src/site/azext_site/aaz/latest/site/_quickstart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 866a2fc76b7..43962f5bcd8 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -56,7 +56,7 @@ def _build_arguments_schema(cls, *args, **kwargs): options=["-g", "--resource-group"], help="Resource group for deployment.", ) - + _args_schema.location = AAZResourceLocationArg( options=["-l", "--location"], help="Location for the deployment. Default: resource group location.", @@ -99,7 +99,7 @@ def handle(self): if has_value(self.ctx.args.location): loc = self.ctx.args.location.to_serialized_data() invoke_args.extend(["--parameters", f"location={loc}"]) - + if has_value(self.ctx.args.config_name): cfg = self.ctx.args.config_name.to_serialized_data() invoke_args.append(f"configName={cfg}") From b906a4723fbb5c858435729c701c921cea93d616 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Wed, 11 Feb 2026 10:36:57 +0530 Subject: [PATCH 06/24] {Site} Add argument formatting for site name in quickstart command --- src/site/azext_site/aaz/latest/site/_quickstart.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 43962f5bcd8..7b679d51eb8 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -7,6 +7,7 @@ from azure.cli.core.aaz import ( # type: ignore[import-unresolved] AAZCommand, AAZStrArg, + AAZStrArgFormat, AAZBoolArg, AAZResourceGroupNameArg, has_value, @@ -45,6 +46,11 @@ def _build_arguments_schema(cls, *args, **kwargs): options=["-n", "--name"], required=True, help="Site name (siteName).", + fmt=AAZStrArgFormat( + pattern=r"^[a-zA-Z0-9][a-zA-Z0-9-_.]{0,62}[a-zA-Z0-9]$", + min_length=2, + max_length=64, + ), ) _args_schema.defaultconfiguration = AAZBoolArg( From 1f3ca3e0dc24104b0203d07ba0294a12671e8829 Mon Sep 17 00:00:00 2001 From: akanksha020901 Date: Wed, 11 Feb 2026 10:41:05 +0530 Subject: [PATCH 07/24] Apply suggestion from @Copilot propagate the underlying error from az deployment group create Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/azext_site/aaz/latest/site/_quickstart.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 43962f5bcd8..85ed9bc1af7 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -107,6 +107,16 @@ def handle(self): cli = get_default_cli() rc = cli.invoke(invoke_args) if rc != 0: - raise CLIInternalError("ARM deployment failed for site quickstart.") + # Include deployment context and underlying CLI error (if any) for easier troubleshooting. + underlying_error = None + if getattr(cli, "result", None) is not None: + underlying_error = getattr(cli.result, "error", None) + msg = ( + "ARM deployment failed for site quickstart. " + f"Deployment name: {deployment_name}, resource group: {rg}." + ) + if underlying_error: + msg = f"{msg} Underlying error: {underlying_error}" + raise CLIInternalError(msg) return cli.result.result From aac1e8be4775e83aa71b29d5a0327aebc739b04b Mon Sep 17 00:00:00 2001 From: akanksha020901 Date: Wed, 11 Feb 2026 10:41:43 +0530 Subject: [PATCH 08/24] Apply suggestion from @Copilot removing unused params Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/azext_site/templates/infra/main.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index 1ca18606a3a..d48ceda6d0b 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -98,18 +98,6 @@ } } } - }, - "useArcGateway": { - "type": "bool", - "defaultValue": false - }, - "arcGatewayConfigName": { - "type": "string", - "defaultValue": "arcConfig1" - }, - "arcGatewayConfiguration": { - "type": "object", - "defaultValue": {} } }, "variables": { From 819a00ac4aac42675620710102b36feb5d421402 Mon Sep 17 00:00:00 2001 From: akanksha020901 Date: Wed, 11 Feb 2026 10:42:23 +0530 Subject: [PATCH 09/24] Apply suggestion from @Copilot adding -g property in this Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/azext_site/_help.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/site/azext_site/_help.py b/src/site/azext_site/_help.py index e71f34058f2..058091b3cfc 100644 --- a/src/site/azext_site/_help.py +++ b/src/site/azext_site/_help.py @@ -15,7 +15,5 @@ short-summary: Quickstart deploy Site + Config + ConfigRef using an internal ARM template. examples: - name: Resource group scope - text: az site quickstart --name MySite01 --defaultconfiguration -g MyRG -l eastus - - name: Subscription scope - text: az site quickstart --name MySite01 --defaultconfiguration -l eastus + text: az site quickstart --name MySite01 --defaultconfiguration -g MyRG """ From 1f3e672f774bef742aa370b8a6f7ee4068793951 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Mon, 16 Feb 2026 13:39:58 +0530 Subject: [PATCH 10/24] {Site} Add resourceGroupName parameter with default value in main.json --- src/site/azext_site/templates/infra/main.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index d48ceda6d0b..f59cc633844 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -21,6 +21,10 @@ "siteName": { "type": "string" }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, "description": { "type": "string", "defaultValue": "" From 63d2ba21043a408db55080a18b22f6e5f2b02444 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Mon, 16 Feb 2026 14:28:59 +0530 Subject: [PATCH 11/24] {Site} Make resource group argument required in Quickstart command --- src/site/azext_site/aaz/latest/site/_quickstart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index a7baa7e91a6..921b16468c9 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -60,6 +60,7 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.resource_group = AAZResourceGroupNameArg( options=["-g", "--resource-group"], + required=True, help="Resource group for deployment.", ) From e199fc3be96c2a1f8fb82192a8994d0e73ec4715 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 17 Feb 2026 12:40:07 +0530 Subject: [PATCH 12/24] {Site} Enhance quickstart deployment with error suppression and success messages --- .../azext_site/aaz/latest/site/_quickstart.py | 54 ++++++++++++++++--- src/site/azext_site/templates/infra/main.json | 2 +- src/site/azext_site/tests/latest/test_site.py | 19 +++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 921b16468c9..2e9d389b697 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -20,6 +20,9 @@ CLIInternalError, ) from azure.cli.core import get_default_cli # type: ignore[import-unresolved] +from knack.log import get_logger + +logger = get_logger(__name__) def _resolve_template_path() -> Path: @@ -91,16 +94,17 @@ def handle(self): site_name = self.ctx.args.name.to_serialized_data() rg = self.ctx.args.resource_group.to_serialized_data() - deployment_name = f"site-quickstart-{site_name}" + # 1) Create deployment with fully suppressed output (prevents template/deployment payload from printing) invoke_args = [ "deployment", "group", "create", "--name", deployment_name, "--resource-group", rg, "--template-file", str(template), - "--parameters", - f"siteName={site_name}", + "--parameters", f"siteName={site_name}", + "--only-show-errors", + "--output", "none", ] if has_value(self.ctx.args.location): @@ -109,12 +113,11 @@ def handle(self): if has_value(self.ctx.args.config_name): cfg = self.ctx.args.config_name.to_serialized_data() - invoke_args.append(f"configName={cfg}") + invoke_args.extend(["--parameters", f"configName={cfg}"]) cli = get_default_cli() rc = cli.invoke(invoke_args) if rc != 0: - # Include deployment context and underlying CLI error (if any) for easier troubleshooting. underlying_error = None if getattr(cli, "result", None) is not None: underlying_error = getattr(cli.result, "error", None) @@ -126,4 +129,43 @@ def handle(self): msg = f"{msg} Underlying error: {underlying_error}" raise CLIInternalError(msg) - return cli.result.result + # 2) Query deployment operations and print friendly success messages + ops_args = [ + "deployment", "operation", "group", "list", + "--name", deployment_name, + "--resource-group", rg, + "--only-show-errors", + "--output", "none", + ] + cli.invoke(ops_args) + ops = [] + if getattr(cli, "result", None) is not None: + ops = cli.result.result or [] + + succeeded_types = set() + if isinstance(ops, list): + for op in ops: + if not isinstance(op, dict): + continue + props = op.get("properties") or {} + if not isinstance(props, dict): + continue + if props.get("provisioningState") != "Succeeded": + continue + tr = props.get("targetResource") or {} + if isinstance(tr, dict): + rtype = tr.get("resourceType") + if rtype: + succeeded_types.add(rtype) + + if "Microsoft.Edge/sites" in succeeded_types: + print("Site created successfully.") + if "Microsoft.Edge/Configurations" in succeeded_types: + print("Config created successfully.") + if "Microsoft.Edge/configurationReferences" in succeeded_types: + print("Config reference created successfully.") + + if not ({"Microsoft.Edge/sites", "Microsoft.Edge/Configurations", "Microsoft.Edge/configurationReferences"} & succeeded_types): + print("Deployment completed successfully.") + + return None \ No newline at end of file diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index f59cc633844..f530de6d040 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -46,7 +46,7 @@ }, "location": { "type": "string", - "defaultValue": "[resourceGroup().location]" + "defaultValue": "eastus" }, "configName": { "type": "string", diff --git a/src/site/azext_site/tests/latest/test_site.py b/src/site/azext_site/tests/latest/test_site.py index 9e791a28a41..768e8fdc9b2 100644 --- a/src/site/azext_site/tests/latest/test_site.py +++ b/src/site/azext_site/tests/latest/test_site.py @@ -114,6 +114,23 @@ def test_edge_site_crud(self): ], ) + #Quickstart deploy (Site + Config + ConfigRef) + site_name = self.create_random_name(prefix="clitestqs", length=24) + deployment_name = f"site-quickstart-{site_name}" + self.kwargs.update({ + "qs_site": site_name, + "qs_deployment": deployment_name, + }) + self.cmd( + "az site quickstart -g {rg} -n {qs_site} --defaultconfiguration", + checks=[ + self.check("name", "{qs_deployment}"), + self.check("properties.provisioningState", "Succeeded"), + self.exists("properties.outputs.siteId.value"), + self.exists("properties.outputs.configId.value"), + ], + ) + #List Sites at resource group scope result = self.cmd( "az site list -g {rg}" @@ -131,3 +148,5 @@ def test_edge_site_crud(self): #Delete Site at subscription scope self.cmd("az site delete --site-name TestSubsSiteName --yes") + + \ No newline at end of file From c76d5fe808f026403585e39a88412a6dd0864b96 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 17 Feb 2026 14:46:13 +0530 Subject: [PATCH 13/24] {Site} Enhance error handling and messaging in quickstart deployment --- .../azext_site/aaz/latest/site/_quickstart.py | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 2e9d389b697..0e8a3abe6bf 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json from pathlib import Path from azure.cli.core.aaz import ( # type: ignore[import-unresolved] AAZCommand, @@ -96,7 +97,6 @@ def handle(self): rg = self.ctx.args.resource_group.to_serialized_data() deployment_name = f"site-quickstart-{site_name}" - # 1) Create deployment with fully suppressed output (prevents template/deployment payload from printing) invoke_args = [ "deployment", "group", "create", "--name", deployment_name, @@ -118,15 +118,63 @@ def handle(self): cli = get_default_cli() rc = cli.invoke(invoke_args) if rc != 0: + # Capture the original error first (before more invokes overwrite cli.result) underlying_error = None if getattr(cli, "result", None) is not None: underlying_error = getattr(cli.result, "error", None) + + deployment_error = None + failed_ops = None + + # Try to fetch ARM deployment error object (code/message/details) + try: + show_args = [ + "deployment", "group", "show", + "--name", deployment_name, + "--resource-group", rg, + "--only-show-errors", + "--query", "properties.error", + "--output", "json", + ] + cli.invoke(show_args) + if getattr(cli, "result", None) is not None: + deployment_error = cli.result.result + except Exception: + deployment_error = None + + # Try to fetch failed operations (often contains the most actionable message) + try: + ops_args = [ + "deployment", "operation", "group", "list", + "--name", deployment_name, + "--resource-group", rg, + "--only-show-errors", + "--query", + "[?properties.provisioningState=='Failed']." + "{type:properties.targetResource.resourceType," + " name:properties.targetResource.resourceName," + " statusMessage:properties.statusMessage}", + "--output", "json", + ] + cli.invoke(ops_args) + if getattr(cli, "result", None) is not None: + failed_ops = cli.result.result + except Exception: + failed_ops = None + msg = ( "ARM deployment failed for site quickstart. " f"Deployment name: {deployment_name}, resource group: {rg}." ) if underlying_error: - msg = f"{msg} Underlying error: {underlying_error}" + msg = f"{msg}\nUnderlying error: {underlying_error}" + + if deployment_error: + msg = f"{msg}\nDeployment error:\n{json.dumps(deployment_error, indent=2)}" + + if failed_ops: + msg = f"{msg}\nFailed operations:\n{json.dumps(failed_ops, indent=2)}" + raise CLIInternalError(msg) # 2) Query deployment operations and print friendly success messages @@ -157,7 +205,7 @@ def handle(self): rtype = tr.get("resourceType") if rtype: succeeded_types.add(rtype) - + if "Microsoft.Edge/sites" in succeeded_types: print("Site created successfully.") if "Microsoft.Edge/Configurations" in succeeded_types: From a23a275f8fa6eb2c4b7c48f19ec5b6ef38651a24 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 19 Feb 2026 17:10:46 +0530 Subject: [PATCH 14/24] {Site} Implement quickstart deployment enhancements with resource group creation and configuration validation --- .../azext_site/aaz/latest/site/_quickstart.py | 163 ++++++++++++------ src/site/azext_site/templates/infra/main.json | 16 +- 2 files changed, 124 insertions(+), 55 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 0e8a3abe6bf..ba577066a2b 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -9,7 +9,6 @@ AAZCommand, AAZStrArg, AAZStrArgFormat, - AAZBoolArg, AAZResourceGroupNameArg, has_value, register_command, @@ -32,6 +31,59 @@ def _resolve_template_path() -> Path: return azext_root / "templates" / "infra" / "main.json" + +def _get_deployment_outputs(cli, deployment_name: str, resource_group: str) -> tuple[str | None, str | None]: + site_id = None + config_id = None + try: + outputs_args = [ + "deployment", "group", "show", + "--name", deployment_name, + "--resource-group", resource_group, + "--only-show-errors", + "--query", "properties.outputs", + "--output", "none", + ] + cli.invoke(outputs_args) + if getattr(cli, "result", None) is not None and isinstance(cli.result.result, dict): + outputs = cli.result.result + if isinstance(outputs.get("siteId"), dict): + site_id = outputs.get("siteId", {}).get("value") + if isinstance(outputs.get("configId"), dict): + config_id = outputs.get("configId", {}).get("value") + except Exception: + return None, None + return site_id, config_id + + +def _arm_id_suffix(arm_id: str | None) -> str: + return f" ARM ID - {arm_id}" if arm_id else "" + + +def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str: + create_loc = (location_arg or "eastus2").strip() + if not create_loc: + create_loc = "eastus2" + + create_args = [ + "group", "create", + "--name", rg_name, + "--location", create_loc, + "--only-show-errors", + "--output", "none", + ] + rc = cli.invoke(create_args) + if rc != 0: + underlying_error = None + if getattr(cli, "result", None) is not None: + underlying_error = getattr(cli.result, "error", None) + msg = f"Failed to create resource group '{rg_name}' in location '{create_loc}'." + if underlying_error: + msg = f"{msg}\nUnderlying error: {underlying_error}" + raise CLIInternalError(msg) + + return create_loc + @register_command("site quickstart") class Quickstart(AAZCommand): """Quickstart: deploy internal ARM template to create Site + Config + ConfigRef.""" @@ -57,20 +109,27 @@ def _build_arguments_schema(cls, *args, **kwargs): ), ) - _args_schema.defaultconfiguration = AAZBoolArg( - options=["--defaultconfiguration", "--default-configuration"], - help="Trigger the internal ARM template flow (Site + Config + ConfigRef).", + _args_schema.scope = AAZStrArg( + options=["--scope"], + help="Scope for site creation. Currently supported: resource-group (default).", + ) + + _args_schema.configuration = AAZStrArg( + options=["--configuration"], + help=( + "Configuration source. Currently supported: defaults. " + "Use --configuration defaults." + ), ) _args_schema.resource_group = AAZResourceGroupNameArg( options=["-g", "--resource-group"], - required=True, - help="Resource group for deployment.", + help="Resource group for deployment. If omitted, defaults to '-rg' and will be created.", ) _args_schema.location = AAZResourceLocationArg( options=["-l", "--location"], - help="Location for the deployment. Default: resource group location.", + help="Location. Used only when creating the default resource group (default: eastus2).", ) _args_schema.config_name = AAZStrArg( @@ -82,10 +141,6 @@ def _build_arguments_schema(cls, *args, **kwargs): def _handler(self, command_args): super()._handler(command_args) - - if not self.ctx.args.defaultconfiguration: - raise InvalidArgumentValueError("Specify --defaultconfiguration to run quickstart.") - return self.handle() def handle(self): @@ -93,8 +148,39 @@ def handle(self): if not template.exists(): raise FileOperationError(f"Internal ARM template not found: {template}") + scope_raw = None + if has_value(self.ctx.args.scope): + scope_raw = (self.ctx.args.scope.to_serialized_data() or "").strip() + scope = (scope_raw or "resource-group").lower() + if scope != "resource-group": + raise InvalidArgumentValueError( + "Invalid --scope value. Currently supported: resource-group." + ) + + cfg_raw = None + if has_value(self.ctx.args.configuration): + cfg_raw = (self.ctx.args.configuration.to_serialized_data() or "").strip() + if not cfg_raw: + cfg_raw = "defaults" + if cfg_raw.lower() != "defaults": + raise InvalidArgumentValueError( + "Invalid --configuration value. Currently supported: defaults." + ) + site_name = self.ctx.args.name.to_serialized_data() - rg = self.ctx.args.resource_group.to_serialized_data() + cli = get_default_cli() + + location_arg = None + if has_value(self.ctx.args.location): + location_arg = self.ctx.args.location.to_serialized_data() + + if has_value(self.ctx.args.resource_group): + rg = self.ctx.args.resource_group.to_serialized_data() + rg_location = _create_resource_group(cli, rg, location_arg) + else: + rg = f"{site_name}" + rg_location = _create_resource_group(cli, rg, location_arg) + deployment_name = f"site-quickstart-{site_name}" invoke_args = [ @@ -103,19 +189,15 @@ def handle(self): "--resource-group", rg, "--template-file", str(template), "--parameters", f"siteName={site_name}", + "--parameters", f"location={rg_location}", "--only-show-errors", "--output", "none", ] - if has_value(self.ctx.args.location): - loc = self.ctx.args.location.to_serialized_data() - invoke_args.extend(["--parameters", f"location={loc}"]) - if has_value(self.ctx.args.config_name): cfg = self.ctx.args.config_name.to_serialized_data() invoke_args.extend(["--parameters", f"configName={cfg}"]) - cli = get_default_cli() rc = cli.invoke(invoke_args) if rc != 0: # Capture the original error first (before more invokes overwrite cli.result) @@ -177,43 +259,16 @@ def handle(self): raise CLIInternalError(msg) - # 2) Query deployment operations and print friendly success messages - ops_args = [ - "deployment", "operation", "group", "list", - "--name", deployment_name, - "--resource-group", rg, - "--only-show-errors", - "--output", "none", - ] - cli.invoke(ops_args) - ops = [] - if getattr(cli, "result", None) is not None: - ops = cli.result.result or [] - - succeeded_types = set() - if isinstance(ops, list): - for op in ops: - if not isinstance(op, dict): - continue - props = op.get("properties") or {} - if not isinstance(props, dict): - continue - if props.get("provisioningState") != "Succeeded": - continue - tr = props.get("targetResource") or {} - if isinstance(tr, dict): - rtype = tr.get("resourceType") - if rtype: - succeeded_types.add(rtype) - - if "Microsoft.Edge/sites" in succeeded_types: - print("Site created successfully.") - if "Microsoft.Edge/Configurations" in succeeded_types: - print("Config created successfully.") - if "Microsoft.Edge/configurationReferences" in succeeded_types: - print("Config reference created successfully.") - - if not ({"Microsoft.Edge/sites", "Microsoft.Edge/Configurations", "Microsoft.Edge/configurationReferences"} & succeeded_types): + site_id, config_id = _get_deployment_outputs(cli, deployment_name, rg) + config_ref_id = ( + f"{site_id}/providers/Microsoft.Edge/configurationReferences/default" if site_id else None + ) + + if site_id or config_id: + print("Site created successfully." + _arm_id_suffix(site_id)) + print("Config created successfully." + _arm_id_suffix(config_id)) + print("Config reference created successfully." + _arm_id_suffix(config_ref_id)) + else: print("Deployment completed successfully.") return None \ No newline at end of file diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index f530de6d040..1f3d0a0c6b2 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -18,9 +18,23 @@ "type": "string", "defaultValue": "2025-06-01" }, + "scope": { + "type": "string", + "defaultValue": "resource-group", + "allowedValues": [ + "resource-group" + ], + "metadata": { + "description": "Reserved. Deployment scope selector; currently only resource-group is supported." + } + }, "siteName": { "type": "string" }, + "configuration": { + "type": "object", + "defaultValue": {} + }, "resourceGroupName": { "type": "string", "defaultValue": "[resourceGroup().name]" @@ -46,7 +60,7 @@ }, "location": { "type": "string", - "defaultValue": "eastus" + "defaultValue": "eastus2" }, "configName": { "type": "string", From ac97fe9acab205362cce21a30676e7df537d9c99 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 19 Feb 2026 17:35:21 +0530 Subject: [PATCH 15/24] Refactor quickstart command help text and clean up whitespace in _quickstart.py --- src/site/azext_site/aaz/latest/site/_quickstart.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index ba577066a2b..3ac118e9d8d 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -31,7 +31,6 @@ def _resolve_template_path() -> Path: return azext_root / "templates" / "infra" / "main.json" - def _get_deployment_outputs(cli, deployment_name: str, resource_group: str) -> tuple[str | None, str | None]: site_id = None config_id = None @@ -84,6 +83,7 @@ def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str: return create_loc + @register_command("site quickstart") class Quickstart(AAZCommand): """Quickstart: deploy internal ARM template to create Site + Config + ConfigRef.""" @@ -124,7 +124,7 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.resource_group = AAZResourceGroupNameArg( options=["-g", "--resource-group"], - help="Resource group for deployment. If omitted, defaults to '-rg' and will be created.", + help="Resource group for deployment. If omitted, defaults to 'siteName' and will be created.", ) _args_schema.location = AAZResourceLocationArg( @@ -156,7 +156,7 @@ def handle(self): raise InvalidArgumentValueError( "Invalid --scope value. Currently supported: resource-group." ) - + cfg_raw = None if has_value(self.ctx.args.configuration): cfg_raw = (self.ctx.args.configuration.to_serialized_data() or "").strip() @@ -180,7 +180,7 @@ def handle(self): else: rg = f"{site_name}" rg_location = _create_resource_group(cli, rg, location_arg) - + deployment_name = f"site-quickstart-{site_name}" invoke_args = [ @@ -271,4 +271,4 @@ def handle(self): else: print("Deployment completed successfully.") - return None \ No newline at end of file + return None From c4cfa7a3b1d16b1e5de78d8f9b8008f5c8e36060 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 19 Feb 2026 17:41:12 +0530 Subject: [PATCH 16/24] {Site} Update quickstart command examples to use correct configuration argument --- src/site/azext_site/_help.py | 2 +- src/site/azext_site/tests/latest/test_site.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site/azext_site/_help.py b/src/site/azext_site/_help.py index 058091b3cfc..7fd1b809178 100644 --- a/src/site/azext_site/_help.py +++ b/src/site/azext_site/_help.py @@ -15,5 +15,5 @@ short-summary: Quickstart deploy Site + Config + ConfigRef using an internal ARM template. examples: - name: Resource group scope - text: az site quickstart --name MySite01 --defaultconfiguration -g MyRG + text: az site quickstart --name MySite01 --configuration defaults-g MyRG """ diff --git a/src/site/azext_site/tests/latest/test_site.py b/src/site/azext_site/tests/latest/test_site.py index 768e8fdc9b2..7affb98b6a9 100644 --- a/src/site/azext_site/tests/latest/test_site.py +++ b/src/site/azext_site/tests/latest/test_site.py @@ -122,7 +122,7 @@ def test_edge_site_crud(self): "qs_deployment": deployment_name, }) self.cmd( - "az site quickstart -g {rg} -n {qs_site} --defaultconfiguration", + "az site quickstart -g {rg} -n {qs_site} --configuration defaults", checks=[ self.check("name", "{qs_deployment}"), self.check("properties.provisioningState", "Succeeded"), From 48c4586c53680631c8f7deaf9f5e26ba79fbf761 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 19 Feb 2026 17:43:59 +0530 Subject: [PATCH 17/24] {Site} Fix quickstart command example to use correct configuration argument syntax --- src/site/azext_site/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/azext_site/_help.py b/src/site/azext_site/_help.py index 7fd1b809178..ef3c011fe05 100644 --- a/src/site/azext_site/_help.py +++ b/src/site/azext_site/_help.py @@ -15,5 +15,5 @@ short-summary: Quickstart deploy Site + Config + ConfigRef using an internal ARM template. examples: - name: Resource group scope - text: az site quickstart --name MySite01 --configuration defaults-g MyRG + text: az site quickstart --name MySite01 --configuration defaults --g MyRG """ From 5e75a10d87284112eb604f518051c6383940254e Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 19 Feb 2026 21:08:40 +0530 Subject: [PATCH 18/24] {Site} Fix quickstart command example to use correct shorthand for resource group flag --- src/site/azext_site/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/azext_site/_help.py b/src/site/azext_site/_help.py index ef3c011fe05..ee2c2607324 100644 --- a/src/site/azext_site/_help.py +++ b/src/site/azext_site/_help.py @@ -15,5 +15,5 @@ short-summary: Quickstart deploy Site + Config + ConfigRef using an internal ARM template. examples: - name: Resource group scope - text: az site quickstart --name MySite01 --configuration defaults --g MyRG + text: az site quickstart --name MySite01 --configuration defaults -g MyRG """ From 451370375cbbb4fc92c5b65cd89943eb841c2401 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Tue, 3 Mar 2026 09:38:11 +0530 Subject: [PATCH 19/24] Refactor deployment output handling in quickstart command to improve error reporting and success tracking --- .../azext_site/aaz/latest/site/_quickstart.py | 168 +++++++++++------- 1 file changed, 103 insertions(+), 65 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 3ac118e9d8d..45dcac70727 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -31,32 +31,89 @@ def _resolve_template_path() -> Path: return azext_root / "templates" / "infra" / "main.json" -def _get_deployment_outputs(cli, deployment_name: str, resource_group: str) -> tuple[str | None, str | None]: - site_id = None - config_id = None +def _get_deployment_ops(cli, deployment_name: str, resource_group: str) -> list[dict] | None: + """Return ARM deployment operations for a group deployment. + + Single call used for both: + - printing any succeeded resources (site/config/configRef) + - surfacing failed/canceled operations for error details + """ try: - outputs_args = [ - "deployment", "group", "show", + ops_args = [ + "deployment", "operation", "group", "list", "--name", deployment_name, "--resource-group", resource_group, "--only-show-errors", - "--query", "properties.outputs", + "--query", + "[].{" + " state:properties.provisioningState," + " type:properties.targetResource.resourceType," + " name:properties.targetResource.resourceName," + " id:properties.targetResource.id," + " statusMessage:properties.statusMessage" + "}", "--output", "none", ] - cli.invoke(outputs_args) - if getattr(cli, "result", None) is not None and isinstance(cli.result.result, dict): - outputs = cli.result.result - if isinstance(outputs.get("siteId"), dict): - site_id = outputs.get("siteId", {}).get("value") - if isinstance(outputs.get("configId"), dict): - config_id = outputs.get("configId", {}).get("value") + cli.invoke(ops_args) + if getattr(cli, "result", None) is not None and isinstance(cli.result.result, list): + return cli.result.result except Exception: - return None, None - return site_id, config_id + return None + return None + + +def _pick_failed_ops(ops: list[dict] | None) -> list[dict] | None: + if not isinstance(ops, list): + return None + + failed = [] + for op in ops: + if not isinstance(op, dict): + continue + state = (op.get("state") or "").lower() + if state in ("failed", "canceled"): + failed.append({ + "type": op.get("type"), + "name": op.get("name"), + "state": op.get("state"), + "statusMessage": op.get("statusMessage"), + }) + + return failed or None + +def _pick_succeeded_ids(ops: list[dict] | None) -> tuple[str | None, str | None, str | None]: + """Return (site_id, config_id, config_ref_id) for succeeded operations (best-effort).""" + if not isinstance(ops, list): + return None, None, None + + site_id = None + config_id = None + config_ref_id = None + + for op in ops: + if not isinstance(op, dict): + continue + if (op.get("state") or "").lower() != "succeeded": + continue + + r_type = op.get("type") + r_id = op.get("id") + + # resourceType casing can vary; normalize comparisons + r_type_norm = (r_type or "").lower() + if r_type_norm == "microsoft.edge/sites" and not site_id: + site_id = r_id + elif r_type_norm == "microsoft.edge/configurations" and not config_id: + config_id = r_id + elif r_type_norm == "microsoft.edge/configurationreferences" and not config_ref_id: + config_ref_id = r_id + + return site_id, config_id, config_ref_id + def _arm_id_suffix(arm_id: str | None) -> str: - return f" ARM ID - {arm_id}" if arm_id else "" + return f" Azure Resource ID - {arm_id}" if arm_id else "" def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str: @@ -205,44 +262,37 @@ def handle(self): if getattr(cli, "result", None) is not None: underlying_error = getattr(cli.result, "error", None) + # Single call: list all operations and reuse for both succeeded + failed reporting + all_ops = _get_deployment_ops(cli, deployment_name, rg) + succeeded_site_id, succeeded_config_id, succeeded_config_ref_id = _pick_succeeded_ids(all_ops) + + # Print succeeded resources even if the overall deployment failed + if succeeded_site_id: + print("Site created successfully." + _arm_id_suffix(succeeded_site_id)) + if succeeded_config_id: + print("Config created successfully." + _arm_id_suffix(succeeded_config_id)) + if succeeded_config_ref_id: + print("Config reference created successfully." + _arm_id_suffix(succeeded_config_ref_id)) + + failed_ops = _pick_failed_ops(all_ops) + + # Optional enrichment: fetch the top-level deployment error only when ops aren't available deployment_error = None - failed_ops = None - - # Try to fetch ARM deployment error object (code/message/details) - try: - show_args = [ - "deployment", "group", "show", - "--name", deployment_name, - "--resource-group", rg, - "--only-show-errors", - "--query", "properties.error", - "--output", "json", - ] - cli.invoke(show_args) - if getattr(cli, "result", None) is not None: - deployment_error = cli.result.result - except Exception: - deployment_error = None - - # Try to fetch failed operations (often contains the most actionable message) - try: - ops_args = [ - "deployment", "operation", "group", "list", - "--name", deployment_name, - "--resource-group", rg, - "--only-show-errors", - "--query", - "[?properties.provisioningState=='Failed']." - "{type:properties.targetResource.resourceType," - " name:properties.targetResource.resourceName," - " statusMessage:properties.statusMessage}", - "--output", "json", - ] - cli.invoke(ops_args) - if getattr(cli, "result", None) is not None: - failed_ops = cli.result.result - except Exception: - failed_ops = None + if not failed_ops: + try: + show_args = [ + "deployment", "group", "show", + "--name", deployment_name, + "--resource-group", rg, + "--only-show-errors", + "--query", "properties.error", + "--output", "json", + ] + cli.invoke(show_args) + if getattr(cli, "result", None) is not None: + deployment_error = cli.result.result + except Exception: + deployment_error = None msg = ( "ARM deployment failed for site quickstart. " @@ -259,16 +309,4 @@ def handle(self): raise CLIInternalError(msg) - site_id, config_id = _get_deployment_outputs(cli, deployment_name, rg) - config_ref_id = ( - f"{site_id}/providers/Microsoft.Edge/configurationReferences/default" if site_id else None - ) - - if site_id or config_id: - print("Site created successfully." + _arm_id_suffix(site_id)) - print("Config created successfully." + _arm_id_suffix(config_id)) - print("Config reference created successfully." + _arm_id_suffix(config_ref_id)) - else: - print("Deployment completed successfully.") - return None From b6bb0958591155b6abe9145437a096815e426301 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Thu, 5 Mar 2026 12:55:15 +0530 Subject: [PATCH 20/24] Fix formatting issues in _pick_failed_ops and _pick_succeeded_ids functions --- src/site/azext_site/aaz/latest/site/_quickstart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 45dcac70727..87e86cff10b 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -81,6 +81,7 @@ def _pick_failed_ops(ops: list[dict] | None) -> list[dict] | None: return failed or None + def _pick_succeeded_ids(ops: list[dict] | None) -> tuple[str | None, str | None, str | None]: """Return (site_id, config_id, config_ref_id) for succeeded operations (best-effort).""" if not isinstance(ops, list): @@ -111,7 +112,6 @@ def _pick_succeeded_ids(ops: list[dict] | None) -> tuple[str | None, str | None, return site_id, config_id, config_ref_id - def _arm_id_suffix(arm_id: str | None) -> str: return f" Azure Resource ID - {arm_id}" if arm_id else "" From 3525f0c118ef575ae1c5cb0b5bf526d608d24768 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Fri, 6 Mar 2026 16:25:16 +0530 Subject: [PATCH 21/24] Refactor quickstart command to enhance error handling and improve deployment operation summaries --- .../azext_site/aaz/latest/site/_quickstart.py | 281 +++++++++++------- 1 file changed, 180 insertions(+), 101 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 87e86cff10b..14c3f88e3ab 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -31,6 +31,91 @@ def _resolve_template_path() -> Path: return azext_root / "templates" / "infra" / "main.json" +def _load_template_configuration_children(template_path: Path, config_id: str | None) -> list[dict]: + """Load child resources under Microsoft.Edge/Configurations from the bundled ARM template. + + Best-effort only: this uses template defaults (no ARM reads). + """ + try: + def _resolve_template_value(params: dict, value): + """Resolve a template value like "[parameters('x')]" to that parameter's defaultValue (best-effort).""" + if not isinstance(value, str): + return value + + text = value.strip() + prefix = "[parameters('" + suffix = "')]" + if text.startswith(prefix) and text.endswith(suffix): + param_name = text[len(prefix):-len(suffix)] + param_def = params.get(param_name) + if isinstance(param_def, dict) and "defaultValue" in param_def: + return param_def.get("defaultValue") + return value + + raw = template_path.read_text(encoding="utf-8") + data = json.loads(raw) + params = data.get("parameters") if isinstance(data, dict) else None + params = params if isinstance(params, dict) else {} + + resources = data.get("resources") if isinstance(data, dict) else None + resources = resources if isinstance(resources, list) else [] + + config_resource = None + for res in resources: + if not isinstance(res, dict): + continue + if (res.get("type") or "").lower() == "microsoft.edge/configurations": + config_resource = res + break + + if not isinstance(config_resource, dict): + return [] + + child_resources = config_resource.get("resources") + child_resources = child_resources if isinstance(child_resources, list) else [] + + children: list[dict] = [] + for child in child_resources: + if not isinstance(child, dict): + continue + + child_type = child.get("type") + if not isinstance(child_type, str) or not child_type: + continue + + child_name = _resolve_template_value(params, child.get("name")) + if not isinstance(child_name, str) or not child_name: + continue + + child_kind = _resolve_template_value(params, child.get("kind")) + if not isinstance(child_kind, str): + child_kind = None + + child_properties = _resolve_template_value(params, child.get("properties")) + if not isinstance(child_properties, dict): + child_properties = {} + + child_id = None + if config_id: + child_id = f"{config_id}/{child_type}/{child_name}" + + payload = { + "id": child_id, + "type": child_type, + "name": child_name, + "properties": child_properties, + } + if child_kind: + payload["kind"] = child_kind + + children.append(payload) + + return children + except Exception: # best-effort only + logger.debug("Failed to load configuration children from template '%s'", template_path) + return [] + + def _get_deployment_ops(cli, deployment_name: str, resource_group: str) -> list[dict] | None: """Return ARM deployment operations for a group deployment. @@ -62,58 +147,39 @@ def _get_deployment_ops(cli, deployment_name: str, resource_group: str) -> list[ return None -def _pick_failed_ops(ops: list[dict] | None) -> list[dict] | None: +def _summarize_deployment_ops(ops: list[dict] | None) -> tuple[str | None, str | None, str | None, list[dict]]: + """Return (site_id, config_id, config_ref_id, failed_ops) (best-effort).""" + site_id = None + config_id = None + config_ref_id = None + failed_ops: list[dict] = [] + if not isinstance(ops, list): - return None + return site_id, config_id, config_ref_id, failed_ops - failed = [] for op in ops: if not isinstance(op, dict): continue + state = (op.get("state") or "").lower() - if state in ("failed", "canceled"): - failed.append({ + if state == "succeeded": + r_type_norm = (op.get("type") or "").lower() + r_id = op.get("id") + if r_type_norm == "microsoft.edge/sites" and not site_id: + site_id = r_id + elif r_type_norm == "microsoft.edge/configurations" and not config_id: + config_id = r_id + elif r_type_norm == "microsoft.edge/configurationreferences" and not config_ref_id: + config_ref_id = r_id + elif state in ("failed", "canceled"): + failed_ops.append({ "type": op.get("type"), "name": op.get("name"), "state": op.get("state"), "statusMessage": op.get("statusMessage"), }) - return failed or None - - -def _pick_succeeded_ids(ops: list[dict] | None) -> tuple[str | None, str | None, str | None]: - """Return (site_id, config_id, config_ref_id) for succeeded operations (best-effort).""" - if not isinstance(ops, list): - return None, None, None - - site_id = None - config_id = None - config_ref_id = None - - for op in ops: - if not isinstance(op, dict): - continue - if (op.get("state") or "").lower() != "succeeded": - continue - - r_type = op.get("type") - r_id = op.get("id") - - # resourceType casing can vary; normalize comparisons - r_type_norm = (r_type or "").lower() - if r_type_norm == "microsoft.edge/sites" and not site_id: - site_id = r_id - elif r_type_norm == "microsoft.edge/configurations" and not config_id: - config_id = r_id - elif r_type_norm == "microsoft.edge/configurationreferences" and not config_ref_id: - config_ref_id = r_id - - return site_id, config_id, config_ref_id - - -def _arm_id_suffix(arm_id: str | None) -> str: - return f" Azure Resource ID - {arm_id}" if arm_id else "" + return site_id, config_id, config_ref_id, failed_ops def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str: @@ -133,10 +199,18 @@ def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str: underlying_error = None if getattr(cli, "result", None) is not None: underlying_error = getattr(cli.result, "error", None) - msg = f"Failed to create resource group '{rg_name}' in location '{create_loc}'." + + az_error = CLIInternalError( + f"Failed to create or update resource group '{rg_name}' in location '{create_loc}'." + ) + recommendations = [ + "Verify the location is valid, or specify a different one with --location.", + "Verify you are logged in and have permission to create resource groups in the current subscription.", + ] if underlying_error: - msg = f"{msg}\nUnderlying error: {underlying_error}" - raise CLIInternalError(msg) + recommendations.append(f"Review the Azure CLI error details: {underlying_error}") + az_error.set_recommendation(recommendations) + raise az_error return create_loc @@ -203,26 +277,28 @@ def _handler(self, command_args): def handle(self): template = _resolve_template_path() if not template.exists(): - raise FileOperationError(f"Internal ARM template not found: {template}") - - scope_raw = None + az_error = FileOperationError(f"Internal ARM template not found: {template}") + az_error.set_recommendation([ + "Reinstall or update the 'site' extension to restore the bundled templates.", + "If you are developing locally, ensure 'templates/infra/main.json' exists under the extension root.", + ]) + raise az_error + + scope = "resource-group" if has_value(self.ctx.args.scope): - scope_raw = (self.ctx.args.scope.to_serialized_data() or "").strip() - scope = (scope_raw or "resource-group").lower() + scope = (self.ctx.args.scope.to_serialized_data() or "").strip().lower() or "resource-group" if scope != "resource-group": - raise InvalidArgumentValueError( - "Invalid --scope value. Currently supported: resource-group." - ) + az_error = InvalidArgumentValueError("Invalid value for --scope. Only 'resource-group' is supported.") + az_error.set_recommendation("Use --scope resource-group, or omit --scope to use the default.") + raise az_error - cfg_raw = None + configuration = "defaults" if has_value(self.ctx.args.configuration): - cfg_raw = (self.ctx.args.configuration.to_serialized_data() or "").strip() - if not cfg_raw: - cfg_raw = "defaults" - if cfg_raw.lower() != "defaults": - raise InvalidArgumentValueError( - "Invalid --configuration value. Currently supported: defaults." - ) + configuration = (self.ctx.args.configuration.to_serialized_data() or "").strip() or "defaults" + if configuration.lower() != "defaults": + az_error = InvalidArgumentValueError("Invalid value for --configuration. Only 'defaults' is supported.") + az_error.set_recommendation("Use --configuration defaults, or omit --configuration to use the default.") + raise az_error site_name = self.ctx.args.name.to_serialized_data() cli = get_default_cli() @@ -231,12 +307,8 @@ def handle(self): if has_value(self.ctx.args.location): location_arg = self.ctx.args.location.to_serialized_data() - if has_value(self.ctx.args.resource_group): - rg = self.ctx.args.resource_group.to_serialized_data() - rg_location = _create_resource_group(cli, rg, location_arg) - else: - rg = f"{site_name}" - rg_location = _create_resource_group(cli, rg, location_arg) + rg = self.ctx.args.resource_group.to_serialized_data() if has_value(self.ctx.args.resource_group) else site_name + rg_location = _create_resource_group(cli, rg, location_arg) deployment_name = f"site-quickstart-{site_name}" @@ -262,51 +334,58 @@ def handle(self): if getattr(cli, "result", None) is not None: underlying_error = getattr(cli.result, "error", None) - # Single call: list all operations and reuse for both succeeded + failed reporting all_ops = _get_deployment_ops(cli, deployment_name, rg) - succeeded_site_id, succeeded_config_id, succeeded_config_ref_id = _pick_succeeded_ids(all_ops) + succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops) # Print succeeded resources even if the overall deployment failed if succeeded_site_id: - print("Site created successfully." + _arm_id_suffix(succeeded_site_id)) + print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}") if succeeded_config_id: - print("Config created successfully." + _arm_id_suffix(succeeded_config_id)) + print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}") if succeeded_config_ref_id: - print("Config reference created successfully." + _arm_id_suffix(succeeded_config_ref_id)) - - failed_ops = _pick_failed_ops(all_ops) - - # Optional enrichment: fetch the top-level deployment error only when ops aren't available - deployment_error = None - if not failed_ops: - try: - show_args = [ - "deployment", "group", "show", - "--name", deployment_name, - "--resource-group", rg, - "--only-show-errors", - "--query", "properties.error", - "--output", "json", - ] - cli.invoke(show_args) - if getattr(cli, "result", None) is not None: - deployment_error = cli.result.result - except Exception: - deployment_error = None - - msg = ( - "ARM deployment failed for site quickstart. " - f"Deployment name: {deployment_name}, resource group: {rg}." + print(f"Config reference created successfully. Azure Resource ID - {succeeded_config_ref_id}") + + az_error = CLIInternalError( + f"Deployment failed to create all required resources. Deployment name '{deployment_name}', resource group '{rg}'." ) - if underlying_error: - msg = f"{msg}\nUnderlying error: {underlying_error}" - if deployment_error: - msg = f"{msg}\nDeployment error:\n{json.dumps(deployment_error, indent=2)}" + recommendations = [ + f"Run: az deployment group show --resource-group {rg} --name {deployment_name} --query properties.error --output jsonc", + f"Run: az deployment operation group list --resource-group {rg} --name {deployment_name} --output table", + ] if failed_ops: - msg = f"{msg}\nFailed operations:\n{json.dumps(failed_ops, indent=2)}" + failed_summary = "; ".join( + f"{op.get('type')} '{op.get('name')}' ({op.get('state')})" + for op in failed_ops + if isinstance(op, dict) + ) + if failed_summary: + recommendations.append(f"Review failed resources: {failed_summary}") - raise CLIInternalError(msg) + if succeeded_site_id or succeeded_config_id or succeeded_config_ref_id: + recommendations.append("Some resources may have been created. Review the resource group resources and clean up if needed.") - return None + if underlying_error: + recommendations.append(f"Review the Azure CLI error details: {underlying_error}") + + az_error.set_recommendation(recommendations) + raise az_error + + # Success: return structured output (JSON by default). + all_ops = _get_deployment_ops(cli, deployment_name, rg) + site_id, config_id, config_ref_id, _ = _summarize_deployment_ops(all_ops) + + child_configs = _load_template_configuration_children(template, config_id) + + return { + "siteId": site_id, + "siteName": site_name, + "type": "Microsoft.Edge/sites", + "siteConfiguration": { + "configurationId": config_id, + "location": rg_location, + "childConfigurations": child_configs, + "configurationReferenceId": config_ref_id, + }, + } From 4a6219686c5eb602fd536064c73ae1c8f6ef011b Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Mon, 9 Mar 2026 15:24:04 +0530 Subject: [PATCH 22/24] Add ARM template handling and configuration defaults to quickstart command --- .../azext_site/aaz/latest/site/_quickstart.py | 215 +++++++++++------- src/site/azext_site/templates/infra/main.json | 7 +- 2 files changed, 139 insertions(+), 83 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 14c3f88e3ab..31f3666542e 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -2,8 +2,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - import json +import logging +from contextlib import contextmanager +from importlib import resources as pkg_resources from pathlib import Path from azure.cli.core.aaz import ( # type: ignore[import-unresolved] AAZCommand, @@ -25,10 +27,48 @@ logger = get_logger(__name__) +_TEMPLATE_RESOURCE = ("templates", "infra", "main.json") + + +@contextmanager +def _template_file(): + """Yield the bundled ARM template as a real file path. + + This avoids relying on repo-relative or local developer filesystem layout. + """ + try: + trav = pkg_resources.files("azext_site").joinpath(*_TEMPLATE_RESOURCE) + with pkg_resources.as_file(trav) as template_path: + yield template_path + except FileNotFoundError as ex: + az_error = FileOperationError( + f"Internal ARM template not found in extension package: {'/'.join(_TEMPLATE_RESOURCE)}" + ) + az_error.set_recommendation([ + "Reinstall or update the 'site' extension to restore the bundled templates.", + f"Details: {ex}", + ]) + raise az_error + + +def _get_configuration_defaults_version(template_path: Path) -> str | None: + """Best-effort: read parameters.configuration.defaultValue.version from main.json.""" + try: + data = json.loads(template_path.read_text(encoding="utf-8")) + params = data.get("parameters") if isinstance(data, dict) else None + params = params if isinstance(params, dict) else {} + cfg = params.get("configuration") if isinstance(params.get("configuration"), dict) else {} + dv = cfg.get("defaultValue") if isinstance(cfg.get("defaultValue"), dict) else {} + ver = dv.get("version") + return ver if isinstance(ver, str) and ver else None + except Exception: + return None + + def _resolve_template_path() -> Path: - # ...\azext_site\aaz\latest\site\_quickstart.py -> ...\azext_site\templates\infra\main.json - azext_root = Path(__file__).resolve().parents[3] # ...\azext_site - return azext_root / "templates" / "infra" / "main.json" + # Back-compat helper retained for callers/tests; quickstart should use _template_file(). + # This path is not used for deployment in order to avoid local filesystem assumptions. + return Path("templates") / "infra" / "main.json" def _load_template_configuration_children(template_path: Path, config_id: str | None) -> list[dict]: @@ -275,15 +315,6 @@ def _handler(self, command_args): return self.handle() def handle(self): - template = _resolve_template_path() - if not template.exists(): - az_error = FileOperationError(f"Internal ARM template not found: {template}") - az_error.set_recommendation([ - "Reinstall or update the 'site' extension to restore the bundled templates.", - "If you are developing locally, ensure 'templates/infra/main.json' exists under the extension root.", - ]) - raise az_error - scope = "resource-group" if has_value(self.ctx.args.scope): scope = (self.ctx.args.scope.to_serialized_data() or "").strip().lower() or "resource-group" @@ -312,80 +343,100 @@ def handle(self): deployment_name = f"site-quickstart-{site_name}" - invoke_args = [ - "deployment", "group", "create", - "--name", deployment_name, - "--resource-group", rg, - "--template-file", str(template), - "--parameters", f"siteName={site_name}", - "--parameters", f"location={rg_location}", - "--only-show-errors", - "--output", "none", - ] + with _template_file() as template: + invoke_args = [ + "deployment", "group", "create", + "--name", deployment_name, + "--resource-group", rg, + "--template-file", str(template), + "--parameters", f"siteName={site_name}", + "--parameters", f"location={rg_location}", + "--only-show-errors", + "--output", "none", + ] - if has_value(self.ctx.args.config_name): - cfg = self.ctx.args.config_name.to_serialized_data() - invoke_args.extend(["--parameters", f"configName={cfg}"]) + config_name = None + if has_value(self.ctx.args.config_name): + config_name = self.ctx.args.config_name.to_serialized_data() + invoke_args.extend(["--parameters", f"configName={config_name}"]) + + if logger.isEnabledFor(logging.DEBUG): + defaults_version = _get_configuration_defaults_version(template) + logger.debug("Quickstart configuration defaults version: %s", defaults_version) + logger.debug( + "Quickstart effective deployment parameters: %s", + json.dumps( + { + "siteName": site_name, + "location": rg_location, + **({"configName": config_name} if config_name else {}), + }, + indent=2, + sort_keys=True, + ), + ) - rc = cli.invoke(invoke_args) - if rc != 0: - # Capture the original error first (before more invokes overwrite cli.result) - underlying_error = None - if getattr(cli, "result", None) is not None: - underlying_error = getattr(cli.result, "error", None) + rc = cli.invoke(invoke_args) + if rc != 0: + # Capture the original error first (before more invokes overwrite cli.result) + underlying_error = None + if getattr(cli, "result", None) is not None: + underlying_error = getattr(cli.result, "error", None) + + all_ops = _get_deployment_ops(cli, deployment_name, rg) + succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops) + + # Print succeeded resources even if the overall deployment failed + if succeeded_site_id: + print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}") + if succeeded_config_id: + print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}") + if succeeded_config_ref_id: + print(f"Config reference created successfully. Azure Resource ID - {succeeded_config_ref_id}") + + az_error = CLIInternalError( + f"Deployment failed to create all required resources. Deployment name '{deployment_name}', resource group '{rg}'." + ) - all_ops = _get_deployment_ops(cli, deployment_name, rg) - succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops) - - # Print succeeded resources even if the overall deployment failed - if succeeded_site_id: - print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}") - if succeeded_config_id: - print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}") - if succeeded_config_ref_id: - print(f"Config reference created successfully. Azure Resource ID - {succeeded_config_ref_id}") - - az_error = CLIInternalError( - f"Deployment failed to create all required resources. Deployment name '{deployment_name}', resource group '{rg}'." - ) - - recommendations = [ - f"Run: az deployment group show --resource-group {rg} --name {deployment_name} --query properties.error --output jsonc", - f"Run: az deployment operation group list --resource-group {rg} --name {deployment_name} --output table", - ] + recommendations = [ + f"Run: az deployment group show --resource-group {rg} --name {deployment_name} --query properties.error --output jsonc", + f"Run: az deployment operation group list --resource-group {rg} --name {deployment_name} --output table", + ] - if failed_ops: - failed_summary = "; ".join( - f"{op.get('type')} '{op.get('name')}' ({op.get('state')})" - for op in failed_ops - if isinstance(op, dict) - ) - if failed_summary: - recommendations.append(f"Review failed resources: {failed_summary}") + if failed_ops: + failed_summary = "; ".join( + f"{op.get('type')} '{op.get('name')}' ({op.get('state')})" + for op in failed_ops + if isinstance(op, dict) + ) + if failed_summary: + recommendations.append(f"Review failed resources: {failed_summary}") - if succeeded_site_id or succeeded_config_id or succeeded_config_ref_id: - recommendations.append("Some resources may have been created. Review the resource group resources and clean up if needed.") + if succeeded_site_id or succeeded_config_id or succeeded_config_ref_id: + recommendations.append( + "Some resources may have been created. Review the resource group resources and clean up if needed." + ) - if underlying_error: - recommendations.append(f"Review the Azure CLI error details: {underlying_error}") + if underlying_error: + recommendations.append(f"Review the Azure CLI error details: {underlying_error}") - az_error.set_recommendation(recommendations) - raise az_error + az_error.set_recommendation(recommendations) + raise az_error - # Success: return structured output (JSON by default). - all_ops = _get_deployment_ops(cli, deployment_name, rg) - site_id, config_id, config_ref_id, _ = _summarize_deployment_ops(all_ops) - - child_configs = _load_template_configuration_children(template, config_id) - - return { - "siteId": site_id, - "siteName": site_name, - "type": "Microsoft.Edge/sites", - "siteConfiguration": { - "configurationId": config_id, - "location": rg_location, - "childConfigurations": child_configs, - "configurationReferenceId": config_ref_id, - }, - } + # Success: return structured output (JSON by default). + all_ops = _get_deployment_ops(cli, deployment_name, rg) + site_id, config_id, config_ref_id, _ = _summarize_deployment_ops(all_ops) + + child_configs = _load_template_configuration_children(template, config_id) + + return { + "siteId": site_id, + "siteName": site_name, + "type": "Microsoft.Edge/sites", + "siteConfiguration": { + "configurationId": config_id, + "location": rg_location, + "childConfigurations": child_configs, + "configurationReferenceId": config_ref_id, + }, + } diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index 1f3d0a0c6b2..90d56761e1c 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -33,7 +33,12 @@ }, "configuration": { "type": "object", - "defaultValue": {} + "defaultValue": { + "version": "1.0.0", + "networkConfigName": "[parameters('networkConfigName')]", + "networkConfigurationKind": "[parameters('networkConfigurationKind')]", + "networkConfiguration": "[parameters('networkConfiguration')]" + } }, "resourceGroupName": { "type": "string", From eb22336b1f1424ddf33343d5445dbf87d4723f92 Mon Sep 17 00:00:00 2001 From: Guneet Aggarwal Date: Wed, 18 Mar 2026 12:33:18 +0530 Subject: [PATCH 23/24] feat: configuration creation in using 'az site quickstart' will now create MRG with service principals from graph api --- .../azext_site/aaz/latest/site/_quickstart.py | 31 +++++++++++++++++++ src/site/azext_site/templates/infra/main.json | 13 ++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 31f3666542e..381c7586425 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -22,6 +22,7 @@ CLIInternalError, ) from azure.cli.core import get_default_cli # type: ignore[import-unresolved] +from azure.cli.command_modules.role import graph_client_factory # type: ignore[import-unresolved] from knack.log import get_logger logger = get_logger(__name__) @@ -29,6 +30,28 @@ _TEMPLATE_RESOURCE = ("templates", "infra", "main.json") +_MANAGED_RESOURCE_APP_IDS = { + "AzureLocal": "1322e676-dee7-41ee-a874-ac923822781c", + "AzureEdgeOnboardingService": "47cb7c39-a99c-4dab-b91c-3a45ea22b1a8", +} + + +def _resolve_additional_identities(cli_ctx) -> list[dict]: + graph_client = graph_client_factory(cli_ctx) + identities: list[dict] = [] + for name, app_id in _MANAGED_RESOURCE_APP_IDS.items(): + result = graph_client.service_principal_list(filter=f"appId eq '{app_id}'") + if len(result) == 0: + az_error = CLIInternalError( + f"Service principal for '{name}' (appId: {app_id}) was not found in this tenant." + ) + raise az_error + identities.append({ + "servicePrincipalObjectId": result[0]["id"], + "name": name, + }) + return identities + @contextmanager def _template_file(): @@ -341,6 +364,8 @@ def handle(self): rg = self.ctx.args.resource_group.to_serialized_data() if has_value(self.ctx.args.resource_group) else site_name rg_location = _create_resource_group(cli, rg, location_arg) + additional_identities = _resolve_additional_identities(self.cli_ctx) + deployment_name = f"site-quickstart-{site_name}" with _template_file() as template: @@ -360,6 +385,12 @@ def handle(self): config_name = self.ctx.args.config_name.to_serialized_data() invoke_args.extend(["--parameters", f"configName={config_name}"]) + if additional_identities: + invoke_args.extend([ + "--parameters", + json.dumps({"additionalIdentitiesMetadata": {"value": additional_identities}}), + ]) + if logger.isEnabledFor(logging.DEBUG): defaults_version = _get_configuration_defaults_version(template) logger.debug("Quickstart configuration defaults version: %s", defaults_version) diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index 90d56761e1c..eb870968a6e 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -8,7 +8,7 @@ }, "configApiVersion": { "type": "string", - "defaultValue": "2025-06-01" + "defaultValue": "2025-12-01-preview" }, "configChildApiVersion": { "type": "string", @@ -121,6 +121,10 @@ } } } + }, + "additionalIdentitiesMetadata": { + "type": "array", + "defaultValue": [] } }, "variables": { @@ -151,7 +155,12 @@ "apiVersion": "[parameters('configApiVersion')]", "name": "[parameters('configName')]", "location": "[parameters('location')]", - "properties": {}, + "properties": { + "managedResourcesConfiguration": { + "enabled": true, + "additionalIdentitiesMetadata": "[parameters('additionalIdentitiesMetadata')]" + } + }, "resources": [ { "type": "NetworkConfigurations", From b0eafab727f124cf2202f51bc0c82017d63bc566 Mon Sep 17 00:00:00 2001 From: Akanksha A Pai Date: Mon, 23 Mar 2026 12:16:59 +0530 Subject: [PATCH 24/24] added config-type parameter to quickstart command for resource provisioning control --- .../azext_site/aaz/latest/site/_quickstart.py | 21 ++++++++++ src/site/azext_site/templates/infra/main.json | 42 +++++++------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/site/azext_site/aaz/latest/site/_quickstart.py b/src/site/azext_site/aaz/latest/site/_quickstart.py index 381c7586425..051e22f0142 100644 --- a/src/site/azext_site/aaz/latest/site/_quickstart.py +++ b/src/site/azext_site/aaz/latest/site/_quickstart.py @@ -331,6 +331,14 @@ def _build_arguments_schema(cls, *args, **kwargs): help="Optional configName override. Default in template: 'siteName-configuration'.", ) + _args_schema.config_type = AAZStrArg( + options=["--config-type"], + help=( + "Configuration type controlling which resources are provisioned. " + "ZTP (default): creates MRG and network config. " + ), + ) + return cls._args_schema def _handler(self, command_args): @@ -354,6 +362,16 @@ def handle(self): az_error.set_recommendation("Use --configuration defaults, or omit --configuration to use the default.") raise az_error + config_type = "ZTP" + if has_value(self.ctx.args.config_type): + config_type = (self.ctx.args.config_type.to_serialized_data() or "").strip().upper() or "ZTP" + if config_type not in ("ZTP",): + az_error = InvalidArgumentValueError( + f"Invalid value '{config_type}' for --config-type. Allowed values: ZTP" + ) + az_error.set_recommendation("Use --config-type ZTP, or omit to default to ZTP.") + raise az_error + site_name = self.ctx.args.name.to_serialized_data() cli = get_default_cli() @@ -385,6 +403,8 @@ def handle(self): config_name = self.ctx.args.config_name.to_serialized_data() invoke_args.extend(["--parameters", f"configName={config_name}"]) + invoke_args.extend(["--parameters", f"configType={config_type}"]) + if additional_identities: invoke_args.extend([ "--parameters", @@ -400,6 +420,7 @@ def handle(self): { "siteName": site_name, "location": rg_location, + "configType": config_type, **({"configName": config_name} if config_name else {}), }, indent=2, diff --git a/src/site/azext_site/templates/infra/main.json b/src/site/azext_site/templates/infra/main.json index eb870968a6e..b052f083ae3 100644 --- a/src/site/azext_site/templates/infra/main.json +++ b/src/site/azext_site/templates/infra/main.json @@ -12,11 +12,11 @@ }, "configChildApiVersion": { "type": "string", - "defaultValue": "2024-09-01-preview" + "defaultValue": "2025-12-01-preview" }, "configRefApiVersion": { "type": "string", - "defaultValue": "2025-06-01" + "defaultValue": "2025-12-01-preview" }, "scope": { "type": "string", @@ -71,38 +71,14 @@ "type": "string", "defaultValue": "[concat(parameters('siteName'), '-configuration')]" }, - "connectivityConfigName": { - "type": "string", - "defaultValue": "connectivityConfig1" - }, - "secretConfigName": { - "type": "string", - "defaultValue": "secretconfig1" - }, "networkConfigName": { "type": "string", - "defaultValue": "networkConfig1" + "defaultValue": "default" }, "networkConfigurationKind": { "type": "string", "defaultValue": "LAN" }, - "tsConfigName": { - "type": "string", - "defaultValue": "tsconfig1" - }, - "timeServerConfiguration": { - "type": "object", - "defaultValue": {} - }, - "connectivityConfiguration": { - "type": "object", - "defaultValue": {} - }, - "securityConfiguration": { - "type": "object", - "defaultValue": {} - }, "networkConfiguration": { "type": "object", "defaultValue": { @@ -125,6 +101,16 @@ "additionalIdentitiesMetadata": { "type": "array", "defaultValue": [] + }, + "configType": { + "type": "string", + "defaultValue": "ZTP", + "allowedValues": [ + "ZTP" + ], + "metadata": { + "description": "Configuration type. ZTP (default) creates MRG and network config." + } } }, "variables": { @@ -157,7 +143,7 @@ "location": "[parameters('location')]", "properties": { "managedResourcesConfiguration": { - "enabled": true, + "enabled": "[equals(parameters('configType'), 'ZTP')]", "additionalIdentitiesMetadata": "[parameters('additionalIdentitiesMetadata')]" } },