diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/__init__.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/__init__.py index f0436124980..1ade9b0eace 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/__init__.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/__init__.py @@ -11,7 +11,10 @@ from .__cmd_group import * from ._create import * from ._delete import * +from ._link import * from ._list import * from ._remove_version import * from ._show import * +from ._unlink import * from ._wait import * +from . import hierarchy diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_link.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_link.py new file mode 100644 index 00000000000..fe846642a89 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_link.py @@ -0,0 +1,216 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + +@register_command( + "workload-orchestration config-template link", +) +class Link(AAZCommand): + """Link a Config Template to hierarchies + :example: Link a Config Template to hierarchies + az workload-orchestration config-template link -g rg1 -n configTemplatename --hierarchy-ids "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --context-id "context123" + """ + + _aaz_info = { + "version": "2025-08-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/configtemplates/{}/linktohierarchies", "2025-08-01"], + ] + } + + AZ_SUPPORT_NO_WAIT = True + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_lro_poller(self._execute_operations, self._output) + + _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) + + # define Arg Group "" + _args_schema = cls._args_schema + _args_schema.config_template_name = AAZStrArg( + options=["-n", "--name", "--config-template-name"], + help="The name of the ConfigTemplate", + required=True, + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + + _args_schema.hierarchy_ids = AAZListArg( + options=["--hierarchy-ids"], + help="List of hierarchy IDs to link to the config template", + required=True, + ) + _args_schema.hierarchy_ids.Element = AAZStrArg() + + _args_schema.context_id = AAZStrArg( + options=["--context-id"], + help="Context ID for the link operation", + required=True, + ) + + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + yield self.ConfigTemplatesLinkToHierarchies(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class ConfigTemplatesLinkToHierarchies(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [202]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200_202, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [200]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200_202, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configTemplates/{configTemplateName}/linkToHierarchies", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "configTemplateName", self.ctx.args.config_template_name, + required=True, + ), + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2025-08-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Content-Type", "application/json", + ), + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + @property + def content(self): + _content_value, _builder = self.new_content_builder( + self.ctx.args, + typ=AAZObjectType, + typ_kwargs={"flags": {"required": True, "client_flatten": True}} + ) + _builder.set_prop("hierarchyIds", AAZListType, ".hierarchy_ids", typ_kwargs={"flags": {"required": True}}) + _builder.set_prop("contextId", AAZStrType, ".context_id", typ_kwargs={"flags": {"required": True}}) + + hierarchy_ids = _builder.get(".hierarchyIds") + if hierarchy_ids is not None: + hierarchy_ids.set_elements(AAZStrType, ".") + + return self.serialize_content(_content_value) + + def on_200_202(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_202 + ) + + _schema_on_200_202 = None + + @classmethod + def _build_schema_on_200_202(cls): + if cls._schema_on_200_202 is not None: + return cls._schema_on_200_202 + + cls._schema_on_200_202 = AAZObjectType() + _schema_on_200_202 = cls._schema_on_200_202 + + # Standard response fields + _schema_on_200_202.message = AAZStrType() + _schema_on_200_202.status = AAZStrType() + + return cls._schema_on_200_202 + + +class _LinkHelper: + """Helper class for Link""" + + +__all__ = ["Link"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_unlink.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_unlink.py new file mode 100644 index 00000000000..8bb7230bae3 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/_unlink.py @@ -0,0 +1,216 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + +@register_command( + "workload-orchestration config-template unlink", +) +class Unlink(AAZCommand): + """Unlink a Config Template from hierarchies + :example: Unlink a Config Template from hierarchies + az workload-orchestration config-template unlink -g rg1 -n configTemplatename --hierarchy-ids "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --context-id "context123" + """ + + _aaz_info = { + "version": "2025-08-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/configtemplates/{}/unlinkfromhierarchies", "2025-08-01"], + ] + } + + AZ_SUPPORT_NO_WAIT = True + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_lro_poller(self._execute_operations, self._output) + + _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) + + # define Arg Group "" + _args_schema = cls._args_schema + _args_schema.config_template_name = AAZStrArg( + options=["-n", "--name", "--config-template-name"], + help="The name of the ConfigTemplate", + required=True, + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + + _args_schema.hierarchy_ids = AAZListArg( + options=["--hierarchy-ids"], + help="List of hierarchy IDs to unlink from the config template", + required=True, + ) + _args_schema.hierarchy_ids.Element = AAZStrArg() + + _args_schema.context_id = AAZStrArg( + options=["--context-id"], + help="Context ID for the unlink operation", + required=True, + ) + + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + yield self.ConfigTemplatesUnlinkFromHierarchies(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class ConfigTemplatesUnlinkFromHierarchies(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [202]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200_202, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [200]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200_202, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configTemplates/{configTemplateName}/unlinkFromHierarchies", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "configTemplateName", self.ctx.args.config_template_name, + required=True, + ), + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2025-08-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Content-Type", "application/json", + ), + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + @property + def content(self): + _content_value, _builder = self.new_content_builder( + self.ctx.args, + typ=AAZObjectType, + typ_kwargs={"flags": {"required": True, "client_flatten": True}} + ) + _builder.set_prop("hierarchyIds", AAZListType, ".hierarchy_ids", typ_kwargs={"flags": {"required": True}}) + _builder.set_prop("contextId", AAZStrType, ".context_id", typ_kwargs={"flags": {"required": True}}) + + hierarchy_ids = _builder.get(".hierarchyIds") + if hierarchy_ids is not None: + hierarchy_ids.set_elements(AAZStrType, ".") + + return self.serialize_content(_content_value) + + def on_200_202(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_202 + ) + + _schema_on_200_202 = None + + @classmethod + def _build_schema_on_200_202(cls): + if cls._schema_on_200_202 is not None: + return cls._schema_on_200_202 + + cls._schema_on_200_202 = AAZObjectType() + _schema_on_200_202 = cls._schema_on_200_202 + + # Standard response fields + _schema_on_200_202.message = AAZStrType() + _schema_on_200_202.status = AAZStrType() + + return cls._schema_on_200_202 + + +class _UnlinkHelper: + """Helper class for Unlink""" + + +__all__ = ["Unlink"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__cmd_group.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__cmd_group.py new file mode 100644 index 00000000000..ca6ee628eca --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__cmd_group.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "workload-orchestration config-template hierarchy", +) +class __CMDGroup(AAZCommandGroup): + """workload-orchestration config-template hierarchy helps to manage config template hierarchies + """ + pass + + +__all__ = ["__CMDGroup"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__init__.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__init__.py new file mode 100644 index 00000000000..3bf9906f2f3 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/__init__.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * +from ._show import * \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/_show.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/_show.py new file mode 100644 index 00000000000..ff628fda71a --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/config_template/hierarchy/_show.py @@ -0,0 +1,222 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "workload-orchestration config-template hierarchy show", +) +class Show(AAZCommand): + """Show linked hierarchies for a config template + :example: Show linked hierarchies for a config template + az workload-orchestration config-template hierarchy show -g rg1 -n configTemplateName + """ + + _aaz_info = { + "version": "2025-08-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/configtemplates/{}/configtemplatemetadatas", "2025-08-01"], + ] + } + + def _handler(self, command_args): + super()._handler(command_args) + self._execute_operations() + return self._output() + + _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) + + # define Arg Group "" + _args_schema = cls._args_schema + _args_schema.config_template_name = AAZStrArg( + options=["-n", "--name", "--config-template-name"], + help="The name of the ConfigTemplate", + required=True, + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + self.ConfigTemplateMetadatasList(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + + # Process the result to extract and return only the linked hierarchy information + hierarchy_info = [] + + if result and "value" in result: + for metadata in result["value"]: + context_info = { + "contextId": metadata.get("id", ""), + "linkedHierarchies": [] + } + + properties = metadata.get("properties", {}) + + # Extract only linked hierarchies + linked = properties.get("linkedHierarchies", []) + for linked_hierarchy in linked: + context_info["linkedHierarchies"].extend( + linked_hierarchy.get("hierarchyIds", []) + ) + + # Only include contexts that have linked hierarchies + if context_info["linkedHierarchies"]: + hierarchy_info.append(context_info) + + return hierarchy_info + + class ConfigTemplateMetadatasList(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configTemplates/{configTemplateName}/configTemplateMetadatas", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "configTemplateName", self.ctx.args.config_template_name, + required=True, + ), + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2025-08-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + _schema_on_200 = cls._schema_on_200 + + _schema_on_200.value = AAZListType() + + value = _schema_on_200.value + value.Element = AAZObjectType() + + element = value.Element + element.id = AAZStrType() + element.name = AAZStrType() + element.properties = AAZObjectType() + element.system_data = AAZObjectType(serialized_name="systemData") + element.type = AAZStrType() + + properties = element.properties + properties.linked_hierarchies = AAZListType(serialized_name="linkedHierarchies") + properties.provisioning_state = AAZStrType(serialized_name="provisioningState") + + linked_hierarchies = properties.linked_hierarchies + linked_hierarchies.Element = AAZObjectType() + + linked_element = linked_hierarchies.Element + linked_element.hierarchy_ids = AAZListType(serialized_name="hierarchyIds") + linked_element.level = AAZStrType() + + hierarchy_ids = linked_element.hierarchy_ids + hierarchy_ids.Element = AAZStrType() + + system_data = element.system_data + system_data.created_at = AAZStrType(serialized_name="createdAt") + system_data.created_by = AAZStrType(serialized_name="createdBy") + system_data.created_by_type = AAZStrType(serialized_name="createdByType") + system_data.last_modified_at = AAZStrType(serialized_name="lastModifiedAt") + system_data.last_modified_by = AAZStrType(serialized_name="lastModifiedBy") + system_data.last_modified_by_type = AAZStrType(serialized_name="lastModifiedByType") + + return cls._schema_on_200 + + +class _ShowHelper: + """Helper class for Show""" + + +__all__ = ["Show"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/__init__.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/__init__.py index 8b28ccba0b4..0da4fcfc421 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/__init__.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/__init__.py @@ -11,4 +11,5 @@ from .__cmd_group import * from ._config_set import * from ._config_show import * -from ._config_download import * \ No newline at end of file +from ._config_download import * +from ._schema_show import * \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_download.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_download.py index ea6eef91518..4aa074808d2 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_download.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_download.py @@ -10,6 +10,8 @@ import os from azure.cli.core.aaz import * +from azure.cli.core.azclierror import CLIInternalError +from ._config_helper import ConfigurationHelper @register_command( @@ -19,7 +21,9 @@ class Download(AAZCommand): """Download configurations available at specified hierarchical entity :example: Download configuration - az workload-orchestration configuration download -g rg1 --target-name target1 --solution-template-name solutionTemplate1 + az workload-orchestration configuration download -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name template1 --version 1.0.0 + :example: Download a Solution Template Configuration + az workload-orchestration configuration download -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name solutionTemplate1 --version 1.0.0 --solution """ _aaz_info = { @@ -45,37 +49,56 @@ def _build_arguments_schema(cls, *args, **kwargs): # define Arg Group "" _args_schema = cls._args_schema - _args_schema.target_name = AAZStrArg( - options=["--target-name"], - help="The name of the Configuration", + _args_schema.resource_group = AAZResourceGroupNameArg( required=True, - id_part="name", + ) + + _args_schema.hierarchy_id = AAZStrArg( + options=["--hierarchy-id"], + help="The ARM ID for the target or site at which values needs to be downloaded", + required=True + ) + + _args_schema.template_subscription = AAZStrArg( + options=["--template_subscription"], + help="Subscription ID for the template. Only needed if the subscription ID for the template is different than the current subscription ID.", + required=False, fmt=AAZStrArgFormat( - pattern="^[a-zA-Z0-9-]{3,24}$", + pattern="^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", ), ) - _args_schema.solution_template_name = AAZStrArg( - options=["--solution-template-name"], - help="The name of the DynamicConfiguration", - required=False, - id_part="child_name_1", + + _args_schema.template_resource_group = AAZStrArg( + options=["--template-resource-group"], + help="Resource group name for the template.", + required=True, + ) + + _args_schema.template_name = AAZStrArg( + options=["--template-name"], + help="The name of the Template (Solution template or Configuration template) to download.", + required=True, fmt=AAZStrArgFormat( pattern="^[a-zA-Z0-9-]{3,24}$", ), ) - _args_schema.resource_group = AAZResourceGroupNameArg( - required=True, + + _args_schema.version = AAZStrArg( + options=["--version"], + help="Version of the template.", + required=True ) + + _args_schema.solution = AAZBoolArg( + options=["--solution"], + help="Flag to indicate that we are downloading a solution. If not provided, we are downloading a config template.", + required=False, + ) + return cls._args_schema def _execute_operations(self): self.pre_operations() - config_name = str(self.ctx.args.target_name) - if len(config_name) > 18: - config_name = config_name[:18] + "Config" - else: - config_name = config_name + "Config" - self.ctx.args.target_name = config_name self.DynamicConfigurationVersionsGet(ctx=self.ctx)() self.post_operations() @@ -96,14 +119,16 @@ def _output(self, *args, **kwargs): print("No config found.") return - # Create filename based on target_name and solution_template_name - target_name = str(self.ctx.args.target_name) - if target_name.endswith("Config"): - # Remove the "Config" suffix for the filename - target_name = target_name[:len(target_name)-6] + # Create filename based on template name and version + template_name = str(self.ctx.args.template_name) + version = str(self.ctx.args.version) + solution_suffix = "_solution" if self.ctx.args.solution else "" - solution_name = str(self.ctx.args.solution_template_name) - filename = f"{target_name}_{solution_name}.yaml" + # Use custom output file if provided + if hasattr(self.ctx.args, 'output_file') and self.ctx.args.output_file: + filename = str(self.ctx.args.output_file) + else: + filename = f"{template_name}_{version}{solution_suffix}_config.yaml" # Get absolute path absolute_path = os.path.abspath(filename) @@ -114,20 +139,36 @@ def _output(self, *args, **kwargs): file.write(config_values) print(f"Configuration saved to: {absolute_path}") except Exception as e: - print(f"Error saving configuration to file: {str(e)}") + print(f"Error saving configuration to file: {str(e)}") class DynamicConfigurationVersionsGet(AAZHttpOperation): CLIENT_TYPE = "MgmtClient" def __call__(self, *args, **kwargs): + # Get configuration ID using the existing client + self.configuration_id = ConfigurationHelper.getConfigurationId(self.ctx.args.hierarchy_id, self.client) + + # Get template unique identifier for dynamic configuration name + template_subscription = self.ctx.args.template_subscription if self.ctx.args.template_subscription else self.ctx.subscription_id + solution_flag = self.ctx.args.solution if self.ctx.args.solution else False + self.dynamic_configuration_name = ConfigurationHelper.getTemplateUniqueIdentifier( + template_subscription, + self.ctx.args.template_resource_group, + self.ctx.args.template_name, + solution_flag, + self.client + ) + request = self.make_request() session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: return self.on_200(session) - config = dict() - config["properties"] = dict() - config["properties"]["values"] = "{}" - if session.http_response.status_code in [404]: + elif session.http_response.status_code in [404]: + # Return empty config for 404 + config = dict() + config["properties"] = dict() + config["properties"]["values"] = "{}" self.ctx.set_var( "instance", config, @@ -136,13 +177,12 @@ def __call__(self, *args, **kwargs): else: return self.on_error(session.http_response) - @property def url(self): - return self.client.format_url( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configurations/{configurationName}/dynamicConfigurations/{dynamicConfigurationName}/versions/version1", - **self.url_parameters - ) + # Use the configuration ID and append the dynamic configuration path + base_url = self.configuration_id + dynamic_config_path = f"/dynamicConfigurations/{self.dynamic_configuration_name}/versions/{self.ctx.args.version}" + return base_url + dynamic_config_path @property def method(self): @@ -152,32 +192,6 @@ def method(self): def error_format(self): return "MgmtErrorFormat" - @property - def url_parameters(self): - solution_template_name = "common" - if has_value(self.ctx.args.solution_template_name): - solution_template_name = self.ctx.args.solution_template_name - - parameters = { - **self.serialize_url_param( - "configurationName", self.ctx.args.target_name, - required=True, - ), - **self.serialize_url_param( - "dynamicConfigurationName", solution_template_name, - required=True, - ), - **self.serialize_url_param( - "resourceGroupName", self.ctx.args.resource_group, - required=True, - ), - **self.serialize_url_param( - "subscriptionId", self.ctx.subscription_id, - required=True, - ), - } - return parameters - @property def query_parameters(self): parameters = { @@ -214,7 +228,6 @@ def _build_schema_on_404(cls): _schema_on_200.properties = AAZFreeFormDictType() return cls._schema_on_200 - @classmethod def _build_schema_on_200(cls): if cls._schema_on_200 is not None: @@ -226,31 +239,22 @@ def _build_schema_on_200(cls): _schema_on_200.id = AAZStrType( flags={"read_only": True}, ) + _schema_on_200.location = AAZStrType( + flags={"required": True}, + ) _schema_on_200.name = AAZStrType( flags={"read_only": True}, ) - _schema_on_200.properties = AAZObjectType() + _schema_on_200.properties = AAZFreeFormDictType() _schema_on_200.system_data = AAZObjectType( serialized_name="systemData", flags={"read_only": True}, ) + _schema_on_200.tags = AAZDictType() _schema_on_200.type = AAZStrType( flags={"read_only": True}, ) - properties = cls._schema_on_200.properties - properties.provisioning_state = AAZStrType( - serialized_name="provisioningState", - flags={"read_only": True}, - ) - properties.schema_id = AAZStrType( - serialized_name="schemaId", - flags={"read_only": True}, - ) - properties.values = AAZStrType( - flags={"required": True}, - ) - system_data = cls._schema_on_200.system_data system_data.created_at = AAZStrType( serialized_name="createdAt", @@ -271,6 +275,9 @@ def _build_schema_on_200(cls): serialized_name="lastModifiedByType", ) + tags = cls._schema_on_200.tags + tags.Element = AAZStrType() + return cls._schema_on_200 diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_helper.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_helper.py new file mode 100644 index 00000000000..22d8e6f7f37 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_helper.py @@ -0,0 +1,256 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +class ConfigurationHelper: + """Helper class for workload orchestration configuration operations""" + + def __init__(self): + """Initialize the configuration helper""" + pass + + @staticmethod + def getConfigurationId(hierarchy_id, client): + """ + Get configuration ID from hierarchy ID by calling configuration reference API + + Args: + hierarchy_id (str): The hierarchy ID (ARM ID of site or target) + client: HTTP client for making the request + + Returns: + str: The configuration ID from configurationResourceId field + + Raises: + CLIInternalError: If configuration reference is not found or configuration doesn't exist + """ + from azure.cli.core.azclierror import CLIInternalError + import json + + # Convert hierarchy_id to string if it's an AAZ type + hierarchy_id_str = str(hierarchy_id) if hierarchy_id else "" + + def try_get_config_id(lookup_id, api_version = "2025-08-01"): + """Helper function to try getting configuration ID for a given lookup ID""" + config_ref_url = client.format_url( + "{hierarchyId}/providers/Microsoft.Edge/configurationreferences/default", + hierarchyId=lookup_id + ) + + request = client._request("GET", config_ref_url, { + "api-version": api_version + }, { + "Accept": "application/json" + }, None, {}, None) + + response = client.send_request(request=request, stream=False) + + if response.http_response.status_code != 200: + return None + + response_text = response.http_response.text() + data = json.loads(response_text) + configuration_id = data.get("properties", {}).get("configurationResourceId") + + if not configuration_id: + return None + + # Verify the configuration exists + config_url = client.format_url( + "{configurationId}", + configurationId=configuration_id + ) + config_request = client._request("GET", config_url, { + "api-version": "2025-08-01" + }, { + "Accept": "application/json" + }, None, {}, None) + + config_response = client.send_request(request=config_request, stream=False) + + if config_response.http_response.status_code == 200: + return configuration_id + + return None + + # Check if hierarchy_id is a service group-based site ID + service_group_id = None + if "/providers/Microsoft.Management/serviceGroups/" in hierarchy_id_str and "/providers/Microsoft.Edge/sites/" in hierarchy_id_str: + # Extract service group ID: everything before /providers/Microsoft.Edge/sites/ + parts = hierarchy_id_str.split("/providers/Microsoft.Edge/sites/") + if len(parts) == 2: + service_group_id = parts[0] + + # Try with service group ID if it's a service group-based site + if service_group_id: + # Try with the original hierarchy_id first + try: + configuration_id = try_get_config_id(hierarchy_id_str, "2025-06-01") + if configuration_id: + return configuration_id + except Exception: + pass + try: + configuration_id = try_get_config_id(service_group_id) + if configuration_id: + return configuration_id + except Exception: + pass + else: + try: + configuration_id = try_get_config_id(hierarchy_id_str) + if configuration_id: + return configuration_id + except Exception: + pass + + + # If we reach here, no configuration was found + raise CLIInternalError(f"No configuration linked to this hierarchy: {hierarchy_id_str}") + + @staticmethod + def getTemplateUniqueIdentifier(subscription_id, template_resource_group_name, template_name, solution_flag, client): + """ + Get template unique identifier from template ID + + Args: + subscription_id (str): The subscription ID for the template + template_resource_group_name (str): The resource group name for the template + template_name (str): The template name + solution_flag (bool): True for solution template, False for configuration template + client: HTTP client for making the request + + Returns: + str: The unique identifier from template properties or template name as fallback + + Raises: + CLIInternalError: If template doesn't exist + """ + from azure.cli.core.azclierror import CLIInternalError + + # Build template ID based on solution flag + if solution_flag: + template_type = "solutionTemplates" + else: + template_type = "configTemplates" + + template_id = f"/subscriptions/{subscription_id}/resourceGroups/{template_resource_group_name}/providers/Microsoft.Edge/{template_type}/{template_name}" + + try: + # Make GET request to template using client.format_url + template_url = client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/{templateType}/{templateName}", + subscriptionId=subscription_id, + resourceGroupName=template_resource_group_name, + templateType=template_type, + templateName=template_name + ) + request = client._request("GET", template_url, { + "api-version": "2025-08-01" + }, { + "Accept": "application/json" + }, None, {}, None) + + response = client.send_request(request=request, stream=False) + + if response.http_response.status_code == 404: + raise CLIInternalError(f"Template doesn't exist with template ID: {template_id}") + elif response.http_response.status_code != 200: + raise CLIInternalError(f"Failed to get template with ID: {template_id}") + + # Parse JSON response + import json + response_text = response.http_response.text() + data = json.loads(response_text) + + unique_identifier = data.get("properties", {}).get("uniqueIdentifier") + + # Return unique identifier if it exists and is not empty, otherwise return template name + if unique_identifier and unique_identifier.strip(): + return unique_identifier + else: + return template_name + + except CLIInternalError: + # Re-raise CLI errors as-is + raise + except Exception as e: + raise CLIInternalError(f"Error getting template unique identifier for template {template_id}: {str(e)}") + + @staticmethod + def getTemplateSchema(subscription_id, resource_group, template_name, version, solution_flag, client): + """ + Get template schema + + Args: + subscription_id (str): The subscription ID for the template + resource_group (str): The resource group name for the template + template_name (str): The template name + version (str): The template version + solution_flag (bool): True for solution template, False for configuration template + client: HTTP client for making the request + + Returns: + str: Raw schema YAML string from the template + + Raises: + CLIInternalError: If schema doesn't exist or request fails + """ + from azure.cli.core.azclierror import CLIInternalError + + # Build template version ID + if solution_flag: + template_type = "solutionTemplates" + schema_endpoint = "solutionSchemas/default" + else: + template_type = "configTemplates" + schema_endpoint = "configTemplateSchemas/default" + + template_version_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Edge/{template_type}/{template_name}/versions/{version}" + schema_url = template_version_id + "/" + schema_endpoint + + try: + # Make GET request to schema endpoint + request = client._request("GET", schema_url, { + "api-version": "2025-08-01" + }, { + "Accept": "application/json" + }, None, {}, None) # Add missing parameters + + response = client.send_request(request=request, stream=False) + + if response.http_response.status_code == 404: + raise CLIInternalError(f"No Editable configs. Schema doesn't exist for template: {template_version_id}") + elif response.http_response.status_code != 200: + raise CLIInternalError(f"Failed to get schema for template: {template_version_id}") + + import json + response_text = response.http_response.text() + data = json.loads(response_text) + value = data.get("properties", {}).get("value") + + if value is None: + raise CLIInternalError(f"No value field found in schema for template: {template_version_id}") + + # Return the raw schema YAML value + return value + + except CLIInternalError: + # Re-raise CLI errors as-is + raise + except Exception as e: + raise CLIInternalError(f"Error getting schema for template {template_version_id}: {str(e)}") + + # Add your helper methods here + + +__all__ = ["ConfigurationHelper"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_set.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_set.py index 718009d7beb..4f25b9c2376 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_set.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_set.py @@ -15,6 +15,7 @@ import json from azure.cli.core.aaz import * from azure.cli.core.azclierror import CLIInternalError +from ._config_helper import ConfigurationHelper @register_command( "workload-orchestration configuration set", @@ -23,9 +24,11 @@ class ShowConfig2(AAZCommand): """To set the values to configurations available at specified hierarchical entity :example: Set a Configuration through editor - az workload-orchestration configuration set -g rg1 --target-name target1 --solution-template-name solutionTemplate1 + az workload-orchestration configuration set -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name template1 --version 1.0.0 :example: Set a Configuration through file - az workload-orchestration configuration set -g rg1 --target-name target1 --solution-template-name solutionTemplate1 --file /path/to/config.yaml + az workload-orchestration configuration set -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name template1 --version 1.0.0 --file /path/to/config.yaml + :example: Set a Solution Template Configuration + az workload-orchestration configuration set -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name solutionTemplate1 --version 1.0.0 --solution """ _aaz_info = { @@ -54,32 +57,56 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.resource_group = AAZResourceGroupNameArg( required=True, ) - _args_schema.solution_name = AAZStrArg( - options=["--solution-template-name"], - help="The name of the Solution, This is required only to set solution configurations", - # required=True, + + _args_schema.hierarchy_id = AAZStrArg( + options=["--hierarchy-id"], + help="The ARM ID for the target or site at which values needs to be set", + required=True + ) + _args_schema.template_subscription = AAZStrArg( + options=["--template_subscription"], + help="Subscription ID for the template. Only needed if the subscription ID for the template is different than the current subscription ID.", + required=False, fmt=AAZStrArgFormat( - pattern="^[a-zA-Z0-9-]{3,24}$", + pattern="^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", ), ) - _args_schema = cls._args_schema - _args_schema.level_name = AAZStrArg( - options=["--target-name"], - help="The Deployment Target or Site name at which values needs to be set", + _args_schema.template_resource_group = AAZStrArg( + options=["--template-resource-group"], + help="Resource group name for the template.", required=True, + ) + + _args_schema.template_name = AAZStrArg( + options=["--template-name"], + help="The name of the Template (Solution template or Configuration template) to configure.", + required=True, + fmt=AAZStrArgFormat( pattern="^[a-zA-Z0-9-]{3,24}$", ), ) + _args_schema.version = AAZStrArg( + options=["--version"], + help="Version of the template.", + required=True + ) + _args_schema.file_path = AAZFileArg( options=["--file","-f"], help="Path to a file containing the configuration values. If provided, the editor will not be opened.", required=False, ) + _args_schema.solution = AAZBoolArg( + options=["--solution"], + help="Flag to indicate that we are configuring a solution. If not provided, we are configuring a config template.", + required=False, + ) + # define Arg Group "Resource" # _args_schema = cls._args_schema @@ -101,17 +128,16 @@ def _build_arguments_schema(cls, *args, **kwargs): def _execute_operations(self): self.pre_operations() - config_name = str(self.ctx.args.level_name) - if len(config_name) > 18: - config_name = config_name[:18] + "Config" - else: - config_name = config_name + "Config" - self.ctx.args.level_name = config_name self.SolutionsGet(ctx=self.ctx)() self.post_operations() @register_callback def pre_operations(self): + # Validate that --solution flag is only used with target hierarchy IDs + if hasattr(self.ctx.args, 'solution') and self.ctx.args.solution: + hierarchy_id = str(self.ctx.args.hierarchy_id).lower() + if "microsoft.edge/targets" not in hierarchy_id: + raise CLIInternalError("The --solution flag can only be used when the hierarchy-id is for a target (Microsoft.Edge/targets). Solutions are only configurable at a target level.") pass @register_callback @@ -127,19 +153,36 @@ class SolutionsGet(AAZHttpOperation): CLIENT_TYPE = "MgmtClient" def __call__(self, *args, **kwargs): + self.configuration_id = ConfigurationHelper.getConfigurationId(self.ctx.args.hierarchy_id, self.client) + + # Get template unique identifier for dynamic configuration name + template_subscription = self.ctx.args.template_subscription if self.ctx.args.template_subscription else self.ctx.subscription_id + solution_flag = self.ctx.args.solution if self.ctx.args.solution else False + self.dynamic_configuration_name = ConfigurationHelper.getTemplateUniqueIdentifier( + template_subscription, + self.ctx.args.template_resource_group, + self.ctx.args.template_name, + solution_flag, + self.client + ) + request = self.make_request() session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + # Dynamic configuration version exists - update flow response = self.get_config_to_update(session) config_to_set = response["properties"]["values"] - # Check if file path is provided - if hasattr(self.ctx.args, 'file_path') and self.ctx.args.file_path: + + # Get new configuration content + if hasattr(self.ctx.args, 'file_path') and self.ctx.args.file_path and has_value(self.ctx.args.file_path): try: config_to_set = str(self.ctx.args.file_path) except Exception as e: - raise CLIInternalError(f"Failed to process file content: {str(e)}") + raise CLIInternalError(f"Failed to read file content: {str(e)}") else: - editor= "vi" + # Open editor with current config + editor = "vi" if platform.system() == "Windows": editor = "notepad" temp_file = tempfile.NamedTemporaryFile(delete=False) @@ -153,40 +196,128 @@ def __call__(self, *args, **kwargs): with open(temp_file.name, "rb") as f: config_to_set = f.read().decode("utf-8") os.unlink(temp_file.name) - # print(config_to_set) + + # Validate that the content is valid YAML + try: + import yaml + yaml.safe_load(config_to_set) + except yaml.YAMLError as e: + raise CLIInternalError(f"The configuration content is not valid YAML: {str(e)}") + except Exception as e: + raise CLIInternalError(f"Failed to validate YAML content: {str(e)}") + + # Update existing configuration new_content = dict() new_content["properties"] = response["properties"] new_content["properties"]["values"] = config_to_set + serialized_new_content = self.serialize_content(new_content) request = self.client._request( "PUT", self.url, self.query_parameters, self.header_parameters2, - new_content, self.form_content, self.stream_content) + serialized_new_content, self.form_content, self.stream_content) session = self.client.send_request(request=request, stream=False, **kwargs) if session.http_response.status_code in [200]: return self.on_200(session) - # return self.on_error(session.http_response) - config = dict() - config["properties"] = dict() - config["properties"]["values"] = "No config found." - # # config.config = AAZStrType() - # # config.config = "[]" - if session.http_response.status_code in [404]: - self.ctx.set_var( - "instance", - config, - schema_builder=self._build_schema_on_404 - ) - # return + else: + return self.on_error(session.http_response) + + elif session.http_response.status_code in [404]: + # Dynamic configuration version doesn't exist - create flow + + # Step 1: Create dynamic configuration (without version) + dynamic_config_url = f"{self.configuration_id}/dynamicConfigurations/{self.dynamic_configuration_name}" + dynamic_config_model = "Application" if (self.ctx.args.solution if self.ctx.args.solution else False) else "Common" + + dynamic_config_content = { + "properties": { + "currentVersion": str(self.ctx.args.version), + "dynamicConfigurationType": "Hierarchy", + "dynamicConfigurationModel": dynamic_config_model + } + } + + serialized_content = self.serialize_content(dynamic_config_content) + request = self.client._request( + "PUT", dynamic_config_url, self.query_parameters, self.header_parameters2, + serialized_content, self.form_content, self.stream_content) + session = self.client.send_request(request=request, stream=False, **kwargs) + + if session.http_response.status_code not in [200, 201]: + return self.on_error(session.http_response) + + # Step 2: Get configuration content for version + if hasattr(self.ctx.args, 'file_path') and self.ctx.args.file_path and has_value(self.ctx.args.file_path): + try: + config_to_set = str(self.ctx.args.file_path) + except Exception as e: + raise CLIInternalError(f"Failed to read file content: {str(e)}") + else: + # Get placeholder content from schema + try: + placeholder_content = self.getConfigPlaceholderFromSchema( + template_subscription, + self.ctx.args.template_resource_group, + self.ctx.args.template_name, + self.ctx.args.version, + solution_flag + ) + except CLIInternalError: + # Re-raise CLI errors (like "No editable configs") to show user + raise + except Exception as e: + # For other unexpected errors, use empty content as fallback + placeholder_content = "" + + editor = "vi" + if platform.system() == "Windows": + editor = "notepad" + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.write(bytes(placeholder_content, "utf-8")) # Use placeholder content + temp_file.close() + editor_output = subprocess.run([editor, temp_file.name], stdout=sys.stdout, stdin=sys.stdin, + stderr=sys.stdout, check=False) + if editor_output.returncode != 0: + os.unlink(temp_file.name) + raise CLIInternalError("Failed to update instance") + with open(temp_file.name, "rb") as f: + config_to_set = f.read().decode("utf-8") + os.unlink(temp_file.name) + + # Validate that the content is valid YAML + try: + import yaml + yaml.safe_load(config_to_set) + except yaml.YAMLError as e: + raise CLIInternalError(f"The configuration content is not valid YAML: {str(e)}") + except Exception as e: + raise CLIInternalError(f"Failed to validate YAML content: {str(e)}") + + # Step 3: Create dynamic configuration version + version_content = { + "properties": { + "values": config_to_set + } + } + + serialized_version_content = self.serialize_content(version_content) + request = self.client._request( + "PUT", self.url, self.query_parameters, self.header_parameters2, + serialized_version_content, self.form_content, self.stream_content) + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200, 201]: + return self.on_200(session) + else: + return self.on_error(session.http_response) else: return self.on_error(session.http_response) @property def url(self): - return self.client.format_url( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configurations/{configName}/DynamicConfigurations/{solutionName}/versions/version1", - **self.url_parameters - ) + # Use the configuration ID and append the dynamic configuration path + base_url = self.configuration_id + dynamic_config_path = f"/dynamicConfigurations/{self.dynamic_configuration_name}/versions/{self.ctx.args.version}" + return base_url + dynamic_config_path @property def method(self): @@ -255,6 +386,81 @@ def header_parameters2(self): def get_config_to_update(self,session): data = self.deserialize_http_content(session) return data + + def getConfigPlaceholderFromSchema(self, subscription_id, resource_group, template_name, version, solution_flag): + """ + Get configuration placeholder from schema + + Args: + subscription_id (str): The subscription ID for the template + resource_group (str): The resource group name for the template + template_name (str): The template name + version (str): The template version + solution_flag (bool): True for solution template, False for configuration template + + Returns: + str: YAML placeholder with config keys extracted from schema + + Raises: + CLIInternalError: If schema doesn't exist or request fails + """ + try: + # Get the raw schema from helper + schema_value = ConfigurationHelper.getTemplateSchema( + subscription_id, resource_group, template_name, version, solution_flag, self.client + ) + + # Parse the YAML value to extract config keys + try: + import yaml + schema_data = yaml.safe_load(schema_value) + configs = schema_data.get("rules", {}).get("configs", {}) + + # Check if there are no editable configs + if not configs: + raise CLIInternalError("No editable configs.") + + # Build placeholder YAML structure + placeholder_dict = {} + + for key in configs.keys(): + if '.' in key: + # Handle nested keys like "A.B" + parts = key.split('.') + current = placeholder_dict + for i, part in enumerate(parts): + if i == len(parts) - 1: + # Last part, don't set any value + if part not in current: + current[part] = None + else: + # Intermediate part, create nested dict if doesn't exist + if part not in current: + current[part] = {} + current = current[part] + else: + # Simple key + placeholder_dict[key] = None + + # Convert to YAML string with custom representer to handle None values + if placeholder_dict: + def represent_none(self, data): + return self.represent_scalar('tag:yaml.org,2002:null', '') + + yaml.add_representer(type(None), represent_none) + placeholder_yaml = yaml.dump(placeholder_dict, default_flow_style=False, allow_unicode=True) + return placeholder_yaml + else: + raise CLIInternalError("No editable configs.") + + except yaml.YAMLError as e: + raise CLIInternalError(f"Failed to parse schema YAML: {str(e)}") + except ImportError: + raise CLIInternalError("PyYAML is required to parse schema. Please install it with: pip install PyYAML") + + except Exception as e: + raise CLIInternalError(f"Error getting configuration placeholder: {str(e)}") + def on_200(self, session): data = self.deserialize_http_content(session) self.ctx.set_var( @@ -265,14 +471,6 @@ def on_200(self, session): _schema_on_200 = None - @classmethod - def _build_schema_on_404(cls): - cls._schema_on_200 = AAZObjectType() - _schema_on_200 = cls._schema_on_200 - _schema_on_200.properties = AAZFreeFormDictType() - return cls._schema_on_200 - - @classmethod def _build_schema_on_200(cls): if cls._schema_on_200 is not None: @@ -290,7 +488,9 @@ def _build_schema_on_200(cls): _schema_on_200.name = AAZStrType( flags={"read_only": True}, ) - _schema_on_200.properties = AAZFreeFormDictType() + _schema_on_200.properties = AAZObjectType( + flags={"required": True}, + ) _schema_on_200.system_data = AAZObjectType( serialized_name="systemData", flags={"read_only": True}, @@ -300,8 +500,10 @@ def _build_schema_on_200(cls): flags={"read_only": True}, ) - - + properties = cls._schema_on_200.properties + properties.values = AAZStrType( + flags={"required": True}, + ) system_data = cls._schema_on_200.system_data system_data.created_at = AAZStrType( diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_show.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_show.py index 39c3eebab75..8f326bb1ab2 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_show.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_config_show.py @@ -9,6 +9,8 @@ # flake8: noqa from azure.cli.core.aaz import * +from azure.cli.core.azclierror import CLIInternalError +from ._config_helper import ConfigurationHelper @register_command( @@ -18,7 +20,9 @@ class ShowConfig(AAZCommand): """To get a configurations available at specified hierarchical entity :example: Show a Configuration - az workload-orchestration configuration show -g rg1 --target-name target1 --solution-template-name solutionTemplate1 + az workload-orchestration configuration show -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name template1 --version 1.0.0 + :example: Show a Solution Template Configuration + az workload-orchestration configuration show -g rg1 --hierarchy-id "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Edge/sites/site1" --template-resource-group rg1 --template-name solutionTemplate1 --version 1.0.0 --solution """ _aaz_info = { @@ -47,22 +51,32 @@ def _build_arguments_schema(cls, *args, **kwargs): _args_schema.resource_group = AAZResourceGroupNameArg( required=True, ) - _args_schema.solution_name = AAZStrArg( - options=["--solution-template-name"], - help="The name of the Solution, This is required only to get solution configurations", - # required=True, - id_part="name", + + _args_schema.hierarchy_id = AAZStrArg( + options=["--hierarchy-id"], + help="The ARM ID for the target or site at which values needs to be shown", + required=True + ) + + _args_schema.template_subscription = AAZStrArg( + options=["--template_subscription"], + help="Subscription ID for the template. Only needed if the subscription ID for the template is different than the current subscription ID.", + required=False, fmt=AAZStrArgFormat( - pattern="^[a-zA-Z0-9-]{3,24}$", + pattern="^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", ), ) - _args_schema = cls._args_schema - _args_schema.level_name = AAZStrArg( - options=["--target-name"], - help="The Target or Site name at which values needs to be set", + _args_schema.template_resource_group = AAZStrArg( + options=["--template-resource-group"], + help="Resource group name for the template.", + required=True, + ) - required = True, + _args_schema.template_name = AAZStrArg( + options=["--template-name"], + help="The name of the Template (Solution template or Configuration template) to show.", + required=True, fmt=AAZStrArgFormat( pattern="^[a-zA-Z0-9-]{3,24}$", ), @@ -79,23 +93,21 @@ def _build_arguments_schema(cls, *args, **kwargs): # ) _args_schema.version = AAZStrArg( options=["--version"], - help="The version of the solution to show configuration for. Defaults to 'version1' if not specified." + help="Version of the template.", + required=True + ) + + _args_schema.solution = AAZBoolArg( + options=["--solution"], + help="Flag to indicate that we are showing a solution. If not provided, we are showing a config template.", + required=False, ) + return cls._args_schema def _execute_operations(self): self.pre_operations() - version = self.ctx.args.version - if version is not None and str(version).lower() != "undefined": - self.SolutionRevisionGet(ctx=self.ctx)() - else: - config_name = str(self.ctx.args.level_name) - if len(config_name) > 18: - config_name = config_name[:18] + "Config" - else: - config_name = config_name + "Config" - self.ctx.args.level_name = config_name - self.SolutionsGet(ctx=self.ctx)() + self.SolutionsGet(ctx=self.ctx)() self.post_operations() @register_callback @@ -109,37 +121,40 @@ def post_operations(self): def _output(self, *args, **kwargs): result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) print(result["properties"]["values"]) + pass class SolutionsGet(AAZHttpOperation): CLIENT_TYPE = "MgmtClient" def __call__(self, *args, **kwargs): + # Get configuration ID using the existing client + self.configuration_id = ConfigurationHelper.getConfigurationId(self.ctx.args.hierarchy_id, self.client) + + # Get template unique identifier for dynamic configuration name + template_subscription = self.ctx.args.template_subscription if self.ctx.args.template_subscription else self.ctx.subscription_id + solution_flag = self.ctx.args.solution if self.ctx.args.solution else False + self.dynamic_configuration_name = ConfigurationHelper.getTemplateUniqueIdentifier( + template_subscription, + self.ctx.args.template_resource_group, + self.ctx.args.template_name, + solution_flag, + self.client + ) + request = self.make_request() session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: return self.on_200(session) - config = dict() - config["properties"] = dict() - config["properties"]["values"] = "{}" - # # config.config = AAZStrType() - # # config.config = "[]" - if session.http_response.status_code in [404]: - self.ctx.set_var( - "instance", - config, - schema_builder=self._build_schema_on_404 - ) - # return else: return self.on_error(session.http_response) - @property def url(self): - return self.client.format_url( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/configurations/{configName}/DynamicConfigurations/{solutionName}/versions/version1", - **self.url_parameters - ) + # Use the configuration ID and append the dynamic configuration path + base_url = self.configuration_id + dynamic_config_path = f"/dynamicConfigurations/{self.dynamic_configuration_name}/versions/{self.ctx.args.version}" + return base_url + dynamic_config_path @property def method(self): @@ -149,32 +164,6 @@ def method(self): def error_format(self): return "MgmtErrorFormat" - @property - def url_parameters(self): - sol_name = "common" - if has_value(self.ctx.args.solution_name): - sol_name = self.ctx.args.solution_name - - parameters = { - **self.serialize_url_param( - "resourceGroupName", self.ctx.args.resource_group, - required=True, - ), - **self.serialize_url_param( - "solutionName", sol_name, - required=True, - ), - **self.serialize_url_param( - "configName", self.ctx.args.level_name, - required=True, - ), - **self.serialize_url_param( - "subscriptionId", self.ctx.subscription_id, - required=True, - ), - } - return parameters - @property def query_parameters(self): parameters = { @@ -204,14 +193,6 @@ def on_200(self, session): _schema_on_200 = None - @classmethod - def _build_schema_on_404(cls): - cls._schema_on_200 = AAZObjectType() - _schema_on_200 = cls._schema_on_200 - _schema_on_200.properties = AAZFreeFormDictType() - return cls._schema_on_200 - - @classmethod def _build_schema_on_200(cls): if cls._schema_on_200 is not None: @@ -239,9 +220,6 @@ def _build_schema_on_200(cls): flags={"read_only": True}, ) - - - system_data = cls._schema_on_200.system_data system_data.created_at = AAZStrType( serialized_name="createdAt", @@ -264,7 +242,6 @@ def _build_schema_on_200(cls): tags = cls._schema_on_200.tags tags.Element = AAZStrType() - return cls._schema_on_200 class SolutionRevisionGet(AAZHttpOperation): diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_schema_show.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_schema_show.py new file mode 100644 index 00000000000..5e60935f1d4 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/_schema_show.py @@ -0,0 +1,236 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * +from azure.cli.core.azclierror import CLIInternalError +from ._config_helper import ConfigurationHelper + +@register_command( + "workload-orchestration configuration schema show", + is_preview=False, +) +class SchemaShow(AAZCommand): + """Show the schema placeholder for a configuration template or solution template + :example: Show schema for a Configuration Template + az workload-orchestration configuration schema show --template-resource-group rg1 --template-name template1 --version 1.0.0 + :example: Show schema for a Solution Template + az workload-orchestration configuration schema show --template-resource-group rg1 --template-name solutionTemplate1 --version 1.0.0 --solution + :example: Show schema for a template in different subscription + az workload-orchestration configuration schema show --template-subscription sub1 --template-resource-group rg1 --template-name template1 --version 1.0.0 + """ + + _aaz_info = { + "version": "2025-08-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/configTemplates/{}/versions/{}/configTemplateSchemas/default", "2025-08-01"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/solutionTemplates/{}/versions/{}/solutionSchemas/default", "2025-08-01"], + ] + } + + def _handler(self, command_args): + super()._handler(command_args) + self._execute_operations() + return self._output() + + _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) + + # define Arg Group "" + + _args_schema = cls._args_schema + + _args_schema.template_subscription = AAZStrArg( + options=["--template-subscription"], + help="Subscription ID for the template. Only needed if the subscription ID for the template is different than the current subscription ID.", + required=False, + fmt=AAZStrArgFormat( + pattern="^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + ), + ) + + _args_schema.template_resource_group = AAZStrArg( + options=["--template-resource-group"], + help="Resource group name for the template.", + required=True, + ) + + _args_schema.template_name = AAZStrArg( + options=["--template-name"], + help="The name of the Template (Solution template or Configuration template) to show schema for.", + required=True, + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + + _args_schema.version = AAZStrArg( + options=["--version"], + help="Version of the template.", + required=True + ) + + _args_schema.solution = AAZBoolArg( + options=["--solution"], + help="Flag to indicate that we are showing schema for a solution template. If not provided, we are showing schema for a config template.", + required=False, + ) + + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + self.SchemaGet(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + # Parse the schema to extract just the configs section + try: + import yaml + schema_data = yaml.safe_load(result["properties"]["value"]) + configs = schema_data.get("rules", {}).get("configs", {}) + + # Convert configs directly to YAML string without nesting under rules/configs + if configs: + configs_yaml = yaml.dump(configs, default_flow_style=False, allow_unicode=True) + print(configs_yaml) + else: + print("No configuration keys found in schema") + except Exception as e: + # Fallback to showing raw schema if parsing fails + print(result["properties"]["value"]) + + class SchemaGet(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + # Get template subscription or use current subscription + template_subscription = self.ctx.args.template_subscription if self.ctx.args.template_subscription else self.ctx.subscription_id + solution_flag = self.ctx.args.solution if self.ctx.args.solution else False + + # Get schema using ConfigurationHelper + try: + schema_value = ConfigurationHelper.getTemplateSchema( + template_subscription, + self.ctx.args.template_resource_group, + self.ctx.args.template_name, + self.ctx.args.version, + solution_flag, + self.client + ) + + # Create a mock response structure similar to what AAZ expects + schema_response = { + "properties": { + "value": schema_value + } + } + + # Set the response data for output + self.ctx.set_var( + "instance", + schema_response, + schema_builder=self._build_schema_on_200 + ) + + except Exception as e: + raise CLIInternalError(f"Failed to get schema: {str(e)}") + + @property + def url(self): + # This won't be used since we're making the request manually + return "" + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.location = AAZStrType() + _schema_on_200.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.properties = AAZObjectType( + flags={"required": True}, + ) + _schema_on_200.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200.tags = AAZDictType() + _schema_on_200.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.properties + properties.value = AAZStrType( + flags={"required": True}, + ) + + system_data = cls._schema_on_200.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + tags = cls._schema_on_200.tags + tags.Element = AAZStrType() + + return cls._schema_on_200 + + +class _SchemaShowHelper: + """Helper class for SchemaShow""" + + +__all__ = ["SchemaShow"] \ No newline at end of file diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/schema/__cmd_group.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/schema/__cmd_group.py new file mode 100644 index 00000000000..fa5dc11f389 --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/configuration/schema/__cmd_group.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "workload-orchestration configuration schema", +) +class __CMDGroup(AAZCommandGroup): + """workload-orchestration configuration schema helps to manage configuration template schemas + """ + pass + + +__all__ = ["__CMDGroup"] \ No newline at end of file