From 06f5f1ed6930708f04464e977e3a6e935cf0a936 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 27 Oct 2025 09:49:50 -0700 Subject: [PATCH 01/15] Add stub code --- src/change-state/HISTORY.rst | 8 + src/change-state/README.md | 5 + .../azext_change_state/__init__.py | 42 + src/change-state/azext_change_state/_help.py | 11 + .../azext_change_state/_params.py | 13 + .../azext_change_state/aaz/__init__.py | 6 + .../azext_change_state/aaz/latest/__init__.py | 10 + .../aaz/latest/change_safety/__cmd_group.py | 23 + .../aaz/latest/change_safety/__init__.py | 11 + .../change_safety/change_state/__cmd_group.py | 23 + .../change_safety/change_state/__init__.py | 15 + .../change_safety/change_state/_create.py | 1030 +++++++++++++++++ .../change_safety/change_state/_delete.py | 248 ++++ .../change_safety/change_state/_show.py | 610 ++++++++++ .../change_safety/change_state/_update.py | 1012 ++++++++++++++++ .../azext_change_state/azext_metadata.json | 4 + .../azext_change_state/commands.py | 15 + src/change-state/azext_change_state/custom.py | 14 + .../azext_change_state/tests/__init__.py | 6 + .../tests/latest/__init__.py | 6 + .../tests/latest/test_change_state.py | 13 + src/change-state/setup.cfg | 1 + src/change-state/setup.py | 49 + 23 files changed, 3175 insertions(+) create mode 100644 src/change-state/HISTORY.rst create mode 100644 src/change-state/README.md create mode 100644 src/change-state/azext_change_state/__init__.py create mode 100644 src/change-state/azext_change_state/_help.py create mode 100644 src/change-state/azext_change_state/_params.py create mode 100644 src/change-state/azext_change_state/aaz/__init__.py create mode 100644 src/change-state/azext_change_state/aaz/latest/__init__.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py create mode 100644 src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py create mode 100644 src/change-state/azext_change_state/azext_metadata.json create mode 100644 src/change-state/azext_change_state/commands.py create mode 100644 src/change-state/azext_change_state/custom.py create mode 100644 src/change-state/azext_change_state/tests/__init__.py create mode 100644 src/change-state/azext_change_state/tests/latest/__init__.py create mode 100644 src/change-state/azext_change_state/tests/latest/test_change_state.py create mode 100644 src/change-state/setup.cfg create mode 100644 src/change-state/setup.py diff --git a/src/change-state/HISTORY.rst b/src/change-state/HISTORY.rst new file mode 100644 index 00000000000..abbff5a61a7 --- /dev/null +++ b/src/change-state/HISTORY.rst @@ -0,0 +1,8 @@ +.. :changelog: + +Release History +=============== + +1.0.0b1 +++++++ +* Initial release. \ No newline at end of file diff --git a/src/change-state/README.md b/src/change-state/README.md new file mode 100644 index 00000000000..9e4e42b3250 --- /dev/null +++ b/src/change-state/README.md @@ -0,0 +1,5 @@ +# Azure CLI ChangeState Extension # +This is an extension to Azure CLI to manage ChangeState resources. + +## How to use ## +Please add commands usage here. \ No newline at end of file diff --git a/src/change-state/azext_change_state/__init__.py b/src/change-state/azext_change_state/__init__.py new file mode 100644 index 00000000000..a2755a92cdb --- /dev/null +++ b/src/change-state/azext_change_state/__init__.py @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- + +from azure.cli.core import AzCommandsLoader +from azext_change_state._help import helps # pylint: disable=unused-import + + +class ChangeStateCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + custom_command_type = CliCommandType( + operations_tmpl='azext_change_state.custom#{}') + super().__init__(cli_ctx=cli_ctx, + custom_command_type=custom_command_type) + + def load_command_table(self, args): + from azext_change_state.commands import load_command_table + from azure.cli.core.aaz import load_aaz_command_table + try: + from . import aaz + except ImportError: + aaz = None + if aaz: + load_aaz_command_table( + loader=self, + aaz_pkg_name=aaz.__name__, + args=args + ) + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azext_change_state._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = ChangeStateCommandsLoader diff --git a/src/change-state/azext_change_state/_help.py b/src/change-state/azext_change_state/_help.py new file mode 100644 index 00000000000..126d5d00714 --- /dev/null +++ b/src/change-state/azext_change_state/_help.py @@ -0,0 +1,11 @@ +# -------------------------------------------------------------------------------------------- +# 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: disable=line-too-long +# pylint: disable=too-many-lines + +from knack.help_files import helps # pylint: disable=unused-import diff --git a/src/change-state/azext_change_state/_params.py b/src/change-state/azext_change_state/_params.py new file mode 100644 index 00000000000..cfcec717c9c --- /dev/null +++ b/src/change-state/azext_change_state/_params.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# 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: disable=too-many-lines +# pylint: disable=too-many-statements + + +def load_arguments(self, _): # pylint: disable=unused-argument + pass diff --git a/src/change-state/azext_change_state/aaz/__init__.py b/src/change-state/azext_change_state/aaz/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- diff --git a/src/change-state/azext_change_state/aaz/latest/__init__.py b/src/change-state/azext_change_state/aaz/latest/__init__.py new file mode 100644 index 00000000000..f6acc11aa4e --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/__init__.py @@ -0,0 +1,10 @@ +# -------------------------------------------------------------------------------------------- +# 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 + diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py b/src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py new file mode 100644 index 00000000000..c6dd5b296ba --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/__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( + "change-safety", +) +class __CMDGroup(AAZCommandGroup): + """Manage Change Safety + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py b/src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py new file mode 100644 index 00000000000..5a9d61963d6 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py @@ -0,0 +1,11 @@ +# -------------------------------------------------------------------------------------------- +# 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 * diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py new file mode 100644 index 00000000000..00d6ce9e052 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__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( + "change-safety change-state", +) +class __CMDGroup(AAZCommandGroup): + """Manage Change State + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py new file mode 100644 index 00000000000..a3db3e36481 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py @@ -0,0 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# 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 ._create import * +from ._delete import * +from ._show import * +from ._update import * diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py new file mode 100644 index 00000000000..35c38872ced --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py @@ -0,0 +1,1030 @@ +# -------------------------------------------------------------------------------------------- +# 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( + "change-safety change-state create", +) +class Create(AAZCommand): + """Create a ChangeState + """ + + _aaz_info = { + "version": "2025-09-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ] + } + + 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.change_state_name = AAZStrArg( + options=["-n", "--name", "--change-state-name"], + help="The name of the ChangeState resource.", + required=True, + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,100}$", + max_length=100, + min_length=3, + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg() + + # define Arg Group "Properties" + + _args_schema = cls._args_schema + _args_schema.additional_data = AAZObjectArg( + options=["--additional-data"], + arg_group="Properties", + help="Additional metadata for the change required for various orchestration tools.", + blank={}, + ) + _args_schema.anticipated_end_time = AAZDateTimeArg( + options=["--anticipated-end-time"], + arg_group="Properties", + help="Expected completion time when the change should be finished, in ISO 8601 format.", + fmt=AAZDateTimeFormat( + protocol="iso", + ), + ) + _args_schema.anticipated_start_time = AAZDateTimeArg( + options=["--anticipated-start-time"], + arg_group="Properties", + help="Expected start time when the change execution should begin, in ISO 8601 format.", + fmt=AAZDateTimeFormat( + protocol="iso", + ), + ) + _args_schema.change_type = AAZStrArg( + options=["--change-type"], + arg_group="Properties", + help="Describes the nature of the change.", + enum={"AppDeployment": "AppDeployment", "Config": "Config", "ManualTouch": "ManualTouch", "PolicyDeployment": "PolicyDeployment"}, + ) + _args_schema.comments = AAZStrArg( + options=["--comments"], + arg_group="Properties", + help="Comments about the last update to the changeState resource.", + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _args_schema.description = AAZStrArg( + options=["--description"], + arg_group="Properties", + help="Brief description about the change.", + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _args_schema.links = AAZListArg( + options=["--links"], + arg_group="Properties", + help="Collection of related links for the change.", + ) + _args_schema.orchestration_tool = AAZStrArg( + options=["--orchestration-tool"], + arg_group="Properties", + help="Tool used for deployment orchestration of this change.", + ) + _args_schema.parameters = AAZDictArg( + options=["--parameters"], + arg_group="Properties", + help="Schema of parameters that will be provided for each stageProgression.", + ) + _args_schema.release_label = AAZStrArg( + options=["--release-label"], + arg_group="Properties", + help="Label for the release associated with this change.", + ) + _args_schema.rollout_type = AAZStrArg( + options=["--rollout-type"], + arg_group="Properties", + help="Describes the type of the rollout used for the change.", + enum={"Emergency": "Emergency", "Hotfix": "Hotfix", "Normal": "Normal"}, + ) + _args_schema.stage_map = AAZObjectArg( + options=["--stage-map"], + arg_group="Properties", + help="Reference to the StageMap, defining progression.", + ) + + links = cls._args_schema.links + links.Element = AAZObjectArg() + + _element = cls._args_schema.links.Element + _element.description = AAZStrArg( + options=["description"], + help="Description or note about the link.", + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _element.name = AAZStrArg( + options=["name"], + help="name of the link.", + required=True, + ) + _element.uri = AAZStrArg( + options=["uri"], + help="URL or comma separated URLs for the link.", + required=True, + ) + + parameters = cls._args_schema.parameters + parameters.Element = AAZObjectArg() + + _element = cls._args_schema.parameters.Element + _element.array = AAZObjectArg( + options=["array"], + ) + _element.metadata = AAZDictArg( + options=["metadata"], + help="user-specified parameter metadata", + ) + _element.number = AAZObjectArg( + options=["number"], + ) + _element.object = AAZObjectArg( + options=["object"], + ) + _element.string = AAZObjectArg( + options=["string"], + ) + + array = cls._args_schema.parameters.Element.array + array.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + ) + array.default_value = AAZListArg( + options=["default-value"], + help="Default value for the parameter.", + ) + + allowed_values = cls._args_schema.parameters.Element.array.allowed_values + allowed_values.Element = AAZAnyTypeArg() + + default_value = cls._args_schema.parameters.Element.array.default_value + default_value.Element = AAZAnyTypeArg() + + metadata = cls._args_schema.parameters.Element.metadata + metadata.Element = AAZStrArg() + + number = cls._args_schema.parameters.Element.number + number.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + ) + number.default_value = AAZIntArg( + options=["default-value"], + help="Default value for the parameter.", + ) + + allowed_values = cls._args_schema.parameters.Element.number.allowed_values + allowed_values.Element = AAZIntArg() + + object = cls._args_schema.parameters.Element.object + object.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + ) + object.default_value = AAZObjectArg( + options=["default-value"], + help="Default value for the parameter.", + blank={}, + ) + + allowed_values = cls._args_schema.parameters.Element.object.allowed_values + allowed_values.Element = AAZDictArg() + + _element = cls._args_schema.parameters.Element.object.allowed_values.Element + _element.Element = AAZAnyTypeArg() + + string = cls._args_schema.parameters.Element.string + string.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + ) + string.default_value = AAZStrArg( + options=["default-value"], + help="Default value for the parameter.", + ) + + allowed_values = cls._args_schema.parameters.Element.string.allowed_values + allowed_values.Element = AAZStrArg() + + stage_map = cls._args_schema.stage_map + stage_map.parameters = AAZDictArg( + options=["parameters"], + help="Key value pairs of parameter names & their values for the stageMap referenced by the resourceId field.", + ) + stage_map.resource_id = AAZStrArg( + options=["resource-id"], + help="ARM resource ID for the nested stagemap resource.", + ) + + parameters = cls._args_schema.stage_map.parameters + parameters.Element = AAZAnyTypeArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + self.ChangeStatesCreateOrUpdateAtSubscriptionLevel(ctx=self.ctx)() + if condition_1: + self.ChangeStatesCreateOrUpdate(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 ChangeStatesCreateOrUpdateAtSubscriptionLevel(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, 201]: + return self.on_200_201(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "PUT" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_name, + 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-09-01-preview", + 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("properties", AAZObjectType) + + properties = _builder.get(".properties") + if properties is not None: + properties.set_prop("additionalData", AAZObjectType, ".additional_data") + properties.set_prop("anticipatedEndTime", AAZStrType, ".anticipated_end_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("anticipatedStartTime", AAZStrType, ".anticipated_start_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("changeType", AAZStrType, ".change_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("comments", AAZStrType, ".comments") + properties.set_prop("description", AAZStrType, ".description") + properties.set_prop("links", AAZListType, ".links") + properties.set_prop("orchestrationTool", AAZStrType, ".orchestration_tool") + properties.set_prop("parameters", AAZDictType, ".parameters") + properties.set_prop("releaseLabel", AAZStrType, ".release_label") + properties.set_prop("rolloutType", AAZStrType, ".rollout_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("stageMap", AAZObjectType, ".stage_map") + + links = _builder.get(".properties.links") + if links is not None: + links.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.links[]") + if _elements is not None: + _elements.set_prop("description", AAZStrType, ".description") + _elements.set_prop("name", AAZStrType, ".name", typ_kwargs={"flags": {"required": True}}) + _elements.set_prop("uri", AAZStrType, ".uri", typ_kwargs={"flags": {"required": True}}) + + parameters = _builder.get(".properties.parameters") + if parameters is not None: + parameters.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.parameters{}") + if _elements is not None: + _elements.set_prop("metadata", AAZDictType, ".metadata") + _elements.set_const("type", "array", AAZStrType, ".array", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "number", AAZStrType, ".number", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "object", AAZStrType, ".object", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "string", AAZStrType, ".string", typ_kwargs={"flags": {"required": True}}) + _elements.discriminate_by("type", "array") + _elements.discriminate_by("type", "number") + _elements.discriminate_by("type", "object") + _elements.discriminate_by("type", "string") + + metadata = _builder.get(".properties.parameters{}.metadata") + if metadata is not None: + metadata.set_elements(AAZStrType, ".") + + disc_array = _builder.get(".properties.parameters{}{type:array}") + if disc_array is not None: + disc_array.set_prop("allowedValues", AAZListType, ".array.allowed_values") + disc_array.set_prop("defaultValue", AAZListType, ".array.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:array}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZAnyType, ".") + + default_value = _builder.get(".properties.parameters{}{type:array}.defaultValue") + if default_value is not None: + default_value.set_elements(AAZAnyType, ".") + + disc_number = _builder.get(".properties.parameters{}{type:number}") + if disc_number is not None: + disc_number.set_prop("allowedValues", AAZListType, ".number.allowed_values") + disc_number.set_prop("defaultValue", AAZIntType, ".number.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:number}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZIntType, ".") + + disc_object = _builder.get(".properties.parameters{}{type:object}") + if disc_object is not None: + disc_object.set_prop("allowedValues", AAZListType, ".object.allowed_values") + disc_object.set_prop("defaultValue", AAZObjectType, ".object.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:object}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZDictType, ".") + + _elements = _builder.get(".properties.parameters{}{type:object}.allowedValues[]") + if _elements is not None: + _elements.set_elements(AAZAnyType, ".") + + disc_string = _builder.get(".properties.parameters{}{type:string}") + if disc_string is not None: + disc_string.set_prop("allowedValues", AAZListType, ".string.allowed_values") + disc_string.set_prop("defaultValue", AAZStrType, ".string.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:string}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZStrType, ".") + + stage_map = _builder.get(".properties.stageMap") + if stage_map is not None: + stage_map.set_prop("parameters", AAZDictType, ".parameters") + stage_map.set_prop("resourceId", AAZStrType, ".resource_id") + + parameters = _builder.get(".properties.stageMap.parameters") + if parameters is not None: + parameters.set_elements(AAZAnyType, ".") + + return self.serialize_content(_content_value) + + def on_200_201(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_201 + ) + + _schema_on_200_201 = None + + @classmethod + def _build_schema_on_200_201(cls): + if cls._schema_on_200_201 is not None: + return cls._schema_on_200_201 + + cls._schema_on_200_201 = AAZObjectType() + + _schema_on_200_201 = cls._schema_on_200_201 + _schema_on_200_201.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200_201.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200_201.properties = AAZObjectType() + _schema_on_200_201.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200_201.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200_201.properties + properties.additional_data = AAZObjectType( + serialized_name="additionalData", + ) + properties.anticipated_end_time = AAZStrType( + serialized_name="anticipatedEndTime", + flags={"required": True}, + ) + properties.anticipated_start_time = AAZStrType( + serialized_name="anticipatedStartTime", + flags={"required": True}, + ) + properties.change_definition = AAZObjectType( + serialized_name="changeDefinition", + flags={"required": True}, + ) + properties.change_type = AAZStrType( + serialized_name="changeType", + flags={"required": True}, + ) + properties.comments = AAZStrType() + properties.description = AAZStrType() + properties.links = AAZListType() + properties.orchestration_tool = AAZStrType( + serialized_name="orchestrationTool", + ) + properties.parameters = AAZDictType() + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.release_label = AAZStrType( + serialized_name="releaseLabel", + ) + properties.rollout_type = AAZStrType( + serialized_name="rolloutType", + flags={"required": True}, + ) + properties.stage_map = AAZObjectType( + serialized_name="stageMap", + ) + properties.stage_map_snapshot = AAZListType( + serialized_name="stageMapSnapshot", + flags={"read_only": True}, + ) + properties.status = AAZStrType( + flags={"read_only": True}, + ) + + change_definition = cls._schema_on_200_201.properties.change_definition + change_definition.details = AAZObjectType( + flags={"required": True}, + ) + change_definition.kind = AAZStrType( + flags={"required": True}, + ) + change_definition.name = AAZStrType( + flags={"required": True}, + ) + + links = cls._schema_on_200_201.properties.links + links.Element = AAZObjectType() + + _element = cls._schema_on_200_201.properties.links.Element + _element.description = AAZStrType() + _element.name = AAZStrType( + flags={"required": True}, + ) + _element.uri = AAZStrType( + flags={"required": True}, + ) + + parameters = cls._schema_on_200_201.properties.parameters + parameters.Element = AAZObjectType() + + _element = cls._schema_on_200_201.properties.parameters.Element + _element.metadata = AAZDictType() + _element.type = AAZStrType( + flags={"required": True}, + ) + + metadata = cls._schema_on_200_201.properties.parameters.Element.metadata + metadata.Element = AAZStrType() + + disc_array = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array") + disc_array.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_array.default_value = AAZListType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array").allowed_values + allowed_values.Element = AAZAnyType() + + default_value = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array").default_value + default_value.Element = AAZAnyType() + + disc_number = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "number") + disc_number.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_number.default_value = AAZIntType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "number").allowed_values + allowed_values.Element = AAZIntType() + + disc_object = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object") + disc_object.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_object.default_value = AAZObjectType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object").allowed_values + allowed_values.Element = AAZDictType() + + _element = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object").allowed_values.Element + _element.Element = AAZAnyType() + + disc_string = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "string") + disc_string.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_string.default_value = AAZStrType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "string").allowed_values + allowed_values.Element = AAZStrType() + + stage_map = cls._schema_on_200_201.properties.stage_map + stage_map.parameters = AAZDictType() + stage_map.resource_id = AAZStrType( + serialized_name="resourceId", + ) + + parameters = cls._schema_on_200_201.properties.stage_map.parameters + parameters.Element = AAZAnyType() + + stage_map_snapshot = cls._schema_on_200_201.properties.stage_map_snapshot + stage_map_snapshot.Element = AAZAnyType() + + system_data = cls._schema_on_200_201.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_201 + + class ChangeStatesCreateOrUpdate(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, 201]: + return self.on_200_201(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "PUT" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_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-09-01-preview", + 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("properties", AAZObjectType) + + properties = _builder.get(".properties") + if properties is not None: + properties.set_prop("additionalData", AAZObjectType, ".additional_data") + properties.set_prop("anticipatedEndTime", AAZStrType, ".anticipated_end_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("anticipatedStartTime", AAZStrType, ".anticipated_start_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("changeType", AAZStrType, ".change_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("comments", AAZStrType, ".comments") + properties.set_prop("description", AAZStrType, ".description") + properties.set_prop("links", AAZListType, ".links") + properties.set_prop("orchestrationTool", AAZStrType, ".orchestration_tool") + properties.set_prop("parameters", AAZDictType, ".parameters") + properties.set_prop("releaseLabel", AAZStrType, ".release_label") + properties.set_prop("rolloutType", AAZStrType, ".rollout_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("stageMap", AAZObjectType, ".stage_map") + + links = _builder.get(".properties.links") + if links is not None: + links.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.links[]") + if _elements is not None: + _elements.set_prop("description", AAZStrType, ".description") + _elements.set_prop("name", AAZStrType, ".name", typ_kwargs={"flags": {"required": True}}) + _elements.set_prop("uri", AAZStrType, ".uri", typ_kwargs={"flags": {"required": True}}) + + parameters = _builder.get(".properties.parameters") + if parameters is not None: + parameters.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.parameters{}") + if _elements is not None: + _elements.set_prop("metadata", AAZDictType, ".metadata") + _elements.set_const("type", "array", AAZStrType, ".array", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "number", AAZStrType, ".number", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "object", AAZStrType, ".object", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "string", AAZStrType, ".string", typ_kwargs={"flags": {"required": True}}) + _elements.discriminate_by("type", "array") + _elements.discriminate_by("type", "number") + _elements.discriminate_by("type", "object") + _elements.discriminate_by("type", "string") + + metadata = _builder.get(".properties.parameters{}.metadata") + if metadata is not None: + metadata.set_elements(AAZStrType, ".") + + disc_array = _builder.get(".properties.parameters{}{type:array}") + if disc_array is not None: + disc_array.set_prop("allowedValues", AAZListType, ".array.allowed_values") + disc_array.set_prop("defaultValue", AAZListType, ".array.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:array}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZAnyType, ".") + + default_value = _builder.get(".properties.parameters{}{type:array}.defaultValue") + if default_value is not None: + default_value.set_elements(AAZAnyType, ".") + + disc_number = _builder.get(".properties.parameters{}{type:number}") + if disc_number is not None: + disc_number.set_prop("allowedValues", AAZListType, ".number.allowed_values") + disc_number.set_prop("defaultValue", AAZIntType, ".number.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:number}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZIntType, ".") + + disc_object = _builder.get(".properties.parameters{}{type:object}") + if disc_object is not None: + disc_object.set_prop("allowedValues", AAZListType, ".object.allowed_values") + disc_object.set_prop("defaultValue", AAZObjectType, ".object.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:object}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZDictType, ".") + + _elements = _builder.get(".properties.parameters{}{type:object}.allowedValues[]") + if _elements is not None: + _elements.set_elements(AAZAnyType, ".") + + disc_string = _builder.get(".properties.parameters{}{type:string}") + if disc_string is not None: + disc_string.set_prop("allowedValues", AAZListType, ".string.allowed_values") + disc_string.set_prop("defaultValue", AAZStrType, ".string.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:string}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZStrType, ".") + + stage_map = _builder.get(".properties.stageMap") + if stage_map is not None: + stage_map.set_prop("parameters", AAZDictType, ".parameters") + stage_map.set_prop("resourceId", AAZStrType, ".resource_id") + + parameters = _builder.get(".properties.stageMap.parameters") + if parameters is not None: + parameters.set_elements(AAZAnyType, ".") + + return self.serialize_content(_content_value) + + def on_200_201(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_201 + ) + + _schema_on_200_201 = None + + @classmethod + def _build_schema_on_200_201(cls): + if cls._schema_on_200_201 is not None: + return cls._schema_on_200_201 + + cls._schema_on_200_201 = AAZObjectType() + + _schema_on_200_201 = cls._schema_on_200_201 + _schema_on_200_201.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200_201.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200_201.properties = AAZObjectType() + _schema_on_200_201.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200_201.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200_201.properties + properties.additional_data = AAZObjectType( + serialized_name="additionalData", + ) + properties.anticipated_end_time = AAZStrType( + serialized_name="anticipatedEndTime", + flags={"required": True}, + ) + properties.anticipated_start_time = AAZStrType( + serialized_name="anticipatedStartTime", + flags={"required": True}, + ) + properties.change_definition = AAZObjectType( + serialized_name="changeDefinition", + flags={"required": True}, + ) + properties.change_type = AAZStrType( + serialized_name="changeType", + flags={"required": True}, + ) + properties.comments = AAZStrType() + properties.description = AAZStrType() + properties.links = AAZListType() + properties.orchestration_tool = AAZStrType( + serialized_name="orchestrationTool", + ) + properties.parameters = AAZDictType() + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.release_label = AAZStrType( + serialized_name="releaseLabel", + ) + properties.rollout_type = AAZStrType( + serialized_name="rolloutType", + flags={"required": True}, + ) + properties.stage_map = AAZObjectType( + serialized_name="stageMap", + ) + properties.stage_map_snapshot = AAZListType( + serialized_name="stageMapSnapshot", + flags={"read_only": True}, + ) + properties.status = AAZStrType( + flags={"read_only": True}, + ) + + change_definition = cls._schema_on_200_201.properties.change_definition + change_definition.details = AAZObjectType( + flags={"required": True}, + ) + change_definition.kind = AAZStrType( + flags={"required": True}, + ) + change_definition.name = AAZStrType( + flags={"required": True}, + ) + + links = cls._schema_on_200_201.properties.links + links.Element = AAZObjectType() + + _element = cls._schema_on_200_201.properties.links.Element + _element.description = AAZStrType() + _element.name = AAZStrType( + flags={"required": True}, + ) + _element.uri = AAZStrType( + flags={"required": True}, + ) + + parameters = cls._schema_on_200_201.properties.parameters + parameters.Element = AAZObjectType() + + _element = cls._schema_on_200_201.properties.parameters.Element + _element.metadata = AAZDictType() + _element.type = AAZStrType( + flags={"required": True}, + ) + + metadata = cls._schema_on_200_201.properties.parameters.Element.metadata + metadata.Element = AAZStrType() + + disc_array = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array") + disc_array.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_array.default_value = AAZListType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array").allowed_values + allowed_values.Element = AAZAnyType() + + default_value = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "array").default_value + default_value.Element = AAZAnyType() + + disc_number = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "number") + disc_number.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_number.default_value = AAZIntType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "number").allowed_values + allowed_values.Element = AAZIntType() + + disc_object = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object") + disc_object.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_object.default_value = AAZObjectType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object").allowed_values + allowed_values.Element = AAZDictType() + + _element = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "object").allowed_values.Element + _element.Element = AAZAnyType() + + disc_string = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "string") + disc_string.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_string.default_value = AAZStrType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200_201.properties.parameters.Element.discriminate_by("type", "string").allowed_values + allowed_values.Element = AAZStrType() + + stage_map = cls._schema_on_200_201.properties.stage_map + stage_map.parameters = AAZDictType() + stage_map.resource_id = AAZStrType( + serialized_name="resourceId", + ) + + parameters = cls._schema_on_200_201.properties.stage_map.parameters + parameters.Element = AAZAnyType() + + stage_map_snapshot = cls._schema_on_200_201.properties.stage_map_snapshot + stage_map_snapshot.Element = AAZAnyType() + + system_data = cls._schema_on_200_201.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_201 + + +class _CreateHelper: + """Helper class for Create""" + + +__all__ = ["Create"] diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py new file mode 100644 index 00000000000..be78b96198f --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py @@ -0,0 +1,248 @@ +# -------------------------------------------------------------------------------------------- +# 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( + "change-safety change-state delete", + confirmation="Are you sure you want to perform this operation?", +) +class Delete(AAZCommand): + """Delete a ChangeState + """ + + _aaz_info = { + "version": "2025-09-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ] + } + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_lro_poller(self._execute_operations, None) + + _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.change_state_name = AAZStrArg( + options=["-n", "--name", "--change-state-name"], + help="The name of the ChangeState resource.", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,100}$", + max_length=100, + min_length=3, + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + yield self.ChangeStatesDeleteAtSubscriptionLevel(ctx=self.ctx)() + if condition_1: + yield self.ChangeStatesDelete(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + class ChangeStatesDeleteAtSubscriptionLevel(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( + False, + session, + self.on_200_201, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [204]: + return self.client.build_lro_polling( + False, + session, + self.on_204, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [200, 201]: + return self.client.build_lro_polling( + False, + session, + self.on_200_201, + 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}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "DELETE" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_name, + 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-09-01-preview", + required=True, + ), + } + return parameters + + def on_204(self, session): + pass + + def on_200_201(self, session): + pass + + class ChangeStatesDelete(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( + False, + session, + self.on_200_201, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [204]: + return self.client.build_lro_polling( + False, + session, + self.on_204, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [200, 201]: + return self.client.build_lro_polling( + False, + session, + self.on_200_201, + 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.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "DELETE" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_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-09-01-preview", + required=True, + ), + } + return parameters + + def on_204(self, session): + pass + + def on_200_201(self, session): + pass + + +class _DeleteHelper: + """Helper class for Delete""" + + +__all__ = ["Delete"] diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py new file mode 100644 index 00000000000..f73fcf27901 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py @@ -0,0 +1,610 @@ +# -------------------------------------------------------------------------------------------- +# 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( + "change-safety change-state show", +) +class Show(AAZCommand): + """Get a ChangeState + """ + + _aaz_info = { + "version": "2025-09-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ] + } + + 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.change_state_name = AAZStrArg( + options=["-n", "--name", "--change-state-name"], + help="The name of the ChangeState resource.", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,100}$", + max_length=100, + min_length=3, + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + self.ChangeStatesGetAtSubscriptionLevel(ctx=self.ctx)() + if condition_1: + self.ChangeStatesGet(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 ChangeStatesGetAtSubscriptionLevel(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}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **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( + "changeStateName", self.ctx.args.change_state_name, + 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-09-01-preview", + 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.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.properties = AAZObjectType() + _schema_on_200.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.properties + properties.additional_data = AAZObjectType( + serialized_name="additionalData", + ) + properties.anticipated_end_time = AAZStrType( + serialized_name="anticipatedEndTime", + flags={"required": True}, + ) + properties.anticipated_start_time = AAZStrType( + serialized_name="anticipatedStartTime", + flags={"required": True}, + ) + properties.change_definition = AAZObjectType( + serialized_name="changeDefinition", + flags={"required": True}, + ) + properties.change_type = AAZStrType( + serialized_name="changeType", + flags={"required": True}, + ) + properties.comments = AAZStrType() + properties.description = AAZStrType() + properties.links = AAZListType() + properties.orchestration_tool = AAZStrType( + serialized_name="orchestrationTool", + ) + properties.parameters = AAZDictType() + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.release_label = AAZStrType( + serialized_name="releaseLabel", + ) + properties.rollout_type = AAZStrType( + serialized_name="rolloutType", + flags={"required": True}, + ) + properties.stage_map = AAZObjectType( + serialized_name="stageMap", + ) + properties.stage_map_snapshot = AAZListType( + serialized_name="stageMapSnapshot", + flags={"read_only": True}, + ) + properties.status = AAZStrType( + flags={"read_only": True}, + ) + + change_definition = cls._schema_on_200.properties.change_definition + change_definition.details = AAZObjectType( + flags={"required": True}, + ) + change_definition.kind = AAZStrType( + flags={"required": True}, + ) + change_definition.name = AAZStrType( + flags={"required": True}, + ) + + links = cls._schema_on_200.properties.links + links.Element = AAZObjectType() + + _element = cls._schema_on_200.properties.links.Element + _element.description = AAZStrType() + _element.name = AAZStrType( + flags={"required": True}, + ) + _element.uri = AAZStrType( + flags={"required": True}, + ) + + parameters = cls._schema_on_200.properties.parameters + parameters.Element = AAZObjectType() + + _element = cls._schema_on_200.properties.parameters.Element + _element.metadata = AAZDictType() + _element.type = AAZStrType( + flags={"required": True}, + ) + + metadata = cls._schema_on_200.properties.parameters.Element.metadata + metadata.Element = AAZStrType() + + disc_array = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array") + disc_array.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_array.default_value = AAZListType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array").allowed_values + allowed_values.Element = AAZAnyType() + + default_value = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array").default_value + default_value.Element = AAZAnyType() + + disc_number = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "number") + disc_number.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_number.default_value = AAZIntType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "number").allowed_values + allowed_values.Element = AAZIntType() + + disc_object = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object") + disc_object.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_object.default_value = AAZObjectType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object").allowed_values + allowed_values.Element = AAZDictType() + + _element = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object").allowed_values.Element + _element.Element = AAZAnyType() + + disc_string = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "string") + disc_string.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_string.default_value = AAZStrType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "string").allowed_values + allowed_values.Element = AAZStrType() + + stage_map = cls._schema_on_200.properties.stage_map + stage_map.parameters = AAZDictType() + stage_map.resource_id = AAZStrType( + serialized_name="resourceId", + ) + + parameters = cls._schema_on_200.properties.stage_map.parameters + parameters.Element = AAZAnyType() + + stage_map_snapshot = cls._schema_on_200.properties.stage_map_snapshot + stage_map_snapshot.Element = AAZAnyType() + + 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", + ) + + return cls._schema_on_200 + + class ChangeStatesGet(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.ChangeSafety/changeStates/{changeStateName}", + **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( + "changeStateName", self.ctx.args.change_state_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-09-01-preview", + 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.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.properties = AAZObjectType() + _schema_on_200.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.properties + properties.additional_data = AAZObjectType( + serialized_name="additionalData", + ) + properties.anticipated_end_time = AAZStrType( + serialized_name="anticipatedEndTime", + flags={"required": True}, + ) + properties.anticipated_start_time = AAZStrType( + serialized_name="anticipatedStartTime", + flags={"required": True}, + ) + properties.change_definition = AAZObjectType( + serialized_name="changeDefinition", + flags={"required": True}, + ) + properties.change_type = AAZStrType( + serialized_name="changeType", + flags={"required": True}, + ) + properties.comments = AAZStrType() + properties.description = AAZStrType() + properties.links = AAZListType() + properties.orchestration_tool = AAZStrType( + serialized_name="orchestrationTool", + ) + properties.parameters = AAZDictType() + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.release_label = AAZStrType( + serialized_name="releaseLabel", + ) + properties.rollout_type = AAZStrType( + serialized_name="rolloutType", + flags={"required": True}, + ) + properties.stage_map = AAZObjectType( + serialized_name="stageMap", + ) + properties.stage_map_snapshot = AAZListType( + serialized_name="stageMapSnapshot", + flags={"read_only": True}, + ) + properties.status = AAZStrType( + flags={"read_only": True}, + ) + + change_definition = cls._schema_on_200.properties.change_definition + change_definition.details = AAZObjectType( + flags={"required": True}, + ) + change_definition.kind = AAZStrType( + flags={"required": True}, + ) + change_definition.name = AAZStrType( + flags={"required": True}, + ) + + links = cls._schema_on_200.properties.links + links.Element = AAZObjectType() + + _element = cls._schema_on_200.properties.links.Element + _element.description = AAZStrType() + _element.name = AAZStrType( + flags={"required": True}, + ) + _element.uri = AAZStrType( + flags={"required": True}, + ) + + parameters = cls._schema_on_200.properties.parameters + parameters.Element = AAZObjectType() + + _element = cls._schema_on_200.properties.parameters.Element + _element.metadata = AAZDictType() + _element.type = AAZStrType( + flags={"required": True}, + ) + + metadata = cls._schema_on_200.properties.parameters.Element.metadata + metadata.Element = AAZStrType() + + disc_array = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array") + disc_array.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_array.default_value = AAZListType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array").allowed_values + allowed_values.Element = AAZAnyType() + + default_value = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "array").default_value + default_value.Element = AAZAnyType() + + disc_number = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "number") + disc_number.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_number.default_value = AAZIntType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "number").allowed_values + allowed_values.Element = AAZIntType() + + disc_object = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object") + disc_object.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_object.default_value = AAZObjectType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object").allowed_values + allowed_values.Element = AAZDictType() + + _element = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "object").allowed_values.Element + _element.Element = AAZAnyType() + + disc_string = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "string") + disc_string.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_string.default_value = AAZStrType( + serialized_name="defaultValue", + ) + + allowed_values = cls._schema_on_200.properties.parameters.Element.discriminate_by("type", "string").allowed_values + allowed_values.Element = AAZStrType() + + stage_map = cls._schema_on_200.properties.stage_map + stage_map.parameters = AAZDictType() + stage_map.resource_id = AAZStrType( + serialized_name="resourceId", + ) + + parameters = cls._schema_on_200.properties.stage_map.parameters + parameters.Element = AAZAnyType() + + stage_map_snapshot = cls._schema_on_200.properties.stage_map_snapshot + stage_map_snapshot.Element = AAZAnyType() + + 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", + ) + + return cls._schema_on_200 + + +class _ShowHelper: + """Helper class for Show""" + + +__all__ = ["Show"] diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py new file mode 100644 index 00000000000..ee1213da2a6 --- /dev/null +++ b/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py @@ -0,0 +1,1012 @@ +# -------------------------------------------------------------------------------------------- +# 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( + "change-safety change-state update", +) +class Update(AAZCommand): + """Update a ChangeState + """ + + _aaz_info = { + "version": "2025-09-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ] + } + + AZ_SUPPORT_GENERIC_UPDATE = True + + 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.change_state_name = AAZStrArg( + options=["-n", "--name", "--change-state-name"], + help="The name of the ChangeState resource.", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,100}$", + max_length=100, + min_length=3, + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg() + + # define Arg Group "Properties" + + _args_schema = cls._args_schema + _args_schema.additional_data = AAZObjectArg( + options=["--additional-data"], + arg_group="Properties", + help="Additional metadata for the change required for various orchestration tools.", + nullable=True, + blank={}, + ) + _args_schema.anticipated_end_time = AAZDateTimeArg( + options=["--anticipated-end-time"], + arg_group="Properties", + help="Expected completion time when the change should be finished, in ISO 8601 format.", + fmt=AAZDateTimeFormat( + protocol="iso", + ), + ) + _args_schema.anticipated_start_time = AAZDateTimeArg( + options=["--anticipated-start-time"], + arg_group="Properties", + help="Expected start time when the change execution should begin, in ISO 8601 format.", + fmt=AAZDateTimeFormat( + protocol="iso", + ), + ) + _args_schema.change_type = AAZStrArg( + options=["--change-type"], + arg_group="Properties", + help="Describes the nature of the change.", + enum={"AppDeployment": "AppDeployment", "Config": "Config", "ManualTouch": "ManualTouch", "PolicyDeployment": "PolicyDeployment"}, + ) + _args_schema.comments = AAZStrArg( + options=["--comments"], + arg_group="Properties", + help="Comments about the last update to the changeState resource.", + nullable=True, + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _args_schema.description = AAZStrArg( + options=["--description"], + arg_group="Properties", + help="Brief description about the change.", + nullable=True, + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _args_schema.links = AAZListArg( + options=["--links"], + arg_group="Properties", + help="Collection of related links for the change.", + nullable=True, + ) + _args_schema.orchestration_tool = AAZStrArg( + options=["--orchestration-tool"], + arg_group="Properties", + help="Tool used for deployment orchestration of this change.", + nullable=True, + ) + _args_schema.parameters = AAZDictArg( + options=["--parameters"], + arg_group="Properties", + help="Schema of parameters that will be provided for each stageProgression.", + nullable=True, + ) + _args_schema.release_label = AAZStrArg( + options=["--release-label"], + arg_group="Properties", + help="Label for the release associated with this change.", + nullable=True, + ) + _args_schema.rollout_type = AAZStrArg( + options=["--rollout-type"], + arg_group="Properties", + help="Describes the type of the rollout used for the change.", + enum={"Emergency": "Emergency", "Hotfix": "Hotfix", "Normal": "Normal"}, + ) + _args_schema.stage_map = AAZObjectArg( + options=["--stage-map"], + arg_group="Properties", + help="Reference to the StageMap, defining progression.", + nullable=True, + ) + + links = cls._args_schema.links + links.Element = AAZObjectArg( + nullable=True, + ) + + _element = cls._args_schema.links.Element + _element.description = AAZStrArg( + options=["description"], + help="Description or note about the link.", + nullable=True, + fmt=AAZStrArgFormat( + max_length=2000, + ), + ) + _element.name = AAZStrArg( + options=["name"], + help="name of the link.", + ) + _element.uri = AAZStrArg( + options=["uri"], + help="URL or comma separated URLs for the link.", + ) + + parameters = cls._args_schema.parameters + parameters.Element = AAZObjectArg( + nullable=True, + ) + + _element = cls._args_schema.parameters.Element + _element.array = AAZObjectArg( + options=["array"], + ) + _element.metadata = AAZDictArg( + options=["metadata"], + help="user-specified parameter metadata", + nullable=True, + ) + _element.number = AAZObjectArg( + options=["number"], + ) + _element.object = AAZObjectArg( + options=["object"], + ) + _element.string = AAZObjectArg( + options=["string"], + ) + + array = cls._args_schema.parameters.Element.array + array.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + nullable=True, + ) + array.default_value = AAZListArg( + options=["default-value"], + help="Default value for the parameter.", + nullable=True, + ) + + allowed_values = cls._args_schema.parameters.Element.array.allowed_values + allowed_values.Element = AAZAnyTypeArg( + nullable=True, + ) + + default_value = cls._args_schema.parameters.Element.array.default_value + default_value.Element = AAZAnyTypeArg( + nullable=True, + ) + + metadata = cls._args_schema.parameters.Element.metadata + metadata.Element = AAZStrArg( + nullable=True, + ) + + number = cls._args_schema.parameters.Element.number + number.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + nullable=True, + ) + number.default_value = AAZIntArg( + options=["default-value"], + help="Default value for the parameter.", + nullable=True, + ) + + allowed_values = cls._args_schema.parameters.Element.number.allowed_values + allowed_values.Element = AAZIntArg( + nullable=True, + ) + + object = cls._args_schema.parameters.Element.object + object.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + nullable=True, + ) + object.default_value = AAZObjectArg( + options=["default-value"], + help="Default value for the parameter.", + nullable=True, + blank={}, + ) + + allowed_values = cls._args_schema.parameters.Element.object.allowed_values + allowed_values.Element = AAZDictArg( + nullable=True, + ) + + _element = cls._args_schema.parameters.Element.object.allowed_values.Element + _element.Element = AAZAnyTypeArg( + nullable=True, + ) + + string = cls._args_schema.parameters.Element.string + string.allowed_values = AAZListArg( + options=["allowed-values"], + help="Allowed list of the values for the parameter.", + nullable=True, + ) + string.default_value = AAZStrArg( + options=["default-value"], + help="Default value for the parameter.", + nullable=True, + ) + + allowed_values = cls._args_schema.parameters.Element.string.allowed_values + allowed_values.Element = AAZStrArg( + nullable=True, + ) + + stage_map = cls._args_schema.stage_map + stage_map.parameters = AAZDictArg( + options=["parameters"], + help="Key value pairs of parameter names & their values for the stageMap referenced by the resourceId field.", + nullable=True, + ) + stage_map.resource_id = AAZStrArg( + options=["resource-id"], + help="ARM resource ID for the nested stagemap resource.", + nullable=True, + ) + + parameters = cls._args_schema.stage_map.parameters + parameters.Element = AAZAnyTypeArg( + nullable=True, + ) + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + condition_2 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_3 = has_value(self.ctx.args.change_state_name) and has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + self.ChangeStatesGetAtSubscriptionLevel(ctx=self.ctx)() + if condition_1: + self.ChangeStatesGet(ctx=self.ctx)() + self.pre_instance_update(self.ctx.vars.instance) + self.InstanceUpdateByJson(ctx=self.ctx)() + self.InstanceUpdateByGeneric(ctx=self.ctx)() + self.post_instance_update(self.ctx.vars.instance) + if condition_2: + self.ChangeStatesCreateOrUpdateAtSubscriptionLevel(ctx=self.ctx)() + if condition_3: + self.ChangeStatesCreateOrUpdate(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + @register_callback + def pre_instance_update(self, instance): + pass + + @register_callback + def post_instance_update(self, instance): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class ChangeStatesGetAtSubscriptionLevel(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}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **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( + "changeStateName", self.ctx.args.change_state_name, + 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-09-01-preview", + 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() + _UpdateHelper._build_schema_change_state_read(cls._schema_on_200) + + return cls._schema_on_200 + + class ChangeStatesGet(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.ChangeSafety/changeStates/{changeStateName}", + **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( + "changeStateName", self.ctx.args.change_state_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-09-01-preview", + 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() + _UpdateHelper._build_schema_change_state_read(cls._schema_on_200) + + return cls._schema_on_200 + + class ChangeStatesCreateOrUpdateAtSubscriptionLevel(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, 201]: + return self.on_200_201(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "PUT" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_name, + 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-09-01-preview", + 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, + value=self.ctx.vars.instance, + ) + + return self.serialize_content(_content_value) + + def on_200_201(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_201 + ) + + _schema_on_200_201 = None + + @classmethod + def _build_schema_on_200_201(cls): + if cls._schema_on_200_201 is not None: + return cls._schema_on_200_201 + + cls._schema_on_200_201 = AAZObjectType() + _UpdateHelper._build_schema_change_state_read(cls._schema_on_200_201) + + return cls._schema_on_200_201 + + class ChangeStatesCreateOrUpdate(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, 201]: + return self.on_200_201(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ChangeSafety/changeStates/{changeStateName}", + **self.url_parameters + ) + + @property + def method(self): + return "PUT" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "changeStateName", self.ctx.args.change_state_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-09-01-preview", + 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, + value=self.ctx.vars.instance, + ) + + return self.serialize_content(_content_value) + + def on_200_201(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200_201 + ) + + _schema_on_200_201 = None + + @classmethod + def _build_schema_on_200_201(cls): + if cls._schema_on_200_201 is not None: + return cls._schema_on_200_201 + + cls._schema_on_200_201 = AAZObjectType() + _UpdateHelper._build_schema_change_state_read(cls._schema_on_200_201) + + return cls._schema_on_200_201 + + class InstanceUpdateByJson(AAZJsonInstanceUpdateOperation): + + def __call__(self, *args, **kwargs): + self._update_instance(self.ctx.vars.instance) + + def _update_instance(self, instance): + _instance_value, _builder = self.new_content_builder( + self.ctx.args, + value=instance, + typ=AAZObjectType + ) + _builder.set_prop("properties", AAZObjectType) + + properties = _builder.get(".properties") + if properties is not None: + properties.set_prop("additionalData", AAZObjectType, ".additional_data") + properties.set_prop("anticipatedEndTime", AAZStrType, ".anticipated_end_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("anticipatedStartTime", AAZStrType, ".anticipated_start_time", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("changeType", AAZStrType, ".change_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("comments", AAZStrType, ".comments") + properties.set_prop("description", AAZStrType, ".description") + properties.set_prop("links", AAZListType, ".links") + properties.set_prop("orchestrationTool", AAZStrType, ".orchestration_tool") + properties.set_prop("parameters", AAZDictType, ".parameters") + properties.set_prop("releaseLabel", AAZStrType, ".release_label") + properties.set_prop("rolloutType", AAZStrType, ".rollout_type", typ_kwargs={"flags": {"required": True}}) + properties.set_prop("stageMap", AAZObjectType, ".stage_map") + + links = _builder.get(".properties.links") + if links is not None: + links.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.links[]") + if _elements is not None: + _elements.set_prop("description", AAZStrType, ".description") + _elements.set_prop("name", AAZStrType, ".name", typ_kwargs={"flags": {"required": True}}) + _elements.set_prop("uri", AAZStrType, ".uri", typ_kwargs={"flags": {"required": True}}) + + parameters = _builder.get(".properties.parameters") + if parameters is not None: + parameters.set_elements(AAZObjectType, ".") + + _elements = _builder.get(".properties.parameters{}") + if _elements is not None: + _elements.set_prop("metadata", AAZDictType, ".metadata") + _elements.set_const("type", "array", AAZStrType, ".array", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "number", AAZStrType, ".number", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "object", AAZStrType, ".object", typ_kwargs={"flags": {"required": True}}) + _elements.set_const("type", "string", AAZStrType, ".string", typ_kwargs={"flags": {"required": True}}) + _elements.discriminate_by("type", "array") + _elements.discriminate_by("type", "number") + _elements.discriminate_by("type", "object") + _elements.discriminate_by("type", "string") + + metadata = _builder.get(".properties.parameters{}.metadata") + if metadata is not None: + metadata.set_elements(AAZStrType, ".") + + disc_array = _builder.get(".properties.parameters{}{type:array}") + if disc_array is not None: + disc_array.set_prop("allowedValues", AAZListType, ".array.allowed_values") + disc_array.set_prop("defaultValue", AAZListType, ".array.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:array}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZAnyType, ".") + + default_value = _builder.get(".properties.parameters{}{type:array}.defaultValue") + if default_value is not None: + default_value.set_elements(AAZAnyType, ".") + + disc_number = _builder.get(".properties.parameters{}{type:number}") + if disc_number is not None: + disc_number.set_prop("allowedValues", AAZListType, ".number.allowed_values") + disc_number.set_prop("defaultValue", AAZIntType, ".number.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:number}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZIntType, ".") + + disc_object = _builder.get(".properties.parameters{}{type:object}") + if disc_object is not None: + disc_object.set_prop("allowedValues", AAZListType, ".object.allowed_values") + disc_object.set_prop("defaultValue", AAZObjectType, ".object.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:object}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZDictType, ".") + + _elements = _builder.get(".properties.parameters{}{type:object}.allowedValues[]") + if _elements is not None: + _elements.set_elements(AAZAnyType, ".") + + disc_string = _builder.get(".properties.parameters{}{type:string}") + if disc_string is not None: + disc_string.set_prop("allowedValues", AAZListType, ".string.allowed_values") + disc_string.set_prop("defaultValue", AAZStrType, ".string.default_value") + + allowed_values = _builder.get(".properties.parameters{}{type:string}.allowedValues") + if allowed_values is not None: + allowed_values.set_elements(AAZStrType, ".") + + stage_map = _builder.get(".properties.stageMap") + if stage_map is not None: + stage_map.set_prop("parameters", AAZDictType, ".parameters") + stage_map.set_prop("resourceId", AAZStrType, ".resource_id") + + parameters = _builder.get(".properties.stageMap.parameters") + if parameters is not None: + parameters.set_elements(AAZAnyType, ".") + + return _instance_value + + class InstanceUpdateByGeneric(AAZGenericInstanceUpdateOperation): + + def __call__(self, *args, **kwargs): + self._update_instance_by_generic( + self.ctx.vars.instance, + self.ctx.generic_update_args + ) + + +class _UpdateHelper: + """Helper class for Update""" + + _schema_change_state_read = None + + @classmethod + def _build_schema_change_state_read(cls, _schema): + if cls._schema_change_state_read is not None: + _schema.id = cls._schema_change_state_read.id + _schema.name = cls._schema_change_state_read.name + _schema.properties = cls._schema_change_state_read.properties + _schema.system_data = cls._schema_change_state_read.system_data + _schema.type = cls._schema_change_state_read.type + return + + cls._schema_change_state_read = _schema_change_state_read = AAZObjectType() + + change_state_read = _schema_change_state_read + change_state_read.id = AAZStrType( + flags={"read_only": True}, + ) + change_state_read.name = AAZStrType( + flags={"read_only": True}, + ) + change_state_read.properties = AAZObjectType() + change_state_read.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + change_state_read.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = _schema_change_state_read.properties + properties.additional_data = AAZObjectType( + serialized_name="additionalData", + ) + properties.anticipated_end_time = AAZStrType( + serialized_name="anticipatedEndTime", + flags={"required": True}, + ) + properties.anticipated_start_time = AAZStrType( + serialized_name="anticipatedStartTime", + flags={"required": True}, + ) + properties.change_definition = AAZObjectType( + serialized_name="changeDefinition", + flags={"required": True}, + ) + properties.change_type = AAZStrType( + serialized_name="changeType", + flags={"required": True}, + ) + properties.comments = AAZStrType() + properties.description = AAZStrType() + properties.links = AAZListType() + properties.orchestration_tool = AAZStrType( + serialized_name="orchestrationTool", + ) + properties.parameters = AAZDictType() + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.release_label = AAZStrType( + serialized_name="releaseLabel", + ) + properties.rollout_type = AAZStrType( + serialized_name="rolloutType", + flags={"required": True}, + ) + properties.stage_map = AAZObjectType( + serialized_name="stageMap", + ) + properties.stage_map_snapshot = AAZListType( + serialized_name="stageMapSnapshot", + flags={"read_only": True}, + ) + properties.status = AAZStrType( + flags={"read_only": True}, + ) + + change_definition = _schema_change_state_read.properties.change_definition + change_definition.details = AAZObjectType( + flags={"required": True}, + ) + change_definition.kind = AAZStrType( + flags={"required": True}, + ) + change_definition.name = AAZStrType( + flags={"required": True}, + ) + + links = _schema_change_state_read.properties.links + links.Element = AAZObjectType() + + _element = _schema_change_state_read.properties.links.Element + _element.description = AAZStrType() + _element.name = AAZStrType( + flags={"required": True}, + ) + _element.uri = AAZStrType( + flags={"required": True}, + ) + + parameters = _schema_change_state_read.properties.parameters + parameters.Element = AAZObjectType() + + _element = _schema_change_state_read.properties.parameters.Element + _element.metadata = AAZDictType() + _element.type = AAZStrType( + flags={"required": True}, + ) + + metadata = _schema_change_state_read.properties.parameters.Element.metadata + metadata.Element = AAZStrType() + + disc_array = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "array") + disc_array.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_array.default_value = AAZListType( + serialized_name="defaultValue", + ) + + allowed_values = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "array").allowed_values + allowed_values.Element = AAZAnyType() + + default_value = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "array").default_value + default_value.Element = AAZAnyType() + + disc_number = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "number") + disc_number.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_number.default_value = AAZIntType( + serialized_name="defaultValue", + ) + + allowed_values = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "number").allowed_values + allowed_values.Element = AAZIntType() + + disc_object = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "object") + disc_object.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_object.default_value = AAZObjectType( + serialized_name="defaultValue", + ) + + allowed_values = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "object").allowed_values + allowed_values.Element = AAZDictType() + + _element = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "object").allowed_values.Element + _element.Element = AAZAnyType() + + disc_string = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "string") + disc_string.allowed_values = AAZListType( + serialized_name="allowedValues", + ) + disc_string.default_value = AAZStrType( + serialized_name="defaultValue", + ) + + allowed_values = _schema_change_state_read.properties.parameters.Element.discriminate_by("type", "string").allowed_values + allowed_values.Element = AAZStrType() + + stage_map = _schema_change_state_read.properties.stage_map + stage_map.parameters = AAZDictType() + stage_map.resource_id = AAZStrType( + serialized_name="resourceId", + ) + + parameters = _schema_change_state_read.properties.stage_map.parameters + parameters.Element = AAZAnyType() + + stage_map_snapshot = _schema_change_state_read.properties.stage_map_snapshot + stage_map_snapshot.Element = AAZAnyType() + + system_data = _schema_change_state_read.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", + ) + + _schema.id = cls._schema_change_state_read.id + _schema.name = cls._schema_change_state_read.name + _schema.properties = cls._schema_change_state_read.properties + _schema.system_data = cls._schema_change_state_read.system_data + _schema.type = cls._schema_change_state_read.type + + +__all__ = ["Update"] diff --git a/src/change-state/azext_change_state/azext_metadata.json b/src/change-state/azext_change_state/azext_metadata.json new file mode 100644 index 00000000000..71889bb136b --- /dev/null +++ b/src/change-state/azext_change_state/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.75.0" +} \ No newline at end of file diff --git a/src/change-state/azext_change_state/commands.py b/src/change-state/azext_change_state/commands.py new file mode 100644 index 00000000000..b0d842e4993 --- /dev/null +++ b/src/change-state/azext_change_state/commands.py @@ -0,0 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# 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: disable=too-many-lines +# pylint: disable=too-many-statements + +# from azure.cli.core.commands import CliCommandType + + +def load_command_table(self, _): # pylint: disable=unused-argument + pass diff --git a/src/change-state/azext_change_state/custom.py b/src/change-state/azext_change_state/custom.py new file mode 100644 index 00000000000..86df1e48ef5 --- /dev/null +++ b/src/change-state/azext_change_state/custom.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# 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: disable=too-many-lines +# pylint: disable=too-many-statements + +from knack.log import get_logger + + +logger = get_logger(__name__) diff --git a/src/change-state/azext_change_state/tests/__init__.py b/src/change-state/azext_change_state/tests/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/change-state/azext_change_state/tests/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- diff --git a/src/change-state/azext_change_state/tests/latest/__init__.py b/src/change-state/azext_change_state/tests/latest/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/change-state/azext_change_state/tests/latest/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- diff --git a/src/change-state/azext_change_state/tests/latest/test_change_state.py b/src/change-state/azext_change_state/tests/latest/test_change_state.py new file mode 100644 index 00000000000..e8fb36765e6 --- /dev/null +++ b/src/change-state/azext_change_state/tests/latest/test_change_state.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import * + + +class ChangeStateScenario(ScenarioTest): + # TODO: add tests here + pass diff --git a/src/change-state/setup.cfg b/src/change-state/setup.cfg new file mode 100644 index 00000000000..2fdd96e5d39 --- /dev/null +++ b/src/change-state/setup.cfg @@ -0,0 +1 @@ +#setup.cfg \ No newline at end of file diff --git a/src/change-state/setup.py b/src/change-state/setup.py new file mode 100644 index 00000000000..686df8315a1 --- /dev/null +++ b/src/change-state/setup.py @@ -0,0 +1,49 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- + +from codecs import open +from setuptools import setup, find_packages + + +# HISTORY.rst entry. +VERSION = '1.0.0b1' + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'License :: OSI Approved :: MIT License', +] + +DEPENDENCIES = [] + +with open('README.md', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='change-state', + version=VERSION, + description='Microsoft Azure Command-Line Tools ChangeState Extension.', + long_description=README + '\n\n' + HISTORY, + license='MIT', + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + url='https://github.com/Azure/azure-cli-extensions/tree/main/src/change-state', + classifiers=CLASSIFIERS, + packages=find_packages(exclude=["tests"]), + package_data={'azext_change_state': ['azext_metadata.json']}, + install_requires=DEPENDENCIES +) From 74a3d0f89533a8504f66b63e6c1afb2019e181d3 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 15:26:00 -0700 Subject: [PATCH 02/15] Fix parsing --- .../azext_change_state/commands.py | 8 +- src/change-state/azext_change_state/custom.py | 349 ++++++++++++++++++ 2 files changed, 355 insertions(+), 2 deletions(-) diff --git a/src/change-state/azext_change_state/commands.py b/src/change-state/azext_change_state/commands.py index b0d842e4993..bc58e6c79b1 100644 --- a/src/change-state/azext_change_state/commands.py +++ b/src/change-state/azext_change_state/commands.py @@ -8,8 +8,12 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -# from azure.cli.core.commands import CliCommandType +from azext_change_state import custom def load_command_table(self, _): # pylint: disable=unused-argument - pass + """Apply custom command overrides after the AAZ-generated command table is loaded.""" + if 'change-safety change-state create' in self.command_table: + self.command_table['change-safety change-state create'] = custom.ChangeStateCreate(loader=self) + if 'change-safety change-state update' in self.command_table: + self.command_table['change-safety change-state update'] = custom.ChangeStateUpdate(loader=self) diff --git a/src/change-state/azext_change_state/custom.py b/src/change-state/azext_change_state/custom.py index 86df1e48ef5..6002d586236 100644 --- a/src/change-state/azext_change_state/custom.py +++ b/src/change-state/azext_change_state/custom.py @@ -9,6 +9,355 @@ # pylint: disable=too-many-statements from knack.log import get_logger +from azure.cli.core.aaz import has_value, AAZAnyType, AAZListArg, AAZStrArg +from azure.cli.core.aaz._arg_action import AAZArgActionOperations, AAZPromptInputOperation, _ELEMENT_APPEND_KEY +from azure.cli.core.azclierror import InvalidArgumentValueError +from azext_change_state.aaz.latest.change_safety.change_state import Create as _ChangeStateCreate, Update as _ChangeStateUpdate logger = get_logger(__name__) + + +def _inject_change_definition_into_content(content, ctx): + """Attach the computed changeDefinition payload to the serialized request content.""" + change_definition_value = getattr(ctx.vars, "change_definition", None) + if change_definition_value is None: + return content + + change_definition = change_definition_value.to_serialized_data() + if not change_definition: + return content + + if content is None: + content = {} + properties = content.setdefault("properties", {}) + properties["changeDefinition"] = change_definition + return content + + +class ChangeStateCreate(_ChangeStateCreate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._raw_targets = [] + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + schema = super()._build_arguments_schema(*args, **kwargs) + if not hasattr(schema, "targets"): + schema.targets = AAZListArg( + options=["--targets"], + help=( + "Target definitions expressed as key=value pairs separated by commas or semicolons. " + "Example: --targets \"resourceId=,operation=delete\"" + ), + ) + schema.targets.Element = AAZStrArg() + return schema + + def _handler(self, command_args): + # Extract targets before calling parent handler so we can accept flexible input formats. + command_args = dict(command_args) if command_args else {} + raw_targets = command_args.pop('targets', None) + if raw_targets is not None: + self._raw_targets = self._extract_targets_from_arg(raw_targets) + return super()._handler(command_args) + + def _extract_targets_from_arg(self, raw_targets): + """Extract target values from the raw argument.""" + if raw_targets is None: + return [] + + if isinstance(raw_targets, AAZArgActionOperations): + elements = [] + for keys, data in raw_targets._ops: + if isinstance(data, AAZPromptInputOperation): + data = data() + normalized_value = '' + if isinstance(data, (list, tuple)): + normalized_value = ','.join(str(v) for v in data if v is not None) + elif data is not None: + normalized_value = str(data) + + idx = None + key_name = None + for key in keys: + if key == _ELEMENT_APPEND_KEY: + idx = len(elements) + elif isinstance(key, int): + idx = key + elif isinstance(key, str): + key_name = key + + if idx is None: + idx = len(elements) - 1 if elements else 0 + while len(elements) <= idx: + elements.append('') + + if key_name: + combined = f"{key_name}={normalized_value}" if normalized_value else key_name + elements[idx] = f"{elements[idx]},{combined}" if elements[idx] else combined + else: + elements[idx] = normalized_value + + return [value for value in elements if value] + + if hasattr(raw_targets, 'to_serialized_data'): + values = raw_targets.to_serialized_data() + elif isinstance(raw_targets, list): + values = raw_targets + else: + values = [raw_targets] + + return [str(v) for v in values if v is not None] + + def pre_operations(self): + super().pre_operations() + + if not self._raw_targets: + raise InvalidArgumentValueError('--targets is required and must include key=value pairs.') + + # Build and set the changeDefinition with targets + change_definition = self._build_change_definition() + self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) + + def _build_change_definition(self): + """Build the changeDefinition object with targets""" + targets = self._parse_targets(self._raw_targets) + change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" + + return { + 'kind': 'Targets', + 'name': change_name, + 'details': { + 'targets': targets + } + } + + @staticmethod + def _parse_targets(raw_targets): + if raw_targets is None: + raise InvalidArgumentValueError('--targets is required and must include key=value pairs.') + if not raw_targets: + raise InvalidArgumentValueError('--targets is required and must include key=value pairs.') + parsed_targets = [] + for token in raw_targets: + if token is None: + continue + segments = [] + for part in str(token).split(';'): + segments.extend(segment.strip() for segment in part.split(',') if segment.strip()) + print("segments:", segments) + if not segments: + continue + target_entry = {} + for segment in segments: + if '=' not in segment: + raise InvalidArgumentValueError('Each --targets entry must be in key=value format.') + key, value = segment.split('=', 1) + key = key.strip() + value = value.strip() + if not key or not value: + raise InvalidArgumentValueError('Each --targets entry must include a non-empty key and value.') + target_entry[key] = value + if not target_entry: + continue + parsed_targets.append(target_entry) + if not parsed_targets: + raise InvalidArgumentValueError('--targets must include at least one key=value pair.') + return parsed_targets + + def pre_instance_create(self): + """Set the changeDefinition in the request body before creating the instance""" + change_definition = getattr(self.ctx.vars, 'change_definition', None) + if change_definition is not None: + # The changeDefinition will be set in the content property of the HTTP operations + pass + + class ChangeStatesCreateOrUpdateAtSubscriptionLevel(_ChangeStateCreate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): + @property + def content(self): + content = super().content + return _inject_change_definition_into_content(content, self.ctx) + + class ChangeStatesCreateOrUpdate(_ChangeStateCreate.ChangeStatesCreateOrUpdate): + @property + def content(self): + content = super().content + return _inject_change_definition_into_content(content, self.ctx) + + +class ChangeStateUpdate(_ChangeStateUpdate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._raw_targets = [] + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + schema = super()._build_arguments_schema(*args, **kwargs) + if not hasattr(schema, "targets"): + schema.targets = AAZListArg( + options=["--targets"], + help=( + "Optional target definitions expressed as key=value pairs separated by commas or semicolons. " + "Example: --targets \"resourceId=,operation=delete\"" + ), + ) + schema.targets.Element = AAZStrArg() + return schema + + def _handler(self, command_args): + # Extract targets before calling parent handler so we can accept flexible input formats. + command_args = dict(command_args) if command_args else {} + raw_targets = command_args.pop('targets', None) + if raw_targets is not None: + self._raw_targets = self._extract_targets_from_arg(raw_targets) + return super()._handler(command_args) + + def _extract_targets_from_arg(self, raw_targets): + """Extract target values from the raw argument.""" + if raw_targets is None: + return [] + + if isinstance(raw_targets, AAZArgActionOperations): + elements = [] + for keys, data in raw_targets._ops: + if isinstance(data, AAZPromptInputOperation): + data = data() + normalized_value = '' + if isinstance(data, (list, tuple)): + normalized_value = ','.join(str(v) for v in data if v is not None) + elif data is not None: + normalized_value = str(data) + + idx = None + key_name = None + for key in keys: + if key == _ELEMENT_APPEND_KEY: + idx = len(elements) + elif isinstance(key, int): + idx = key + elif isinstance(key, str): + key_name = key + + if idx is None: + idx = len(elements) - 1 if elements else 0 + while len(elements) <= idx: + elements.append('') + + if key_name: + combined = f"{key_name}={normalized_value}" if normalized_value else key_name + elements[idx] = f"{elements[idx]},{combined}" if elements[idx] else combined + else: + elements[idx] = normalized_value + + return [value for value in elements if value] + + if hasattr(raw_targets, 'to_serialized_data'): + values = raw_targets.to_serialized_data() + elif isinstance(raw_targets, list): + values = raw_targets + else: + values = [raw_targets] + + return [str(v) for v in values if v is not None] + + def pre_operations(self): + super().pre_operations() + + # Build and set the changeDefinition with targets if targets are provided + if self._raw_targets: + change_definition = self._build_change_definition() + self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) + + def _build_change_definition(self): + """Build the changeDefinition object with targets""" + targets = self._parse_targets(self._raw_targets) + change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" + + return { + 'kind': 'Targets', + 'name': change_name, + 'details': { + 'targets': targets + } + } + + def _parse_targets(self, raw_targets): + """Parse target strings into structured objects""" + if not raw_targets: + return None # For update, targets may be optional + + parsed_targets = [] + for token in raw_targets: + if not token: + continue + + # Split by semicolon or comma to handle multiple key-value pairs in one token + segments = [] + for part in str(token).replace(';', ',').split(','): + segment = part.strip() + if segment: + segments.append(segment) + + if not segments: + continue + + target_entry = {} + for segment in segments: + if '=' not in segment: + raise InvalidArgumentValueError(f"Each --targets entry must be in key=value format. Invalid: '{segment}'") + + key, value = segment.split('=', 1) + key = key.strip() + value = value.strip() + + if not key or not value: + raise InvalidArgumentValueError('Each --targets entry must include a non-empty key and value.') + + # Map keys to the correct property names + key_mapping = { + 'resourceid': 'resourceId', + 'subscriptionid': 'subscriptionId', + 'resourcegroupname': 'resourceGroupName', + 'resourcegroup': 'resourceGroupName', # Allow shorter alias + 'rg': 'resourceGroupName', # Allow shorter alias + 'resourcetype': 'resourceType', + 'resourcename': 'resourceName', + 'httpmethod': 'httpMethod', + 'method': 'httpMethod', # Allow shorter alias + 'operation': 'httpMethod' # Allow 'operation' as alias for httpMethod + } + + normalized_key = key.lower() + if normalized_key in key_mapping: + mapped_key = key_mapping[normalized_key] + # Normalize HTTP method values to uppercase + if mapped_key == 'httpMethod' and value: + value = value.upper() + target_entry[mapped_key] = value + else: + target_entry[key] = value + + if target_entry: + parsed_targets.append(target_entry) + + return parsed_targets if parsed_targets else None + + def pre_instance_update(self): + """Set the changeDefinition in the request body before updating the instance""" + change_definition = getattr(self.ctx.vars, 'change_definition', None) + if change_definition is not None: + # The changeDefinition will be set in the content property of the HTTP operations + pass + + class ChangeStatesCreateOrUpdateAtSubscriptionLevel(_ChangeStateUpdate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): + @property + def content(self): + content = super().content + return _inject_change_definition_into_content(content, self.ctx) + + class ChangeStatesCreateOrUpdate(_ChangeStateUpdate.ChangeStatesCreateOrUpdate): + @property + def content(self): + content = super().content + return _inject_change_definition_into_content(content, self.ctx) From b80cda8f6cfd82a17a7cd7cf3fe0cac0888dd12e Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 16:10:33 -0700 Subject: [PATCH 03/15] Fix parsing --- src/change-state/azext_change_state/custom.py | 215 ++++++++++-------- 1 file changed, 115 insertions(+), 100 deletions(-) diff --git a/src/change-state/azext_change_state/custom.py b/src/change-state/azext_change_state/custom.py index 6002d586236..2cb450c4a2f 100644 --- a/src/change-state/azext_change_state/custom.py +++ b/src/change-state/azext_change_state/custom.py @@ -35,10 +35,88 @@ def _inject_change_definition_into_content(content, ctx): return content +def _normalize_targets_arg(raw_targets): + """Return a list of raw target strings from the parsed CLI argument.""" + if raw_targets is None: + return [] + + if isinstance(raw_targets, AAZArgActionOperations): + elements = [] + for keys, data in raw_targets._ops: + logger.debug("Processing target op keys=%s data=%s", keys, data) + if isinstance(data, AAZPromptInputOperation): + data = data() + + normalized_value = '' + if isinstance(data, (list, tuple)): + normalized_value = ','.join(str(v) for v in data if v is not None) + elif data is not None: + normalized_value = str(data) + + idx = None + key_name = None + for key in keys: + if key == _ELEMENT_APPEND_KEY: + idx = len(elements) + elif isinstance(key, int): + idx = key + elif isinstance(key, str): + key_name = key + + if idx is None: + idx = len(elements) - 1 if elements else 0 + while len(elements) <= idx: + elements.append('') + + if key_name: + combined = f"{key_name}={normalized_value}" if normalized_value else key_name + elements[idx] = f"{elements[idx]},{combined}" if elements[idx] else combined + else: + elements[idx] = normalized_value + + return [value for value in elements if value] + + if hasattr(raw_targets, 'to_serialized_data'): + values = raw_targets.to_serialized_data() + elif isinstance(raw_targets, list): + values = raw_targets + else: + values = [raw_targets] + + return [str(v) for v in values if v is not None] + + +def _inject_targets_into_result(data, targets): + """Ensure changeDefinition.details.targets is present in the command output.""" + if not targets or data is None: + return + + def process(item): + if not isinstance(item, dict): + return + containers = [] + if isinstance(item.get('properties'), dict): + containers.append(item['properties']) + containers.append(item) + for container in containers: + change_def = container.get('changeDefinition') + if isinstance(change_def, dict): + details = change_def.setdefault('details', {}) + if isinstance(details, dict) and not details.get('targets'): + details['targets'] = targets + + if isinstance(data, list): + for entry in data: + process(entry) + else: + process(data) + + class ChangeStateCreate(_ChangeStateCreate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._raw_targets = [] + self._parsed_targets = None @classmethod def _build_arguments_schema(cls, *args, **kwargs): @@ -59,57 +137,9 @@ def _handler(self, command_args): command_args = dict(command_args) if command_args else {} raw_targets = command_args.pop('targets', None) if raw_targets is not None: - self._raw_targets = self._extract_targets_from_arg(raw_targets) + self._raw_targets = _normalize_targets_arg(raw_targets) return super()._handler(command_args) - def _extract_targets_from_arg(self, raw_targets): - """Extract target values from the raw argument.""" - if raw_targets is None: - return [] - - if isinstance(raw_targets, AAZArgActionOperations): - elements = [] - for keys, data in raw_targets._ops: - if isinstance(data, AAZPromptInputOperation): - data = data() - normalized_value = '' - if isinstance(data, (list, tuple)): - normalized_value = ','.join(str(v) for v in data if v is not None) - elif data is not None: - normalized_value = str(data) - - idx = None - key_name = None - for key in keys: - if key == _ELEMENT_APPEND_KEY: - idx = len(elements) - elif isinstance(key, int): - idx = key - elif isinstance(key, str): - key_name = key - - if idx is None: - idx = len(elements) - 1 if elements else 0 - while len(elements) <= idx: - elements.append('') - - if key_name: - combined = f"{key_name}={normalized_value}" if normalized_value else key_name - elements[idx] = f"{elements[idx]},{combined}" if elements[idx] else combined - else: - elements[idx] = normalized_value - - return [value for value in elements if value] - - if hasattr(raw_targets, 'to_serialized_data'): - values = raw_targets.to_serialized_data() - elif isinstance(raw_targets, list): - values = raw_targets - else: - values = [raw_targets] - - return [str(v) for v in values if v is not None] - def pre_operations(self): super().pre_operations() @@ -118,11 +148,13 @@ def pre_operations(self): # Build and set the changeDefinition with targets change_definition = self._build_change_definition() + logger.debug("Final changeDefinition for create: %s", change_definition) self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) def _build_change_definition(self): """Build the changeDefinition object with targets""" targets = self._parse_targets(self._raw_targets) + self._parsed_targets = targets change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" return { @@ -146,7 +178,6 @@ def _parse_targets(raw_targets): segments = [] for part in str(token).split(';'): segments.extend(segment.strip() for segment in part.split(',') if segment.strip()) - print("segments:", segments) if not segments: continue target_entry = {} @@ -158,7 +189,26 @@ def _parse_targets(raw_targets): value = value.strip() if not key or not value: raise InvalidArgumentValueError('Each --targets entry must include a non-empty key and value.') - target_entry[key] = value + key_mapping = { + 'resourceid': 'resourceId', + 'subscriptionid': 'subscriptionId', + 'resourcegroupname': 'resourceGroupName', + 'resourcegroup': 'resourceGroupName', + 'rg': 'resourceGroupName', + 'resourcetype': 'resourceType', + 'resourcename': 'resourceName', + 'httpmethod': 'httpMethod', + 'method': 'httpMethod', + 'operation': 'httpMethod' + } + normalized_key = key.lower() + if normalized_key in key_mapping: + mapped_key = key_mapping[normalized_key] + if mapped_key == 'httpMethod' and value: + value = value.upper() + target_entry[mapped_key] = value + else: + target_entry[key] = value if not target_entry: continue parsed_targets.append(target_entry) @@ -166,6 +216,11 @@ def _parse_targets(raw_targets): raise InvalidArgumentValueError('--targets must include at least one key=value pair.') return parsed_targets + def _output(self, *args, **kwargs): + result = super()._output(*args, **kwargs) + _inject_targets_into_result(result, self._parsed_targets) + return result + def pre_instance_create(self): """Set the changeDefinition in the request body before creating the instance""" change_definition = getattr(self.ctx.vars, 'change_definition', None) @@ -190,6 +245,7 @@ class ChangeStateUpdate(_ChangeStateUpdate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._raw_targets = [] + self._parsed_targets = None @classmethod def _build_arguments_schema(cls, *args, **kwargs): @@ -210,68 +266,22 @@ def _handler(self, command_args): command_args = dict(command_args) if command_args else {} raw_targets = command_args.pop('targets', None) if raw_targets is not None: - self._raw_targets = self._extract_targets_from_arg(raw_targets) + self._raw_targets = _normalize_targets_arg(raw_targets) return super()._handler(command_args) - def _extract_targets_from_arg(self, raw_targets): - """Extract target values from the raw argument.""" - if raw_targets is None: - return [] - - if isinstance(raw_targets, AAZArgActionOperations): - elements = [] - for keys, data in raw_targets._ops: - if isinstance(data, AAZPromptInputOperation): - data = data() - normalized_value = '' - if isinstance(data, (list, tuple)): - normalized_value = ','.join(str(v) for v in data if v is not None) - elif data is not None: - normalized_value = str(data) - - idx = None - key_name = None - for key in keys: - if key == _ELEMENT_APPEND_KEY: - idx = len(elements) - elif isinstance(key, int): - idx = key - elif isinstance(key, str): - key_name = key - - if idx is None: - idx = len(elements) - 1 if elements else 0 - while len(elements) <= idx: - elements.append('') - - if key_name: - combined = f"{key_name}={normalized_value}" if normalized_value else key_name - elements[idx] = f"{elements[idx]},{combined}" if elements[idx] else combined - else: - elements[idx] = normalized_value - - return [value for value in elements if value] - - if hasattr(raw_targets, 'to_serialized_data'): - values = raw_targets.to_serialized_data() - elif isinstance(raw_targets, list): - values = raw_targets - else: - values = [raw_targets] - - return [str(v) for v in values if v is not None] - def pre_operations(self): super().pre_operations() # Build and set the changeDefinition with targets if targets are provided if self._raw_targets: change_definition = self._build_change_definition() + logger.debug("Final changeDefinition for update: %s", change_definition) self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) def _build_change_definition(self): """Build the changeDefinition object with targets""" targets = self._parse_targets(self._raw_targets) + self._parsed_targets = targets change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" return { @@ -343,6 +353,11 @@ def _parse_targets(self, raw_targets): return parsed_targets if parsed_targets else None + def _output(self, *args, **kwargs): + result = super()._output(*args, **kwargs) + _inject_targets_into_result(result, self._parsed_targets) + return result + def pre_instance_update(self): """Set the changeDefinition in the request body before updating the instance""" change_definition = getattr(self.ctx.vars, 'change_definition', None) From add68de3f12cb98dd9666895a7d959d29e6f1fc3 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 16:57:15 -0700 Subject: [PATCH 04/15] Fix displaying --- .../azext_change_state/commands.py | 14 +++-- src/change-state/azext_change_state/custom.py | 51 ++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/change-state/azext_change_state/commands.py b/src/change-state/azext_change_state/commands.py index bc58e6c79b1..afd3355e44f 100644 --- a/src/change-state/azext_change_state/commands.py +++ b/src/change-state/azext_change_state/commands.py @@ -13,7 +13,13 @@ def load_command_table(self, _): # pylint: disable=unused-argument """Apply custom command overrides after the AAZ-generated command table is loaded.""" - if 'change-safety change-state create' in self.command_table: - self.command_table['change-safety change-state create'] = custom.ChangeStateCreate(loader=self) - if 'change-safety change-state update' in self.command_table: - self.command_table['change-safety change-state update'] = custom.ChangeStateUpdate(loader=self) + # Register custom commands for both 'change-state' and 'change-safety change-state' + for prefix in ["change-state", "change-safety change-state"]: + if f"{prefix} create" in self.command_table: + self.command_table[f"{prefix} create"] = custom.ChangeStateCreate(loader=self) + if f"{prefix} update" in self.command_table: + self.command_table[f"{prefix} update"] = custom.ChangeStateUpdate(loader=self) + if f"{prefix} show" in self.command_table: + self.command_table[f"{prefix} show"] = custom.ChangeStateShow(loader=self) + if f"{prefix} delete" in self.command_table: + self.command_table[f"{prefix} delete"] = custom.ChangeStateDelete(loader=self) diff --git a/src/change-state/azext_change_state/custom.py b/src/change-state/azext_change_state/custom.py index 2cb450c4a2f..9a0a773ebc8 100644 --- a/src/change-state/azext_change_state/custom.py +++ b/src/change-state/azext_change_state/custom.py @@ -12,7 +12,8 @@ from azure.cli.core.aaz import has_value, AAZAnyType, AAZListArg, AAZStrArg from azure.cli.core.aaz._arg_action import AAZArgActionOperations, AAZPromptInputOperation, _ELEMENT_APPEND_KEY from azure.cli.core.azclierror import InvalidArgumentValueError -from azext_change_state.aaz.latest.change_safety.change_state import Create as _ChangeStateCreate, Update as _ChangeStateUpdate +from azext_change_state.aaz.latest.change_safety.change_state import Create as _ChangeStateCreate, Update as _ChangeStateUpdate, Show as _ChangeStateShow, Delete as _ChangeStateDelete +from azure.cli.core.aaz import AAZObjectType, AAZStrType, AAZListType logger = get_logger(__name__) @@ -111,6 +112,27 @@ def process(item): else: process(data) +def _custom_show_schema_builder(): + # Import the generated Show class + from azext_change_state.aaz.latest.change_safety.change_state._show import Show as GeneratedShow + + # Get the base schema from the generated code + base_schema = GeneratedShow.ChangeStatesGet._build_schema_on_200() + + # Inject/override the targets schema + change_definition = base_schema.properties.change_definition + details = change_definition.details + details.targets = AAZListType(flags={"read_only": True}) + details.targets.Element = AAZObjectType() + details.targets.Element.resourceId = AAZStrType() + details.targets.Element.subscriptionId = AAZStrType() + details.targets.Element.resourceGroupName = AAZStrType() + details.targets.Element.resourceType = AAZStrType() + details.targets.Element.resourceName = AAZStrType() + details.targets.Element.httpMethod = AAZStrType() + + return base_schema + class ChangeStateCreate(_ChangeStateCreate): def __init__(self, *args, **kwargs): @@ -376,3 +398,30 @@ class ChangeStatesCreateOrUpdate(_ChangeStateUpdate.ChangeStatesCreateOrUpdate): def content(self): content = super().content return _inject_change_definition_into_content(content, self.ctx) + +class ChangeStateShow(_ChangeStateShow): + def _output(self, *args, **kwargs): + result = super()._output(*args, **kwargs) + # Optionally inject targets schema into result if needed + return result + + class ChangeStatesGetAtSubscriptionLevel(_ChangeStateShow.ChangeStatesGetAtSubscriptionLevel): + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=_custom_show_schema_builder + ) + + class ChangeStatesGet(_ChangeStateShow.ChangeStatesGet): + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=_custom_show_schema_builder + ) + +class ChangeStateDelete(_ChangeStateDelete): + pass From aeb58e6c679b492be1269dc94455ebc35c36b899 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 17:15:11 -0700 Subject: [PATCH 05/15] Fix commands --- .../azext_change_state/commands.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/change-state/azext_change_state/commands.py b/src/change-state/azext_change_state/commands.py index afd3355e44f..d05000e0a8a 100644 --- a/src/change-state/azext_change_state/commands.py +++ b/src/change-state/azext_change_state/commands.py @@ -12,14 +12,14 @@ def load_command_table(self, _): # pylint: disable=unused-argument - """Apply custom command overrides after the AAZ-generated command table is loaded.""" - # Register custom commands for both 'change-state' and 'change-safety change-state' - for prefix in ["change-state", "change-safety change-state"]: - if f"{prefix} create" in self.command_table: - self.command_table[f"{prefix} create"] = custom.ChangeStateCreate(loader=self) - if f"{prefix} update" in self.command_table: - self.command_table[f"{prefix} update"] = custom.ChangeStateUpdate(loader=self) - if f"{prefix} show" in self.command_table: - self.command_table[f"{prefix} show"] = custom.ChangeStateShow(loader=self) - if f"{prefix} delete" in self.command_table: - self.command_table[f"{prefix} delete"] = custom.ChangeStateDelete(loader=self) + from .custom import ChangeStateCreate, ChangeStateUpdate, ChangeStateDelete, ChangeStateShow + + create_command = ChangeStateCreate(loader=self) + update_command = ChangeStateUpdate(loader=self) + delete_command = ChangeStateDelete(loader=self) + show_command = ChangeStateShow(loader=self) + + self.command_table['change-state create'] = create_command + self.command_table['change-state update'] = update_command + self.command_table['change-state delete'] = delete_command + self.command_table['change-state show'] = show_command From 1636d5dc97ce178009fff62e91f215a895ce9d2f Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 17:22:47 -0700 Subject: [PATCH 06/15] Add tests --- .../tests/latest/test_change_state.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/change-state/azext_change_state/tests/latest/test_change_state.py b/src/change-state/azext_change_state/tests/latest/test_change_state.py index e8fb36765e6..018dbf903c0 100644 --- a/src/change-state/azext_change_state/tests/latest/test_change_state.py +++ b/src/change-state/azext_change_state/tests/latest/test_change_state.py @@ -5,9 +5,160 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- +from types import SimpleNamespace + +import pytest from azure.cli.testsdk import * +from azext_change_state.custom import ( + ChangeStateCreate, + ChangeStateUpdate, + _inject_change_definition_into_content, + _inject_targets_into_result, +) +from azure.cli.core.azclierror import InvalidArgumentValueError + class ChangeStateScenario(ScenarioTest): # TODO: add tests here pass + + +class _SerializableValue: + """Test helper that mimics serialized AAZ values.""" + + def __init__(self, data): + self._data = data + + def to_serialized_data(self): + return self._data + + +def test_parse_targets_single_entry(): + result = ChangeStateCreate._parse_targets(["env=prod"]) + assert result == [{"env": "prod"}] + + +def test_parse_targets_with_multiple_delimiters(): + tokens = ["env=prod,region=us; role=web "] + result = ChangeStateCreate._parse_targets(tokens) + assert result == [{"env": "prod", "region": "us", "role": "web"}] + + +def test_parse_targets_rejects_invalid_entries(): + with pytest.raises(InvalidArgumentValueError): + ChangeStateCreate._parse_targets(["invalid"]) + + +def test_parse_targets_maps_resource_group_alias(): + result = ChangeStateCreate._parse_targets(["rg=my-group"]) + assert result == [{"resourceGroupName": "my-group"}] + + +def test_parse_targets_uppercases_http_method_value(): + result = ChangeStateCreate._parse_targets(["httpMethod=delete"]) + assert result == [{"httpMethod": "DELETE"}] + + +def test_parse_targets_maps_operation_to_http_method(): + result = ChangeStateCreate._parse_targets(["operation=POST,resource=/abc"]) + assert result == [{"httpMethod": "POST", "resource": "/abc"}] + + +def test_build_change_definition_uses_targets_and_name(): + cmd = object.__new__(ChangeStateCreate) + cmd._raw_targets = ["env=prod"] + cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=_SerializableValue("test-change"))) + definition = ChangeStateCreate._build_change_definition(cmd) + assert definition == { + "kind": "Targets", + "name": "test-change", + "details": {"targets": [{"env": "prod"}]} + } + + +def test_build_change_definition_normalizes_operation(): + cmd = object.__new__(ChangeStateCreate) + cmd._raw_targets = ["operation=post"] + cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=_SerializableValue("test-change"))) + definition = ChangeStateCreate._build_change_definition(cmd) + assert definition["details"]["targets"] == [{"httpMethod": "POST"}] + + +def test_build_change_definition_handles_serializable_value(): + class DummyName: + def to_serialized_data(self): + return "serialized-name" + + cmd = object.__new__(ChangeStateCreate) + cmd._raw_targets = ["env=prod"] + cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=DummyName())) + definition = ChangeStateCreate._build_change_definition(cmd) + assert definition["name"] == "serialized-name" + + +def test_command_name_overrides(): + assert ChangeStateCreate.AZ_NAME == "change-safety change-state create" + assert ChangeStateUpdate.AZ_NAME == "change-safety change-state update" + + +def test_inject_change_definition_into_dict_payload(): + ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition()))) + original = {"properties": {"existing": "value"}} + updated = _inject_change_definition_into_content(original, ctx) + assert updated["properties"]["changeDefinition"] == _valid_change_definition() + assert updated["properties"]["existing"] == "value" + + +def test_inject_change_definition_into_content_when_none(): + ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition()))) + payload = _inject_change_definition_into_content(None, ctx) + assert payload["properties"]["changeDefinition"] == _valid_change_definition() + + +def test_inject_change_definition_skips_empty_definition(): + ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue({}))) + original = {"properties": {}} + updated = _inject_change_definition_into_content(original, ctx) + assert updated is original + + +def test_inject_targets_into_result_populates_properties_container(): + targets = [{"env": "prod"}] + data = {"properties": {"changeDefinition": {"details": {}}}} + _inject_targets_into_result(data, targets) + assert data["properties"]["changeDefinition"]["details"]["targets"] == targets + + +def test_inject_targets_into_result_handles_list_payloads(): + targets = [{"httpMethod": "POST"}] + data = [{"changeDefinition": {"details": {}}}] + _inject_targets_into_result(data, targets) + assert data[0]["changeDefinition"]["details"]["targets"] == targets + + +def test_create_operation_content_includes_change_definition(): + args_schema = ChangeStateCreate._build_arguments_schema() + args = args_schema() + args.change_state_name = "test-change" + args.change_type = "AppDeployment" + args.rollout_type = "Normal" + args.anticipated_start_time = "2024-11-01T08:00:00Z" + args.anticipated_end_time = "2024-11-01T10:00:00Z" + ctx = SimpleNamespace( + args=args, + vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition())) + ) + + op = object.__new__(ChangeStateCreate.ChangeStatesCreateOrUpdate) + op.ctx = ctx + payload = op.content + assert payload["properties"]["changeDefinition"] == _valid_change_definition() + + +def _valid_change_definition(): + return { + "kind": "Targets", + "name": "test-change", + "details": {"targets": [{"resourceId": "/foo", "operation": "DELETE"}]}, + } From b26a3b2e1fdb6e19736ae11c675b774a6ea2f44c Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Wed, 29 Oct 2025 17:32:31 -0700 Subject: [PATCH 07/15] Fix style --- .../azext_change_state/commands.py | 3 - src/change-state/azext_change_state/custom.py | 80 +++++++++++++++---- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/change-state/azext_change_state/commands.py b/src/change-state/azext_change_state/commands.py index d05000e0a8a..fb19244e143 100644 --- a/src/change-state/azext_change_state/commands.py +++ b/src/change-state/azext_change_state/commands.py @@ -8,9 +8,6 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -from azext_change_state import custom - - def load_command_table(self, _): # pylint: disable=unused-argument from .custom import ChangeStateCreate, ChangeStateUpdate, ChangeStateDelete, ChangeStateShow diff --git a/src/change-state/azext_change_state/custom.py b/src/change-state/azext_change_state/custom.py index 9a0a773ebc8..47c4300dab6 100644 --- a/src/change-state/azext_change_state/custom.py +++ b/src/change-state/azext_change_state/custom.py @@ -7,18 +7,40 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements +# pylint: disable=protected-access from knack.log import get_logger -from azure.cli.core.aaz import has_value, AAZAnyType, AAZListArg, AAZStrArg -from azure.cli.core.aaz._arg_action import AAZArgActionOperations, AAZPromptInputOperation, _ELEMENT_APPEND_KEY +from azure.cli.core.aaz import ( + has_value, + AAZAnyType, + AAZListArg, + AAZStrArg, + AAZObjectType, + AAZStrType, + AAZListType, +) +from azure.cli.core.aaz._arg_action import ( + AAZArgActionOperations, + AAZPromptInputOperation, + _ELEMENT_APPEND_KEY, +) from azure.cli.core.azclierror import InvalidArgumentValueError -from azext_change_state.aaz.latest.change_safety.change_state import Create as _ChangeStateCreate, Update as _ChangeStateUpdate, Show as _ChangeStateShow, Delete as _ChangeStateDelete -from azure.cli.core.aaz import AAZObjectType, AAZStrType, AAZListType +from azext_change_state.aaz.latest.change_safety.change_state import ( + Create as _ChangeStateCreate, + Update as _ChangeStateUpdate, + Show as _ChangeStateShow, + Delete as _ChangeStateDelete, +) logger = get_logger(__name__) +def _build_any_type(): + """Utility to satisfy schema_builder callsites while keeping lint happy.""" + return AAZAnyType() + + def _inject_change_definition_into_content(content, ctx): """Attach the computed changeDefinition payload to the serialized request content.""" change_definition_value = getattr(ctx.vars, "change_definition", None) @@ -112,6 +134,7 @@ def process(item): else: process(data) + def _custom_show_schema_builder(): # Import the generated Show class from azext_change_state.aaz.latest.change_safety.change_state._show import Show as GeneratedShow @@ -171,13 +194,22 @@ def pre_operations(self): # Build and set the changeDefinition with targets change_definition = self._build_change_definition() logger.debug("Final changeDefinition for create: %s", change_definition) - self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) + self.ctx.set_var( + 'change_definition', + change_definition, + schema_builder=_build_any_type, + ) def _build_change_definition(self): """Build the changeDefinition object with targets""" targets = self._parse_targets(self._raw_targets) self._parsed_targets = targets - change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" + change_arg = self.ctx.args.change_state_name + change_name = ( + change_arg.to_serialized_data() + if has_value(change_arg) + else "Change Definition" + ) return { 'kind': 'Targets', @@ -250,13 +282,15 @@ def pre_instance_create(self): # The changeDefinition will be set in the content property of the HTTP operations pass - class ChangeStatesCreateOrUpdateAtSubscriptionLevel(_ChangeStateCreate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): + class ChangeStatesCreateOrUpdateAtSubscriptionLevel( + _ChangeStateCreate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): @property def content(self): content = super().content return _inject_change_definition_into_content(content, self.ctx) - class ChangeStatesCreateOrUpdate(_ChangeStateCreate.ChangeStatesCreateOrUpdate): + class ChangeStatesCreateOrUpdate( + _ChangeStateCreate.ChangeStatesCreateOrUpdate): @property def content(self): content = super().content @@ -298,13 +332,22 @@ def pre_operations(self): if self._raw_targets: change_definition = self._build_change_definition() logger.debug("Final changeDefinition for update: %s", change_definition) - self.ctx.set_var('change_definition', change_definition, schema_builder=lambda: AAZAnyType()) + self.ctx.set_var( + 'change_definition', + change_definition, + schema_builder=_build_any_type, + ) def _build_change_definition(self): """Build the changeDefinition object with targets""" targets = self._parse_targets(self._raw_targets) self._parsed_targets = targets - change_name = self.ctx.args.change_state_name.to_serialized_data() if has_value(self.ctx.args.change_state_name) else "Change Definition" + change_arg = self.ctx.args.change_state_name + change_name = ( + change_arg.to_serialized_data() + if has_value(change_arg) + else "Change Definition" + ) return { 'kind': 'Targets', @@ -337,7 +380,11 @@ def _parse_targets(self, raw_targets): target_entry = {} for segment in segments: if '=' not in segment: - raise InvalidArgumentValueError(f"Each --targets entry must be in key=value format. Invalid: '{segment}'") + error_message = ( + "Each --targets entry must be in key=value format. " + f"Invalid: '{segment}'" + ) + raise InvalidArgumentValueError(error_message) key, value = segment.split('=', 1) key = key.strip() @@ -380,25 +427,29 @@ def _output(self, *args, **kwargs): _inject_targets_into_result(result, self._parsed_targets) return result - def pre_instance_update(self): + def pre_instance_update(self, instance): """Set the changeDefinition in the request body before updating the instance""" + del instance change_definition = getattr(self.ctx.vars, 'change_definition', None) if change_definition is not None: # The changeDefinition will be set in the content property of the HTTP operations pass - class ChangeStatesCreateOrUpdateAtSubscriptionLevel(_ChangeStateUpdate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): + class ChangeStatesCreateOrUpdateAtSubscriptionLevel( + _ChangeStateUpdate.ChangeStatesCreateOrUpdateAtSubscriptionLevel): @property def content(self): content = super().content return _inject_change_definition_into_content(content, self.ctx) - class ChangeStatesCreateOrUpdate(_ChangeStateUpdate.ChangeStatesCreateOrUpdate): + class ChangeStatesCreateOrUpdate( + _ChangeStateUpdate.ChangeStatesCreateOrUpdate): @property def content(self): content = super().content return _inject_change_definition_into_content(content, self.ctx) + class ChangeStateShow(_ChangeStateShow): def _output(self, *args, **kwargs): result = super()._output(*args, **kwargs) @@ -423,5 +474,6 @@ def on_200(self, session): schema_builder=_custom_show_schema_builder ) + class ChangeStateDelete(_ChangeStateDelete): pass From ea95cc08fa6d117b2ce150fe8ae61e45cc87c39d Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 11:22:44 -0800 Subject: [PATCH 08/15] Rename cli extension module name --- .../HISTORY.rst | 0 src/azure-changesafety/README.md | 49 ++++++ .../azext_change_state/__init__.py | 0 .../azext_change_state/_help.py | 96 ++++++++++ .../azext_change_state/_params.py | 0 .../azext_change_state/aaz/__init__.py | 0 .../azext_change_state/aaz/latest/__init__.py | 0 .../aaz/latest/change_safety/__cmd_group.py | 2 +- .../aaz/latest/change_safety/__init__.py | 0 .../change_safety/change_state/__cmd_group.py | 2 +- .../change_safety/change_state/__init__.py | 0 .../change_safety/change_state/_create.py | 6 +- .../change_safety/change_state/_delete.py | 6 +- .../change_safety/change_state/_show.py | 6 +- .../change_safety/change_state/_update.py | 2 +- .../azext_change_state/azext_metadata.json | 0 .../azext_change_state/commands.py | 8 +- .../azext_change_state/custom.py | 13 +- .../azext_change_state/tests/__init__.py | 0 .../tests/latest/__init__.py | 0 .../tests/latest/test_change_state.py | 98 +++++++++++ .../setup.cfg | 0 .../setup.py | 0 src/change-state/README.md | 5 - src/change-state/azext_change_state/_help.py | 11 -- .../tests/latest/test_change_state.py | 164 ------------------ 26 files changed, 269 insertions(+), 199 deletions(-) rename src/{change-state => azure-changesafety}/HISTORY.rst (100%) create mode 100644 src/azure-changesafety/README.md rename src/{change-state => azure-changesafety}/azext_change_state/__init__.py (100%) create mode 100644 src/azure-changesafety/azext_change_state/_help.py rename src/{change-state => azure-changesafety}/azext_change_state/_params.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/__init__.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/__init__.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/__cmd_group.py (96%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/__init__.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py (94%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/__init__.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/_create.py (99%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/_delete.py (97%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/_show.py (99%) rename src/{change-state => azure-changesafety}/azext_change_state/aaz/latest/change_safety/change_state/_update.py (99%) rename src/{change-state => azure-changesafety}/azext_change_state/azext_metadata.json (100%) rename src/{change-state => azure-changesafety}/azext_change_state/commands.py (73%) rename src/{change-state => azure-changesafety}/azext_change_state/custom.py (97%) rename src/{change-state => azure-changesafety}/azext_change_state/tests/__init__.py (100%) rename src/{change-state => azure-changesafety}/azext_change_state/tests/latest/__init__.py (100%) create mode 100644 src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py rename src/{change-state => azure-changesafety}/setup.cfg (100%) rename src/{change-state => azure-changesafety}/setup.py (100%) delete mode 100644 src/change-state/README.md delete mode 100644 src/change-state/azext_change_state/_help.py delete mode 100644 src/change-state/azext_change_state/tests/latest/test_change_state.py diff --git a/src/change-state/HISTORY.rst b/src/azure-changesafety/HISTORY.rst similarity index 100% rename from src/change-state/HISTORY.rst rename to src/azure-changesafety/HISTORY.rst diff --git a/src/azure-changesafety/README.md b/src/azure-changesafety/README.md new file mode 100644 index 00000000000..b2077c14a44 --- /dev/null +++ b/src/azure-changesafety/README.md @@ -0,0 +1,49 @@ +# Azure CLI Change Safety Extension +Azure CLI extension for managing Change Safety `ChangeState` resources used to coordinate operational changes across Azure targets. + +## Installation +```bash +az extension add --source --yes +# or install the latest published build +az extension add --name azure-changesafety +``` + +## Commands +```bash +az changesafety changestate create # Create a ChangeState definition for one or more targets. +az changesafety changestate update # Update metadata, rollout configuration, or target definitions. +az changesafety changestate delete # Delete a ChangeState resource. +az changesafety changestate show # Display details for a ChangeState resource. +``` + +Run `az changesafety changestate -h` to see full parameter details and examples. + +## Examples +Create a ChangeState describing a web app rollout: +```bash +az changesafety changestate create \ + -g MyResourceGroup \ + -n webapp-rollout-01 \ + --change-type AppDeployment \ + --rollout-type Normal \ + --targets "resourceId=/subscriptions//resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=create" \ + --links name=Runbook uri=https://contoso.com/runbook +``` + +Update the rollout type and add a comment: +```bash +az changesafety changestate update \ + -g MyResourceGroup \ + -n webapp-rollout-01 \ + --rollout-type Emergency \ + --comments "Escalated due to customer impact" +``` + +Delete a ChangeState: +```bash +az changesafety changestate delete -g MyResourceGroup -n webapp-rollout-01 --yes +``` + +## Additional Information +- View command documentation: `az changesafety changestate -h` +- Remove the extension when no longer needed: `az extension remove --name azure-changesafety` \ No newline at end of file diff --git a/src/change-state/azext_change_state/__init__.py b/src/azure-changesafety/azext_change_state/__init__.py similarity index 100% rename from src/change-state/azext_change_state/__init__.py rename to src/azure-changesafety/azext_change_state/__init__.py diff --git a/src/azure-changesafety/azext_change_state/_help.py b/src/azure-changesafety/azext_change_state/_help.py new file mode 100644 index 00000000000..a5c041b913b --- /dev/null +++ b/src/azure-changesafety/azext_change_state/_help.py @@ -0,0 +1,96 @@ +# -------------------------------------------------------------------------------------------- +# 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: disable=line-too-long +# pylint: disable=too-many-lines + +from knack.help_files import helps # pylint: disable=unused-import + +helps['changesafety'] = """ + type: group + short-summary: Manage Change Safety resources. +""" + +helps['changesafety changestate'] = """ + type: group + short-summary: Manage ChangeState resources that describe planned changes across targets. +""" + +helps['changesafety changestate create'] = """ + type: command + short-summary: Create a ChangeState resource. + long-summary: > + Provide at least one target definition to describe which resources or operations the change + will affect. Targets are expressed as comma or semicolon separated key=value pairs such as + resourceId=RESOURCE_ID,operation=DELETE. The command is also available through the alias + `az change-safety change-state`. + parameters: + - name: --targets + short-summary: > + One or more target definitions expressed as key=value pairs (for example + resourceId=RESOURCE_ID,operation=CREATE,resourceType=Microsoft.Compute/virtualMachines). + - name: --change-type + short-summary: Classify the change such as AppDeployment, Config, ManualTouch, or PolicyDeployment. + - name: --rollout-type + short-summary: Specify the rollout urgency (Normal, Hotfix, or Emergency). + - name: --stage-map + short-summary: Reference an existing StageMap resource using resource-id=RESOURCE_ID and optional parameters key=value pairs. + - name: --links + short-summary: Add supporting links by repeating --links name=NAME uri=URL [description=TEXT]. + examples: + - name: Create a change state for a VM rollout + text: |- + az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT + - name: Create with staging rollout configuration + text: |- + az changesafety changestate create -g MyResourceGroup -n ops-change-01 --rollout-type Hotfix --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST +""" + +helps['changesafety changestate update'] = """ + type: command + short-summary: Update an existing ChangeState resource. + long-summary: > + Use this command to modify descriptive metadata, rollout settings, or replace targets for an + existing change. When you pass --targets, the supplied definitions overwrite the previous set. + This command is also available through the alias `az change-safety change-state`. + parameters: + - name: --targets + short-summary: > + Optional target definitions to replace the existing list. Provide key=value pairs such as + resourceId=RESOURCE_ID,operation=DELETE. + - name: --comments + short-summary: Provide notes about the latest update to the change state. + - name: --anticipated-start-time + short-summary: Update the expected start time in ISO 8601 format. + - name: --anticipated-end-time + short-summary: Update the expected completion time in ISO 8601 format. + examples: + - name: Adjust rollout type and add a comment + text: |- + az changesafety changestate update -g MyResourceGroup -n deploy-001 --rollout-type Emergency --comments "Escalated to emergency rollout" + - name: Replace the target definition + text: |- + az changesafety changestate update -g MyResourceGroup -n deploy-001 --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH +""" + +helps['changesafety changestate delete'] = """ + type: command + short-summary: Delete a ChangeState resource. + examples: + - name: Delete a change state without confirmation + text: |- + az changesafety changestate delete -g MyResourceGroup -n deploy-001 --yes +""" + +helps['changesafety changestate show'] = """ + type: command + short-summary: Show details for a ChangeState resource. + examples: + - name: Show a change state + text: |- + az changesafety changestate show -g MyResourceGroup -n deploy-001 +""" diff --git a/src/change-state/azext_change_state/_params.py b/src/azure-changesafety/azext_change_state/_params.py similarity index 100% rename from src/change-state/azext_change_state/_params.py rename to src/azure-changesafety/azext_change_state/_params.py diff --git a/src/change-state/azext_change_state/aaz/__init__.py b/src/azure-changesafety/azext_change_state/aaz/__init__.py similarity index 100% rename from src/change-state/azext_change_state/aaz/__init__.py rename to src/azure-changesafety/azext_change_state/aaz/__init__.py diff --git a/src/change-state/azext_change_state/aaz/latest/__init__.py b/src/azure-changesafety/azext_change_state/aaz/latest/__init__.py similarity index 100% rename from src/change-state/azext_change_state/aaz/latest/__init__.py rename to src/azure-changesafety/azext_change_state/aaz/latest/__init__.py diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__cmd_group.py similarity index 96% rename from src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__cmd_group.py index c6dd5b296ba..1734459314b 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/__cmd_group.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__cmd_group.py @@ -12,7 +12,7 @@ @register_command_group( - "change-safety", + "changesafety", ) class __CMDGroup(AAZCommandGroup): """Manage Change Safety diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__init__.py similarity index 100% rename from src/change-state/azext_change_state/aaz/latest/change_safety/__init__.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__init__.py diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py similarity index 94% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py index 00d6ce9e052..401be3b73af 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py @@ -12,7 +12,7 @@ @register_command_group( - "change-safety change-state", + "changesafety changestate", ) class __CMDGroup(AAZCommandGroup): """Manage Change State diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__init__.py similarity index 100% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/__init__.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__init__.py diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_create.py similarity index 99% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_create.py index 35c38872ced..0fe7e63f36f 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_create.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_create.py @@ -12,7 +12,7 @@ @register_command( - "change-safety change-state create", + "changesafety changestate create", ) class Create(AAZCommand): """Create a ChangeState @@ -21,8 +21,8 @@ class Create(AAZCommand): _aaz_info = { "version": "2025-09-01-preview", "resources": [ - ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], - ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], ] } diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_delete.py similarity index 97% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_delete.py index be78b96198f..96893b74400 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_delete.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_delete.py @@ -12,7 +12,7 @@ @register_command( - "change-safety change-state delete", + "changesafety changestate delete", confirmation="Are you sure you want to perform this operation?", ) class Delete(AAZCommand): @@ -22,8 +22,8 @@ class Delete(AAZCommand): _aaz_info = { "version": "2025-09-01-preview", "resources": [ - ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], - ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], ] } diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_show.py similarity index 99% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_show.py index f73fcf27901..aeb51bc3422 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_show.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_show.py @@ -12,7 +12,7 @@ @register_command( - "change-safety change-state show", + "changesafety changestate show", ) class Show(AAZCommand): """Get a ChangeState @@ -21,8 +21,8 @@ class Show(AAZCommand): _aaz_info = { "version": "2025-09-01-preview", "resources": [ - ["mgmt-plane", "/subscriptions/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], - ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.changesafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.ChangeSafety/changestates/{}", "2025-09-01-preview"], ] } diff --git a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_update.py similarity index 99% rename from src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py rename to src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_update.py index ee1213da2a6..3de21715670 100644 --- a/src/change-state/azext_change_state/aaz/latest/change_safety/change_state/_update.py +++ b/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_update.py @@ -12,7 +12,7 @@ @register_command( - "change-safety change-state update", + "changesafety changestate update", ) class Update(AAZCommand): """Update a ChangeState diff --git a/src/change-state/azext_change_state/azext_metadata.json b/src/azure-changesafety/azext_change_state/azext_metadata.json similarity index 100% rename from src/change-state/azext_change_state/azext_metadata.json rename to src/azure-changesafety/azext_change_state/azext_metadata.json diff --git a/src/change-state/azext_change_state/commands.py b/src/azure-changesafety/azext_change_state/commands.py similarity index 73% rename from src/change-state/azext_change_state/commands.py rename to src/azure-changesafety/azext_change_state/commands.py index fb19244e143..e0bfc0702af 100644 --- a/src/change-state/azext_change_state/commands.py +++ b/src/azure-changesafety/azext_change_state/commands.py @@ -16,7 +16,7 @@ def load_command_table(self, _): # pylint: disable=unused-argument delete_command = ChangeStateDelete(loader=self) show_command = ChangeStateShow(loader=self) - self.command_table['change-state create'] = create_command - self.command_table['change-state update'] = update_command - self.command_table['change-state delete'] = delete_command - self.command_table['change-state show'] = show_command + self.command_table['changesafety changestate create'] = create_command + self.command_table['changesafety changestate update'] = update_command + self.command_table['changesafety changestate delete'] = delete_command + self.command_table['changesafety changestate show'] = show_command diff --git a/src/change-state/azext_change_state/custom.py b/src/azure-changesafety/azext_change_state/custom.py similarity index 97% rename from src/change-state/azext_change_state/custom.py rename to src/azure-changesafety/azext_change_state/custom.py index 47c4300dab6..55a78c9c3b7 100644 --- a/src/change-state/azext_change_state/custom.py +++ b/src/azure-changesafety/azext_change_state/custom.py @@ -106,7 +106,14 @@ def _normalize_targets_arg(raw_targets): else: values = [raw_targets] - return [str(v) for v in values if v is not None] + normalized_values = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text: + normalized_values.append(text) + return normalized_values def _inject_targets_into_result(data, targets): @@ -171,7 +178,7 @@ def _build_arguments_schema(cls, *args, **kwargs): options=["--targets"], help=( "Target definitions expressed as key=value pairs separated by commas or semicolons. " - "Example: --targets \"resourceId=,operation=delete\"" + "Example: --targets \"resourceId=RESOURCE_ID,operation=delete\"" ), ) schema.targets.Element = AAZStrArg() @@ -311,7 +318,7 @@ def _build_arguments_schema(cls, *args, **kwargs): options=["--targets"], help=( "Optional target definitions expressed as key=value pairs separated by commas or semicolons. " - "Example: --targets \"resourceId=,operation=delete\"" + "Example: --targets \"resourceId=RESOURCE_ID,operation=delete\"" ), ) schema.targets.Element = AAZStrArg() diff --git a/src/change-state/azext_change_state/tests/__init__.py b/src/azure-changesafety/azext_change_state/tests/__init__.py similarity index 100% rename from src/change-state/azext_change_state/tests/__init__.py rename to src/azure-changesafety/azext_change_state/tests/__init__.py diff --git a/src/change-state/azext_change_state/tests/latest/__init__.py b/src/azure-changesafety/azext_change_state/tests/latest/__init__.py similarity index 100% rename from src/change-state/azext_change_state/tests/latest/__init__.py rename to src/azure-changesafety/azext_change_state/tests/latest/__init__.py diff --git a/src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py b/src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py new file mode 100644 index 00000000000..de8e1247e3c --- /dev/null +++ b/src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py @@ -0,0 +1,98 @@ +# -------------------------------------------------------------------------------------------- +# 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 +# -------------------------------------------------------------------------------------------- + +from types import SimpleNamespace + +import pytest +from azure.cli.testsdk import * + +from azext_change_state.custom import ( + ChangeStateCreate, + ChangeStateUpdate, + _inject_change_definition_into_content, + _inject_targets_into_result, + _normalize_targets_arg, +) +from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.aaz._arg_action import AAZArgActionOperations, _ELEMENT_APPEND_KEY + + +class ChangeStateScenario(ScenarioTest): + def test_normalize_targets_from_operations(self): + operations = AAZArgActionOperations.__new__(AAZArgActionOperations) + operations._ops = [ + ((_ELEMENT_APPEND_KEY,), "env=prod"), + ((0, "resourceId"), "/subscriptions/000/resourceGroups/rg/providers/Microsoft.Web/sites/app"), + ((0, "operation"), "delete"), + ((1,), "subscriptionId=00000000-0000-0000-0000-000000000000"), + ] + + normalized = _normalize_targets_arg(operations) + + assert normalized == [ + "env=prod,resourceId=/subscriptions/000/resourceGroups/rg/providers/Microsoft.Web/sites/app,operation=delete", + "subscriptionId=00000000-0000-0000-0000-000000000000", + ] + + def test_normalize_targets_from_serializable_value(self): + class DummySerializable: + def to_serialized_data(self): + return ["rg=my-rg", None, "", "operation=show"] + + normalized = _normalize_targets_arg(DummySerializable()) + + assert normalized == ["rg=my-rg", "operation=show"] + + def test_normalize_targets_from_list_of_strings(self): + raw_targets = [" resourceId=/foo ", "", "operation=PUT", None] + + normalized = _normalize_targets_arg(raw_targets) + + assert normalized == ["resourceId=/foo", "operation=PUT"] + + def test_normalize_targets_with_none_returns_empty(self): + assert _normalize_targets_arg(None) == [] + + def test_inject_change_definition_into_content_adds_properties(self): + ctx = _dummy_ctx_with_change_definition({"details": {"targets": []}}) + content = {"properties": {"existing": "value"}} + + result = _inject_change_definition_into_content(content, ctx) + + assert result["properties"]["existing"] == "value" + assert result["properties"]["changeDefinition"] == {"details": {"targets": []}} + + def test_inject_change_definition_with_empty_payload_noop(self): + ctx = _dummy_ctx_with_change_definition({}) + original = {"properties": {"foo": "bar"}} + + result = _inject_change_definition_into_content(original.copy(), ctx) + + assert result == original + + def test_inject_targets_into_result_updates_nested_properties(self): + data = {"properties": {"changeDefinition": {"details": {}}}} + targets = [{"resourceId": "/foo"}] + + _inject_targets_into_result(data, targets) + + assert data["properties"]["changeDefinition"]["details"]["targets"] == targets + + def test_inject_targets_does_not_override_existing(self): + existing = [{"resourceId": "/existing"}] + data = {"changeDefinition": {"details": {"targets": existing.copy()}}} + new_targets = [{"resourceId": "/new"}] + + _inject_targets_into_result(data, new_targets) + + assert data["changeDefinition"]["details"]["targets"] == existing + + +def _dummy_ctx_with_change_definition(payload): + dummy = SimpleNamespace() + dummy.to_serialized_data = lambda: payload + return SimpleNamespace(vars=SimpleNamespace(change_definition=dummy)) diff --git a/src/change-state/setup.cfg b/src/azure-changesafety/setup.cfg similarity index 100% rename from src/change-state/setup.cfg rename to src/azure-changesafety/setup.cfg diff --git a/src/change-state/setup.py b/src/azure-changesafety/setup.py similarity index 100% rename from src/change-state/setup.py rename to src/azure-changesafety/setup.py diff --git a/src/change-state/README.md b/src/change-state/README.md deleted file mode 100644 index 9e4e42b3250..00000000000 --- a/src/change-state/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Azure CLI ChangeState Extension # -This is an extension to Azure CLI to manage ChangeState resources. - -## How to use ## -Please add commands usage here. \ No newline at end of file diff --git a/src/change-state/azext_change_state/_help.py b/src/change-state/azext_change_state/_help.py deleted file mode 100644 index 126d5d00714..00000000000 --- a/src/change-state/azext_change_state/_help.py +++ /dev/null @@ -1,11 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# 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: disable=line-too-long -# pylint: disable=too-many-lines - -from knack.help_files import helps # pylint: disable=unused-import diff --git a/src/change-state/azext_change_state/tests/latest/test_change_state.py b/src/change-state/azext_change_state/tests/latest/test_change_state.py deleted file mode 100644 index 018dbf903c0..00000000000 --- a/src/change-state/azext_change_state/tests/latest/test_change_state.py +++ /dev/null @@ -1,164 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# 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 -# -------------------------------------------------------------------------------------------- - -from types import SimpleNamespace - -import pytest -from azure.cli.testsdk import * - -from azext_change_state.custom import ( - ChangeStateCreate, - ChangeStateUpdate, - _inject_change_definition_into_content, - _inject_targets_into_result, -) -from azure.cli.core.azclierror import InvalidArgumentValueError - - -class ChangeStateScenario(ScenarioTest): - # TODO: add tests here - pass - - -class _SerializableValue: - """Test helper that mimics serialized AAZ values.""" - - def __init__(self, data): - self._data = data - - def to_serialized_data(self): - return self._data - - -def test_parse_targets_single_entry(): - result = ChangeStateCreate._parse_targets(["env=prod"]) - assert result == [{"env": "prod"}] - - -def test_parse_targets_with_multiple_delimiters(): - tokens = ["env=prod,region=us; role=web "] - result = ChangeStateCreate._parse_targets(tokens) - assert result == [{"env": "prod", "region": "us", "role": "web"}] - - -def test_parse_targets_rejects_invalid_entries(): - with pytest.raises(InvalidArgumentValueError): - ChangeStateCreate._parse_targets(["invalid"]) - - -def test_parse_targets_maps_resource_group_alias(): - result = ChangeStateCreate._parse_targets(["rg=my-group"]) - assert result == [{"resourceGroupName": "my-group"}] - - -def test_parse_targets_uppercases_http_method_value(): - result = ChangeStateCreate._parse_targets(["httpMethod=delete"]) - assert result == [{"httpMethod": "DELETE"}] - - -def test_parse_targets_maps_operation_to_http_method(): - result = ChangeStateCreate._parse_targets(["operation=POST,resource=/abc"]) - assert result == [{"httpMethod": "POST", "resource": "/abc"}] - - -def test_build_change_definition_uses_targets_and_name(): - cmd = object.__new__(ChangeStateCreate) - cmd._raw_targets = ["env=prod"] - cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=_SerializableValue("test-change"))) - definition = ChangeStateCreate._build_change_definition(cmd) - assert definition == { - "kind": "Targets", - "name": "test-change", - "details": {"targets": [{"env": "prod"}]} - } - - -def test_build_change_definition_normalizes_operation(): - cmd = object.__new__(ChangeStateCreate) - cmd._raw_targets = ["operation=post"] - cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=_SerializableValue("test-change"))) - definition = ChangeStateCreate._build_change_definition(cmd) - assert definition["details"]["targets"] == [{"httpMethod": "POST"}] - - -def test_build_change_definition_handles_serializable_value(): - class DummyName: - def to_serialized_data(self): - return "serialized-name" - - cmd = object.__new__(ChangeStateCreate) - cmd._raw_targets = ["env=prod"] - cmd.ctx = SimpleNamespace(args=SimpleNamespace(change_state_name=DummyName())) - definition = ChangeStateCreate._build_change_definition(cmd) - assert definition["name"] == "serialized-name" - - -def test_command_name_overrides(): - assert ChangeStateCreate.AZ_NAME == "change-safety change-state create" - assert ChangeStateUpdate.AZ_NAME == "change-safety change-state update" - - -def test_inject_change_definition_into_dict_payload(): - ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition()))) - original = {"properties": {"existing": "value"}} - updated = _inject_change_definition_into_content(original, ctx) - assert updated["properties"]["changeDefinition"] == _valid_change_definition() - assert updated["properties"]["existing"] == "value" - - -def test_inject_change_definition_into_content_when_none(): - ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition()))) - payload = _inject_change_definition_into_content(None, ctx) - assert payload["properties"]["changeDefinition"] == _valid_change_definition() - - -def test_inject_change_definition_skips_empty_definition(): - ctx = SimpleNamespace(vars=SimpleNamespace(change_definition=_SerializableValue({}))) - original = {"properties": {}} - updated = _inject_change_definition_into_content(original, ctx) - assert updated is original - - -def test_inject_targets_into_result_populates_properties_container(): - targets = [{"env": "prod"}] - data = {"properties": {"changeDefinition": {"details": {}}}} - _inject_targets_into_result(data, targets) - assert data["properties"]["changeDefinition"]["details"]["targets"] == targets - - -def test_inject_targets_into_result_handles_list_payloads(): - targets = [{"httpMethod": "POST"}] - data = [{"changeDefinition": {"details": {}}}] - _inject_targets_into_result(data, targets) - assert data[0]["changeDefinition"]["details"]["targets"] == targets - - -def test_create_operation_content_includes_change_definition(): - args_schema = ChangeStateCreate._build_arguments_schema() - args = args_schema() - args.change_state_name = "test-change" - args.change_type = "AppDeployment" - args.rollout_type = "Normal" - args.anticipated_start_time = "2024-11-01T08:00:00Z" - args.anticipated_end_time = "2024-11-01T10:00:00Z" - ctx = SimpleNamespace( - args=args, - vars=SimpleNamespace(change_definition=_SerializableValue(_valid_change_definition())) - ) - - op = object.__new__(ChangeStateCreate.ChangeStatesCreateOrUpdate) - op.ctx = ctx - payload = op.content - assert payload["properties"]["changeDefinition"] == _valid_change_definition() - - -def _valid_change_definition(): - return { - "kind": "Targets", - "name": "test-change", - "details": {"targets": [{"resourceId": "/foo", "operation": "DELETE"}]}, - } From bdeffae6b9782d806da27482b5b4c0e6cd3697d9 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 13:01:03 -0800 Subject: [PATCH 09/15] Fix linter --- src/azure-changesafety/azext_change_state/_help.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/azure-changesafety/azext_change_state/_help.py b/src/azure-changesafety/azext_change_state/_help.py index a5c041b913b..26b4a50bd87 100644 --- a/src/azure-changesafety/azext_change_state/_help.py +++ b/src/azure-changesafety/azext_change_state/_help.py @@ -44,10 +44,10 @@ examples: - name: Create a change state for a VM rollout text: |- - az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT + az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT" - name: Create with staging rollout configuration text: |- - az changesafety changestate create -g MyResourceGroup -n ops-change-01 --rollout-type Hotfix --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST + az changesafety changestate create -g MyResourceGroup -n ops-change-01 --rollout-type Hotfix --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST" """ helps['changesafety changestate update'] = """ @@ -74,7 +74,7 @@ az changesafety changestate update -g MyResourceGroup -n deploy-001 --rollout-type Emergency --comments "Escalated to emergency rollout" - name: Replace the target definition text: |- - az changesafety changestate update -g MyResourceGroup -n deploy-001 --targets resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH + az changesafety changestate update -g MyResourceGroup -n deploy-001 --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH" """ helps['changesafety changestate delete'] = """ From 78286ddac1ac3991cb66ce89176340464c0ffabe Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 13:21:25 -0800 Subject: [PATCH 10/15] Fix linter --- src/azure-changesafety/azext_change_state/_help.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/azure-changesafety/azext_change_state/_help.py b/src/azure-changesafety/azext_change_state/_help.py index 26b4a50bd87..0c231b6d404 100644 --- a/src/azure-changesafety/azext_change_state/_help.py +++ b/src/azure-changesafety/azext_change_state/_help.py @@ -42,6 +42,9 @@ - name: --links short-summary: Add supporting links by repeating --links name=NAME uri=URL [description=TEXT]. examples: + - name: Create with stage map reference and status link + text: |- + az changesafety changestate create -g MyResourceGroup -n deploy-002 --change-type ManualTouch --rollout-type Normal --stage-map "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rollout-stage-map" --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH" --links name=status uri=https://contoso.com/change/rollout-002 - name: Create a change state for a VM rollout text: |- az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT" @@ -72,6 +75,9 @@ - name: Adjust rollout type and add a comment text: |- az changesafety changestate update -g MyResourceGroup -n deploy-001 --rollout-type Emergency --comments "Escalated to emergency rollout" + - name: Update scheduling window + text: |- + az changesafety changestate update -g MyResourceGroup -n deploy-001 --anticipated-start-time "2024-09-01T08:00:00Z" --anticipated-end-time "2024-09-01T12:00:00Z" - name: Replace the target definition text: |- az changesafety changestate update -g MyResourceGroup -n deploy-001 --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH" From 56b725b968d8c90d1168f5d6d8752c29372fafac Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 13:39:21 -0800 Subject: [PATCH 11/15] Fix linter --- src/azure-changesafety/azext_change_state/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-changesafety/azext_change_state/_help.py b/src/azure-changesafety/azext_change_state/_help.py index 0c231b6d404..cae17e48d1c 100644 --- a/src/azure-changesafety/azext_change_state/_help.py +++ b/src/azure-changesafety/azext_change_state/_help.py @@ -44,7 +44,7 @@ examples: - name: Create with stage map reference and status link text: |- - az changesafety changestate create -g MyResourceGroup -n deploy-002 --change-type ManualTouch --rollout-type Normal --stage-map "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rollout-stage-map" --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH" --links name=status uri=https://contoso.com/change/rollout-002 + az changesafety changestate create -g MyResourceGroup -n deploy-002 --change-type ManualTouch --rollout-type Normal --stage-map "{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rollout-stage-map}" --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH" --links "[{name:status,uri:'https://contoso.com/change/rollout-002'}]" - name: Create a change state for a VM rollout text: |- az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT" From 964371c0a29ce9b6630181e7e5140d5d10d88468 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 13:48:25 -0800 Subject: [PATCH 12/15] Fix module name --- .../__init__.py | 8 ++++---- .../{azext_change_state => azext_changesafety}/_help.py | 0 .../{azext_change_state => azext_changesafety}/_params.py | 0 .../aaz/__init__.py | 0 .../aaz/latest/__init__.py | 0 .../aaz/latest/change_safety/__cmd_group.py | 0 .../aaz/latest/change_safety/__init__.py | 0 .../aaz/latest/change_safety/change_state/__cmd_group.py | 0 .../aaz/latest/change_safety/change_state/__init__.py | 0 .../aaz/latest/change_safety/change_state/_create.py | 0 .../aaz/latest/change_safety/change_state/_delete.py | 0 .../aaz/latest/change_safety/change_state/_show.py | 0 .../aaz/latest/change_safety/change_state/_update.py | 0 .../azext_metadata.json | 0 .../commands.py | 0 .../{azext_change_state => azext_changesafety}/custom.py | 4 ++-- .../tests/__init__.py | 0 .../tests/latest/__init__.py | 0 .../tests/latest/test_change_state.py | 2 +- src/azure-changesafety/setup.py | 2 +- 20 files changed, 8 insertions(+), 8 deletions(-) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/__init__.py (83%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/_help.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/_params.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/__cmd_group.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/__cmd_group.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/_create.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/_delete.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/_show.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/aaz/latest/change_safety/change_state/_update.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/azext_metadata.json (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/commands.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/custom.py (99%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/tests/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/tests/latest/__init__.py (100%) rename src/azure-changesafety/{azext_change_state => azext_changesafety}/tests/latest/test_change_state.py (98%) diff --git a/src/azure-changesafety/azext_change_state/__init__.py b/src/azure-changesafety/azext_changesafety/__init__.py similarity index 83% rename from src/azure-changesafety/azext_change_state/__init__.py rename to src/azure-changesafety/azext_changesafety/__init__.py index a2755a92cdb..05751262e4f 100644 --- a/src/azure-changesafety/azext_change_state/__init__.py +++ b/src/azure-changesafety/azext_changesafety/__init__.py @@ -6,7 +6,7 @@ # -------------------------------------------------------------------------------------------- from azure.cli.core import AzCommandsLoader -from azext_change_state._help import helps # pylint: disable=unused-import +from azext_changesafety._help import helps # pylint: disable=unused-import class ChangeStateCommandsLoader(AzCommandsLoader): @@ -14,12 +14,12 @@ class ChangeStateCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType custom_command_type = CliCommandType( - operations_tmpl='azext_change_state.custom#{}') + operations_tmpl='azext_changesafety.custom#{}') super().__init__(cli_ctx=cli_ctx, custom_command_type=custom_command_type) def load_command_table(self, args): - from azext_change_state.commands import load_command_table + from azext_changesafety.commands import load_command_table from azure.cli.core.aaz import load_aaz_command_table try: from . import aaz @@ -35,7 +35,7 @@ def load_command_table(self, args): return self.command_table def load_arguments(self, command): - from azext_change_state._params import load_arguments + from azext_changesafety._params import load_arguments load_arguments(self, command) diff --git a/src/azure-changesafety/azext_change_state/_help.py b/src/azure-changesafety/azext_changesafety/_help.py similarity index 100% rename from src/azure-changesafety/azext_change_state/_help.py rename to src/azure-changesafety/azext_changesafety/_help.py diff --git a/src/azure-changesafety/azext_change_state/_params.py b/src/azure-changesafety/azext_changesafety/_params.py similarity index 100% rename from src/azure-changesafety/azext_change_state/_params.py rename to src/azure-changesafety/azext_changesafety/_params.py diff --git a/src/azure-changesafety/azext_change_state/aaz/__init__.py b/src/azure-changesafety/azext_changesafety/aaz/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/__init__.py rename to src/azure-changesafety/azext_changesafety/aaz/__init__.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/__init__.py b/src/azure-changesafety/azext_changesafety/aaz/latest/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/__init__.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/__init__.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__cmd_group.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/__cmd_group.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__cmd_group.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/__cmd_group.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__init__.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/__init__.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/__init__.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/__cmd_group.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__cmd_group.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/__cmd_group.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__init__.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/__init__.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/__init__.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_create.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_create.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_create.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_create.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_delete.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_delete.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_delete.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_delete.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_show.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_show.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_show.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_show.py diff --git a/src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_update.py b/src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_update.py similarity index 100% rename from src/azure-changesafety/azext_change_state/aaz/latest/change_safety/change_state/_update.py rename to src/azure-changesafety/azext_changesafety/aaz/latest/change_safety/change_state/_update.py diff --git a/src/azure-changesafety/azext_change_state/azext_metadata.json b/src/azure-changesafety/azext_changesafety/azext_metadata.json similarity index 100% rename from src/azure-changesafety/azext_change_state/azext_metadata.json rename to src/azure-changesafety/azext_changesafety/azext_metadata.json diff --git a/src/azure-changesafety/azext_change_state/commands.py b/src/azure-changesafety/azext_changesafety/commands.py similarity index 100% rename from src/azure-changesafety/azext_change_state/commands.py rename to src/azure-changesafety/azext_changesafety/commands.py diff --git a/src/azure-changesafety/azext_change_state/custom.py b/src/azure-changesafety/azext_changesafety/custom.py similarity index 99% rename from src/azure-changesafety/azext_change_state/custom.py rename to src/azure-changesafety/azext_changesafety/custom.py index 55a78c9c3b7..25f13f592e4 100644 --- a/src/azure-changesafety/azext_change_state/custom.py +++ b/src/azure-changesafety/azext_changesafety/custom.py @@ -25,7 +25,7 @@ _ELEMENT_APPEND_KEY, ) from azure.cli.core.azclierror import InvalidArgumentValueError -from azext_change_state.aaz.latest.change_safety.change_state import ( +from azext_changesafety.aaz.latest.change_safety.change_state import ( Create as _ChangeStateCreate, Update as _ChangeStateUpdate, Show as _ChangeStateShow, @@ -144,7 +144,7 @@ def process(item): def _custom_show_schema_builder(): # Import the generated Show class - from azext_change_state.aaz.latest.change_safety.change_state._show import Show as GeneratedShow + from azext_changesafety.aaz.latest.change_safety.change_state._show import Show as GeneratedShow # Get the base schema from the generated code base_schema = GeneratedShow.ChangeStatesGet._build_schema_on_200() diff --git a/src/azure-changesafety/azext_change_state/tests/__init__.py b/src/azure-changesafety/azext_changesafety/tests/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/tests/__init__.py rename to src/azure-changesafety/azext_changesafety/tests/__init__.py diff --git a/src/azure-changesafety/azext_change_state/tests/latest/__init__.py b/src/azure-changesafety/azext_changesafety/tests/latest/__init__.py similarity index 100% rename from src/azure-changesafety/azext_change_state/tests/latest/__init__.py rename to src/azure-changesafety/azext_changesafety/tests/latest/__init__.py diff --git a/src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py b/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py similarity index 98% rename from src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py rename to src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py index de8e1247e3c..91909a8db71 100644 --- a/src/azure-changesafety/azext_change_state/tests/latest/test_change_state.py +++ b/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py @@ -10,7 +10,7 @@ import pytest from azure.cli.testsdk import * -from azext_change_state.custom import ( +from azext_changesafety.custom import ( ChangeStateCreate, ChangeStateUpdate, _inject_change_definition_into_content, diff --git a/src/azure-changesafety/setup.py b/src/azure-changesafety/setup.py index 686df8315a1..79dda33fcd9 100644 --- a/src/azure-changesafety/setup.py +++ b/src/azure-changesafety/setup.py @@ -44,6 +44,6 @@ url='https://github.com/Azure/azure-cli-extensions/tree/main/src/change-state', classifiers=CLASSIFIERS, packages=find_packages(exclude=["tests"]), - package_data={'azext_change_state': ['azext_metadata.json']}, + package_data={'azext_changesafety': ['azext_metadata.json']}, install_requires=DEPENDENCIES ) From 3f5b4d7cfe4039801837c9a1a6e557ef8ad5731e Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 23:35:37 -0800 Subject: [PATCH 13/15] add test scenarios --- src/azure-changesafety/README.md | 8 +- .../azext_changesafety/_help.py | 23 +- .../azext_changesafety/custom.py | 75 ++++++ .../test_change_state_cli_scenario.yaml | 106 ++++++++ .../tests/latest/test_change_state.py | 248 +++++++++++++++++- 5 files changed, 435 insertions(+), 25 deletions(-) create mode 100644 src/azure-changesafety/azext_changesafety/tests/latest/recordings/test_change_state_cli_scenario.yaml diff --git a/src/azure-changesafety/README.md b/src/azure-changesafety/README.md index b2077c14a44..89e7d4aae7c 100644 --- a/src/azure-changesafety/README.md +++ b/src/azure-changesafety/README.md @@ -23,7 +23,7 @@ Create a ChangeState describing a web app rollout: ```bash az changesafety changestate create \ -g MyResourceGroup \ - -n webapp-rollout-01 \ + -n webAppRollout01 \ --change-type AppDeployment \ --rollout-type Normal \ --targets "resourceId=/subscriptions//resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=create" \ @@ -34,16 +34,16 @@ Update the rollout type and add a comment: ```bash az changesafety changestate update \ -g MyResourceGroup \ - -n webapp-rollout-01 \ + -n webAppRollout01 \ --rollout-type Emergency \ --comments "Escalated due to customer impact" ``` Delete a ChangeState: ```bash -az changesafety changestate delete -g MyResourceGroup -n webapp-rollout-01 --yes +az changesafety changestate delete -g MyResourceGroup -n webAppRollout01 --yes ``` ## Additional Information - View command documentation: `az changesafety changestate -h` -- Remove the extension when no longer needed: `az extension remove --name azure-changesafety` \ No newline at end of file +- Remove the extension when no longer needed: `az extension remove --name azure-changesafety` diff --git a/src/azure-changesafety/azext_changesafety/_help.py b/src/azure-changesafety/azext_changesafety/_help.py index cae17e48d1c..c6174626b2d 100644 --- a/src/azure-changesafety/azext_changesafety/_help.py +++ b/src/azure-changesafety/azext_changesafety/_help.py @@ -24,7 +24,7 @@ type: command short-summary: Create a ChangeState resource. long-summary: > - Provide at least one target definition to describe which resources or operations the change + Provide at least one target definition to describe which resources or operations the ChangeState will affect. Targets are expressed as comma or semicolon separated key=value pairs such as resourceId=RESOURCE_ID,operation=DELETE. The command is also available through the alias `az change-safety change-state`. @@ -42,15 +42,16 @@ - name: --links short-summary: Add supporting links by repeating --links name=NAME uri=URL [description=TEXT]. examples: - - name: Create with stage map reference and status link + - name: Create with StageMap reference and status link text: |- - az changesafety changestate create -g MyResourceGroup -n deploy-002 --change-type ManualTouch --rollout-type Normal --stage-map "{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rollout-stage-map}" --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH" --links "[{name:status,uri:'https://contoso.com/change/rollout-002'}]" + az changesafety changestate create -g MyResourceGroup -n changestate002 --change-type ManualTouch --rollout-type Normal --stage-map "{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rolloutStageMap}" --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH" --links "[{name:status,uri:'https://contoso.com/change/rollout-002'}]" + az changesafety changestate delete -g MyResourceGroup -n changestate002 --yes - name: Create a change state for a VM rollout text: |- - az changesafety changestate create -g MyResourceGroup -n deploy-001 --change-type AppDeployment --rollout-type Normal --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT" + az changesafety changestate create -g MyResourceGroup -n changestate001 --change-type AppDeployment --rollout-type Normal --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT" - name: Create with staging rollout configuration text: |- - az changesafety changestate create -g MyResourceGroup -n ops-change-01 --rollout-type Hotfix --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST" + az changesafety changestate create -g MyResourceGroup -n opsChange01 --rollout-type Hotfix --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST" """ helps['changesafety changestate update'] = """ @@ -58,7 +59,7 @@ short-summary: Update an existing ChangeState resource. long-summary: > Use this command to modify descriptive metadata, rollout settings, or replace targets for an - existing change. When you pass --targets, the supplied definitions overwrite the previous set. + existing ChangeState. When you pass --targets, the supplied definitions overwrite the previous set. This command is also available through the alias `az change-safety change-state`. parameters: - name: --targets @@ -74,13 +75,13 @@ examples: - name: Adjust rollout type and add a comment text: |- - az changesafety changestate update -g MyResourceGroup -n deploy-001 --rollout-type Emergency --comments "Escalated to emergency rollout" + az changesafety changestate update -g MyResourceGroup -n changestate001 --rollout-type Emergency --comments "Escalated to emergency rollout" - name: Update scheduling window text: |- - az changesafety changestate update -g MyResourceGroup -n deploy-001 --anticipated-start-time "2024-09-01T08:00:00Z" --anticipated-end-time "2024-09-01T12:00:00Z" + az changesafety changestate update -g MyResourceGroup -n changestate001 --anticipated-start-time "2024-09-01T08:00:00Z" --anticipated-end-time "2024-09-01T12:00:00Z" - name: Replace the target definition text: |- - az changesafety changestate update -g MyResourceGroup -n deploy-001 --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH" + az changesafety changestate update -g MyResourceGroup -n changestate001 --targets "resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH" """ helps['changesafety changestate delete'] = """ @@ -89,7 +90,7 @@ examples: - name: Delete a change state without confirmation text: |- - az changesafety changestate delete -g MyResourceGroup -n deploy-001 --yes + az changesafety changestate delete -g MyResourceGroup -n changestate001 --yes """ helps['changesafety changestate show'] = """ @@ -98,5 +99,5 @@ examples: - name: Show a change state text: |- - az changesafety changestate show -g MyResourceGroup -n deploy-001 + az changesafety changestate show -g MyResourceGroup -n changestate001 """ diff --git a/src/azure-changesafety/azext_changesafety/custom.py b/src/azure-changesafety/azext_changesafety/custom.py index 25f13f592e4..621aa9bbb1e 100644 --- a/src/azure-changesafety/azext_changesafety/custom.py +++ b/src/azure-changesafety/azext_changesafety/custom.py @@ -484,3 +484,78 @@ def on_200(self, session): class ChangeStateDelete(_ChangeStateDelete): pass + + +ChangeStateCreate.AZ_HELP["examples"] = [ + { + "name": "Create with StageMap reference and status link", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n changestate002 " + "--change-type ManualTouch --rollout-type Normal " + "--stage-map \"{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rolloutStageMap}\" " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH\" " + "--links \"[{name:status,uri:'https://contoso.com/change/rollout-002'}]\"\n" + "az changesafety changestate delete -g MyResourceGroup -n changestate002 --yes" + ), + }, + { + "name": "Create a change state for a VM rollout", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n changestate001 " + "--change-type AppDeployment --rollout-type Normal " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT\"" + ), + }, + { + "name": "Create with staging rollout configuration", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n opsChange01 " + "--rollout-type Hotfix " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST\"" + ), + }, +] + +ChangeStateUpdate.AZ_HELP["examples"] = [ + { + "name": "Adjust rollout type and add a comment", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--rollout-type Emergency --comments \"Escalated to emergency rollout\"" + ), + }, + { + "name": "Update scheduling window", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--anticipated-start-time \"2024-09-01T08:00:00Z\" " + "--anticipated-end-time \"2024-09-01T12:00:00Z\"" + ), + }, + { + "name": "Replace the target definition", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH\"" + ), + }, +] + +ChangeStateDelete.AZ_HELP["examples"] = [ + { + "name": "Delete a change state without confirmation", + "text": "az changesafety changestate delete -g MyResourceGroup -n changestate001 --yes", + }, +] + +ChangeStateShow.AZ_HELP["examples"] = [ + { + "name": "Show a change state", + "text": "az changesafety changestate show -g MyResourceGroup -n changestate001", + }, +] diff --git a/src/azure-changesafety/azext_changesafety/tests/latest/recordings/test_change_state_cli_scenario.yaml b/src/azure-changesafety/azext_changesafety/tests/latest/recordings/test_change_state_cli_scenario.yaml new file mode 100644 index 00000000000..f218d0e082d --- /dev/null +++ b/src/azure-changesafety/azext_changesafety/tests/latest/recordings/test_change_state_cli_scenario.yaml @@ -0,0 +1,106 @@ +interactions: +- request: + body: '{"location": "eastus", "properties": {"changeType": "ManualTouch", "rolloutType": "Normal", "changeDefinition": {"kind": "Targets", "name": "changestate002", "details": {"targets": [{"resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.Compute/virtualMachines/myVm", "httpMethod": "PATCH"}]}}}, "tags": null}' + headers: + Accept: + - application/json + CommandName: + - changesafety changestate create + Connection: + - keep-alive + Content-Type: + - application/json + ParameterSetName: + - -g -n --change-type --rollout-type --targets --stage-map --links --comments + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview + response: + body: + string: '{"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002", "name": "changestate002", "type": "Microsoft.ChangeSafety/changeStates", "location": "eastus", "properties": {"changeType": "ManualTouch", "rolloutType": "Normal", "comments": "Initial deployment", "changeDefinition": {"kind": "Targets", "name": "changestate002", "details": {"targets": [{"resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.Compute/virtualMachines/myVm", "httpMethod": "PATCH"}]}}}}' + headers: + Content-Type: + - application/json + Date: + - Mon, 03 Nov 2025 18:00:00 GMT + status: + code: 200 + message: OK + url: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview +- request: + body: '{"properties": {"rolloutType": "Emergency", "comments": "Escalated rollout"}}' + headers: + Accept: + - application/json + CommandName: + - changesafety changestate update + Connection: + - keep-alive + Content-Type: + - application/json + ParameterSetName: + - -g -n --rollout-type --comments + method: PATCH + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview + response: + body: + string: '{"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002", "name": "changestate002", "type": "Microsoft.ChangeSafety/changeStates", "location": "eastus", "properties": {"changeType": "ManualTouch", "rolloutType": "Emergency", "comments": "Escalated rollout", "changeDefinition": {"kind": "Targets", "name": "changestate002", "details": {"targets": [{"resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.Compute/virtualMachines/myVm", "httpMethod": "PATCH"}]}}}}' + headers: + Content-Type: + - application/json + Date: + - Mon, 03 Nov 2025 18:01:00 GMT + status: + code: 200 + message: OK + url: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview +- request: + body: '' + headers: + Accept: + - application/json + CommandName: + - changesafety changestate show + Connection: + - keep-alive + ParameterSetName: + - -g -n + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview + response: + body: + string: '{"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002", "name": "changestate002", "type": "Microsoft.ChangeSafety/changeStates", "location": "eastus", "properties": {"changeType": "ManualTouch", "rolloutType": "Emergency", "comments": "Escalated rollout", "changeDefinition": {"kind": "Targets", "name": "changestate002", "details": {"targets": [{"resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.Compute/virtualMachines/myVm", "httpMethod": "PATCH"}]}}}}' + headers: + Content-Type: + - application/json + Date: + - Mon, 03 Nov 2025 18:02:00 GMT + status: + code: 200 + message: OK + url: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview +- request: + body: '' + headers: + Accept: + - application/json + CommandName: + - changesafety changestate delete + Connection: + - keep-alive + ParameterSetName: + - -g -n --yes + method: DELETE + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview + response: + body: + string: '' + headers: + Content-Type: + - application/json + Date: + - Mon, 03 Nov 2025 18:03:00 GMT + status: + code: 204 + message: No Content + url: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rgChangeSafetyScenario/providers/Microsoft.ChangeSafety/changeStates/changestate002?api-version=2025-09-01-preview +version: 1 diff --git a/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py b/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py index 91909a8db71..0fbb2d5d35c 100644 --- a/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py +++ b/src/azure-changesafety/azext_changesafety/tests/latest/test_change_state.py @@ -5,26 +5,209 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- +import copy +import sys +import types from types import SimpleNamespace +from unittest import mock -import pytest -from azure.cli.testsdk import * +from azure.cli.testsdk import * # pylint: disable=wildcard-import,unused-wildcard-import from azext_changesafety.custom import ( ChangeStateCreate, + ChangeStateDelete, + ChangeStateShow, ChangeStateUpdate, _inject_change_definition_into_content, _inject_targets_into_result, _normalize_targets_arg, ) -from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.aaz import AAZAnyType, has_value from azure.cli.core.aaz._arg_action import AAZArgActionOperations, _ELEMENT_APPEND_KEY class ChangeStateScenario(ScenarioTest): + FAKE_SUBSCRIPTION_ID = "00000000-0000-0000-0000-000000000000" + _SCENARIO_STATE = {} + + class _DummyPoller: # pylint: disable=too-few-public-methods + def result(self, timeout=None): # pylint: disable=unused-argument + return None + + def wait(self, timeout=None): # pylint: disable=unused-argument + return None + + def done(self): + return True + + def add_done_callback(self, func): + if func: + func(self) + + @staticmethod + def _dummy_ctx_with_change_definition(payload): + dummy = SimpleNamespace() + dummy.to_serialized_data = lambda: payload + return SimpleNamespace(vars=SimpleNamespace(change_definition=dummy)) + + @classmethod + def _ensure_msrestazure_stub(cls): + if 'msrestazure' in sys.modules: + return + + msrestazure = types.ModuleType('msrestazure') + azure_operation = types.ModuleType('msrestazure.azure_operation') + + class AzureOperationPoller: # pylint: disable=too-few-public-methods + def _delay(self, *args, **kwargs): # pylint: disable=unused-argument + return + + azure_operation.AzureOperationPoller = AzureOperationPoller + arm_polling = types.ModuleType('msrestazure.polling.arm_polling') + + class ARMPolling: # pylint: disable=too-few-public-methods + def _delay(self, *args, **kwargs): # pylint: disable=unused-argument + return + + arm_polling.ARMPolling = ARMPolling + polling = types.ModuleType('msrestazure.polling') + polling.arm_polling = arm_polling + + msrestazure.azure_operation = azure_operation + msrestazure.polling = polling + + sys.modules['msrestazure'] = msrestazure + sys.modules['msrestazure.azure_operation'] = azure_operation + sys.modules['msrestazure.polling'] = polling + sys.modules['msrestazure.polling.arm_polling'] = arm_polling + + @staticmethod + def _get_arg_value(cmd, arg_name, default=None): + arg = getattr(cmd.ctx.args, arg_name, None) + if arg is None or not has_value(arg): + return default + return arg.to_serialized_data() + + @staticmethod + def _build_mock_instance(name, resource_group, subscription_id, change_type, rollout_type, targets, comments=None): + return { + "id": f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.ChangeSafety/changeStates/{name}", + "name": name, + "type": "Microsoft.ChangeSafety/changeStates", + "location": "eastus", + "properties": { + "changeType": change_type, + "rolloutType": rollout_type, + "comments": comments, + "changeDefinition": { + "kind": "Targets", + "name": name, + "details": { + "targets": targets or [] + } + } + } + } + + @staticmethod + def _mock_create_execute(cmd): + cls = ChangeStateScenario + cmd.pre_operations() + name = cls._get_arg_value(cmd, "change_state_name", "mock-change") + resource_group = cls._get_arg_value(cmd, "resource_group", "mock-rg") + subscription_id = cmd.ctx.subscription_id or cls.FAKE_SUBSCRIPTION_ID + change_type = cls._get_arg_value(cmd, "change_type", "ManualTouch") + rollout_type = cls._get_arg_value(cmd, "rollout_type", "Normal") + comments = cls._get_arg_value(cmd, "comments") + targets = copy.deepcopy(cmd._parsed_targets or []) + instance = cls._build_mock_instance( + name=name, + resource_group=resource_group, + subscription_id=subscription_id, + change_type=change_type, + rollout_type=rollout_type, + targets=targets, + comments=comments, + ) + cls._SCENARIO_STATE["instance"] = copy.deepcopy(instance) + cmd.ctx.set_var("instance", copy.deepcopy(instance), schema_builder=lambda: AAZAnyType()) + cmd.post_operations() + return iter(()) + + @staticmethod + def _mock_update_execute(cmd): + cls = ChangeStateScenario + cmd._raw_targets = [token for token in (cmd._raw_targets or []) if token and token != 'Undefined'] # pylint: disable=protected-access + cmd.pre_operations() + current = copy.deepcopy(cls._SCENARIO_STATE.get("instance")) + if current is None: + name = cls._get_arg_value(cmd, "change_state_name", "mock-change") + resource_group = cls._get_arg_value(cmd, "resource_group", "mock-rg") + subscription_id = cmd.ctx.subscription_id or cls.FAKE_SUBSCRIPTION_ID + current = cls._build_mock_instance( + name=name, + resource_group=resource_group, + subscription_id=subscription_id, + change_type="ManualTouch", + rollout_type="Normal", + targets=[], + ) + new_change_type = cls._get_arg_value(cmd, "change_type") + new_rollout = cls._get_arg_value(cmd, "rollout_type") + new_comments = cls._get_arg_value(cmd, "comments") + if new_change_type: + current["properties"]["changeType"] = new_change_type + if new_rollout: + current["properties"]["rolloutType"] = new_rollout + if new_comments is not None: + current["properties"]["comments"] = new_comments + if cmd._parsed_targets: # pylint: disable=protected-access + current["properties"]["changeDefinition"]["details"]["targets"] = copy.deepcopy(cmd._parsed_targets) # pylint: disable=protected-access + cls._SCENARIO_STATE["instance"] = copy.deepcopy(current) + cmd.ctx.set_var("instance", copy.deepcopy(current), schema_builder=lambda: AAZAnyType()) + cmd.post_operations() + return iter(()) + + @staticmethod + def _mock_show_execute(cmd): + cls = ChangeStateScenario + cmd.pre_operations() + instance = copy.deepcopy(cls._SCENARIO_STATE.get("instance")) + cmd.ctx.set_var("instance", instance, schema_builder=lambda: AAZAnyType()) + cmd.post_operations() + return iter(()) + + @staticmethod + def _mock_delete_execute(cmd): + cls = ChangeStateScenario + cmd.pre_operations() + cls._SCENARIO_STATE.pop("instance", None) + cmd.post_operations() + return iter(()) + + @staticmethod + def _mock_build_lro_poller(cmd, executor, extract_result): # pylint: disable=unused-argument + executor() + return ChangeStateScenario._DummyPoller() # pylint: disable=protected-access + + def setUp(self): + type(self)._ensure_msrestazure_stub() + super().setUp() + type(self)._SCENARIO_STATE.clear() + self._patchers = [ + mock.patch('azext_changesafety.custom.ChangeStateCreate._execute_operations', new=type(self)._mock_create_execute), + mock.patch('azext_changesafety.custom.ChangeStateUpdate._execute_operations', new=type(self)._mock_update_execute), + mock.patch('azext_changesafety.custom.ChangeStateShow._execute_operations', new=type(self)._mock_show_execute), + mock.patch('azext_changesafety.custom.ChangeStateDelete._execute_operations', new=type(self)._mock_delete_execute), + mock.patch('azext_changesafety.custom.ChangeStateDelete.build_lro_poller', new=type(self)._mock_build_lro_poller), + ] + for patcher in self._patchers: + patcher.start() + self.addCleanup(patcher.stop) + def test_normalize_targets_from_operations(self): operations = AAZArgActionOperations.__new__(AAZArgActionOperations) - operations._ops = [ + operations._ops = [ # pylint: disable=protected-access ((_ELEMENT_APPEND_KEY,), "env=prod"), ((0, "resourceId"), "/subscriptions/000/resourceGroups/rg/providers/Microsoft.Web/sites/app"), ((0, "operation"), "delete"), @@ -58,7 +241,7 @@ def test_normalize_targets_with_none_returns_empty(self): assert _normalize_targets_arg(None) == [] def test_inject_change_definition_into_content_adds_properties(self): - ctx = _dummy_ctx_with_change_definition({"details": {"targets": []}}) + ctx = self._dummy_ctx_with_change_definition({"details": {"targets": []}}) content = {"properties": {"existing": "value"}} result = _inject_change_definition_into_content(content, ctx) @@ -67,7 +250,7 @@ def test_inject_change_definition_into_content_adds_properties(self): assert result["properties"]["changeDefinition"] == {"details": {"targets": []}} def test_inject_change_definition_with_empty_payload_noop(self): - ctx = _dummy_ctx_with_change_definition({}) + ctx = self._dummy_ctx_with_change_definition({}) original = {"properties": {"foo": "bar"}} result = _inject_change_definition_into_content(original.copy(), ctx) @@ -91,8 +274,53 @@ def test_inject_targets_does_not_override_existing(self): assert data["changeDefinition"]["details"]["targets"] == existing + def test_change_state_cli_scenario(self): + resource_group = "rgChangeSafetyScenario" + change_state_name = self.create_random_name('chg', 12) + target_resource = ( + f"/subscriptions/{self.FAKE_SUBSCRIPTION_ID}/resourceGroups/{resource_group}/" + "providers/Microsoft.Compute/virtualMachines/myVm" + ) + self.kwargs.update({ + "rg": resource_group, + "name": change_state_name, + "change_type": "ManualTouch", + "rollout_type": "Normal", + "updated_rollout": "Emergency", + "targets": f"resourceId={target_resource},operation=PATCH", + }) + + create_checks = [ + JMESPathCheck('name', change_state_name), + JMESPathCheck('properties.changeType', 'ManualTouch'), + JMESPathCheck('properties.rolloutType', 'Normal'), + JMESPathCheck('properties.changeDefinition.details.targets[0].resourceId', target_resource), + JMESPathCheck('properties.changeDefinition.details.targets[0].httpMethod', 'PATCH'), + ] + self.cmd( + 'az changesafety changestate create -g {rg} -n {name} ' + '--change-type {change_type} --rollout-type {rollout_type} ' + '--targets "{targets}" --comments "Initial deployment"', + checks=create_checks, + ) + + update_checks = [ + JMESPathCheck('properties.rolloutType', 'Emergency'), + JMESPathCheck('properties.comments', 'Escalated rollout'), + ] + self.cmd( + 'az changesafety changestate update -g {rg} -n {name} ' + '--rollout-type {updated_rollout} --comments "Escalated rollout"', + checks=update_checks, + ) + + self.cmd( + 'az changesafety changestate show -g {rg} -n {name}', + checks=[ + JMESPathCheck('properties.comments', 'Escalated rollout'), + JMESPathCheck('properties.changeDefinition.details.targets[0].resourceId', target_resource), + ], + ) -def _dummy_ctx_with_change_definition(payload): - dummy = SimpleNamespace() - dummy.to_serialized_data = lambda: payload - return SimpleNamespace(vars=SimpleNamespace(change_definition=dummy)) + self.cmd('az changesafety changestate delete -g {rg} -n {name} -y') + self.assertNotIn("instance", type(self)._SCENARIO_STATE) From 15783690c08c61ad19f7cd598be0abd97f02cb3d Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Mon, 3 Nov 2025 23:53:21 -0800 Subject: [PATCH 14/15] Fix style --- .../azext_changesafety/custom.py | 158 ++++++++++-------- 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/src/azure-changesafety/azext_changesafety/custom.py b/src/azure-changesafety/azext_changesafety/custom.py index 621aa9bbb1e..320f61fe9a6 100644 --- a/src/azure-changesafety/azext_changesafety/custom.py +++ b/src/azure-changesafety/azext_changesafety/custom.py @@ -486,76 +486,88 @@ class ChangeStateDelete(_ChangeStateDelete): pass -ChangeStateCreate.AZ_HELP["examples"] = [ - { - "name": "Create with StageMap reference and status link", - "text": ( - "az changesafety changestate create -g MyResourceGroup -n changestate002 " - "--change-type ManualTouch --rollout-type Normal " - "--stage-map \"{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/" - "resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rolloutStageMap}\" " - "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" - "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH\" " - "--links \"[{name:status,uri:'https://contoso.com/change/rollout-002'}]\"\n" - "az changesafety changestate delete -g MyResourceGroup -n changestate002 --yes" - ), - }, - { - "name": "Create a change state for a VM rollout", - "text": ( - "az changesafety changestate create -g MyResourceGroup -n changestate001 " - "--change-type AppDeployment --rollout-type Normal " - "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" - "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT\"" - ), - }, - { - "name": "Create with staging rollout configuration", - "text": ( - "az changesafety changestate create -g MyResourceGroup -n opsChange01 " - "--rollout-type Hotfix " - "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" - "resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST\"" - ), - }, -] - -ChangeStateUpdate.AZ_HELP["examples"] = [ - { - "name": "Adjust rollout type and add a comment", - "text": ( - "az changesafety changestate update -g MyResourceGroup -n changestate001 " - "--rollout-type Emergency --comments \"Escalated to emergency rollout\"" - ), - }, - { - "name": "Update scheduling window", - "text": ( - "az changesafety changestate update -g MyResourceGroup -n changestate001 " - "--anticipated-start-time \"2024-09-01T08:00:00Z\" " - "--anticipated-end-time \"2024-09-01T12:00:00Z\"" - ), - }, - { - "name": "Replace the target definition", - "text": ( - "az changesafety changestate update -g MyResourceGroup -n changestate001 " - "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" - "resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH\"" - ), - }, -] - -ChangeStateDelete.AZ_HELP["examples"] = [ - { - "name": "Delete a change state without confirmation", - "text": "az changesafety changestate delete -g MyResourceGroup -n changestate001 --yes", - }, -] - -ChangeStateShow.AZ_HELP["examples"] = [ - { - "name": "Show a change state", - "text": "az changesafety changestate show -g MyResourceGroup -n changestate001", - }, -] +ChangeStateCreate.AZ_HELP = { + **ChangeStateCreate.AZ_HELP, + "examples": [ + { + "name": "Create with StageMap reference and status link", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n changestate002 " + "--change-type ManualTouch --rollout-type Normal " + "--stage-map \"{resource-id:/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.ChangeSafety/stageMaps/rolloutStageMap}\" " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PATCH\" " + "--links \"[{name:status,uri:'https://contoso.com/change/rollout-002'}]\"\n" + "az changesafety changestate delete -g MyResourceGroup -n changestate002 --yes" + ), + }, + { + "name": "Create a change state for a VM rollout", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n changestate001 " + "--change-type AppDeployment --rollout-type Normal " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Compute/virtualMachines/myVm,operation=PUT\"" + ), + }, + { + "name": "Create with staging rollout configuration", + "text": ( + "az changesafety changestate create -g MyResourceGroup -n opsChange01 " + "--rollout-type Hotfix " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Web/sites/myApp,operation=POST\"" + ), + }, + ], +} + +ChangeStateUpdate.AZ_HELP = { + **ChangeStateUpdate.AZ_HELP, + "examples": [ + { + "name": "Adjust rollout type and add a comment", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--rollout-type Emergency --comments \"Escalated to emergency rollout\"" + ), + }, + { + "name": "Update scheduling window", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--anticipated-start-time \"2024-09-01T08:00:00Z\" " + "--anticipated-end-time \"2024-09-01T12:00:00Z\"" + ), + }, + { + "name": "Replace the target definition", + "text": ( + "az changesafety changestate update -g MyResourceGroup -n changestate001 " + "--targets \"resourceId=/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/MyResourceGroup/providers/Microsoft.Sql/servers/myServer,operation=PATCH\"" + ), + }, + ], +} + +ChangeStateDelete.AZ_HELP = { + **ChangeStateDelete.AZ_HELP, + "examples": [ + { + "name": "Delete a change state without confirmation", + "text": "az changesafety changestate delete -g MyResourceGroup -n changestate001 --yes", + }, + ], +} + +ChangeStateShow.AZ_HELP = { + **ChangeStateShow.AZ_HELP, + "examples": [ + { + "name": "Show a change state", + "text": "az changesafety changestate show -g MyResourceGroup -n changestate001", + }, + ], +} From 6c8d92c3a5d54b7953df9229e148807abd99eb26 Mon Sep 17 00:00:00 2001 From: Henry Dai Date: Tue, 4 Nov 2025 09:22:53 -0800 Subject: [PATCH 15/15] Add service name --- src/service_name.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/service_name.json b/src/service_name.json index 24283c09b6f..e489cc04a02 100644 --- a/src/service_name.json +++ b/src/service_name.json @@ -978,5 +978,10 @@ "Command": "az migrate", "AzureServiceName": "Azure Migrate", "URL": "https://learn.microsoft.com/azure/migrate" + }, + { + "Command": "az changesafety", + "AzureServiceName": "ChangeSafety", + "URL": "" } ]