diff --git a/src/azure-cli/azure/cli/command_modules/vm/_help.py b/src/azure-cli/azure/cli/command_modules/vm/_help.py index fc47438fcf2..d95fc7a8fc6 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/_help.py +++ b/src/azure-cli/azure/cli/command_modules/vm/_help.py @@ -2317,16 +2317,6 @@ --security-posture-reference-exclude-extensions "c:\\tmp\\exclude_extensions.json" """ -helps['vmss deallocate'] = """ -type: command -short-summary: Deallocate VMs within a VMSS. -examples: - - name: Deallocate VMs within a VMSS. (autogenerated) - text: | - az vmss deallocate --instance-ids 1 --name MyScaleSet --resource-group MyResourceGroup - crafted: true -""" - helps['vmss delete-instances'] = """ type: command short-summary: Delete VMs within a VMSS. diff --git a/src/azure-cli/azure/cli/command_modules/vm/_params.py b/src/azure-cli/azure/cli/command_modules/vm/_params.py index a01356314c7..b8cd7ef4580 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/_params.py +++ b/src/azure-cli/azure/cli/command_modules/vm/_params.py @@ -705,14 +705,11 @@ def load_arguments(self, _): c.argument('host_group', min_api='2020-06-01', help='Name or ID of dedicated host group that the virtual machine scale set resides in') - for scope in ['vmss deallocate', 'vmss delete-instances', 'vmss restart', 'vmss start', 'vmss stop', 'vmss show', 'vmss update-instances', 'vmss simulate-eviction']: + for scope in ['vmss delete-instances', 'vmss restart', 'vmss start', 'vmss stop', 'vmss show', 'vmss update-instances', 'vmss simulate-eviction']: with self.argument_context(scope) as c: for dest in scaleset_name_aliases: c.argument(dest, vmss_name_type, id_part=None) # due to instance-ids parameter - with self.argument_context('vmss deallocate', operation_group='virtual_machine_scale_sets') as c: - c.argument('hibernate', arg_type=get_three_state_flag(), help='Hibernate a virtual machine from the VM scale set. Available for VMSS with Flexible OrchestrationMode only.', min_api='2023-03-01') - with self.argument_context('vmss reimage') as c: c.argument('instance_ids', nargs='+', help='Space-separated list of VM instance ID. If missing, reimage all instances.', diff --git a/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/__init__.py b/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/__init__.py index a929ea23832..98404021443 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/__init__.py @@ -9,6 +9,7 @@ # flake8: noqa from .__cmd_group import * +from ._deallocate import * from ._delete import * from ._get_os_upgrade_history import * from ._list import * diff --git a/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/_deallocate.py b/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/_deallocate.py new file mode 100644 index 00000000000..55f8d2d4a7d --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vmss/_deallocate.py @@ -0,0 +1,193 @@ +# -------------------------------------------------------------------------------------------- +# 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( + "vmss deallocate", +) +class Deallocate(AAZCommand): + """Deallocate VMs within a VMSS. + + :example: Deallocate VMs within a VMSS. + az vmss deallocate --instance-ids 1 --name MyScaleSet --resource-group MyResourceGroup + """ + + _aaz_info = { + "version": "2024-11-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.compute/virtualmachinescalesets/{}/deallocate", "2024-11-01"], + ] + } + + AZ_SUPPORT_NO_WAIT = True + + 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.resource_group = AAZResourceGroupNameArg( + required=True, + ) + _args_schema.name = AAZStrArg( + options=["-n", "--name"], + help="Scale set name. You can configure the default using `az configure --defaults vmss=`", + required=True, + id_part="name", + ) + _args_schema.hibernate = AAZBoolArg( + options=["--hibernate"], + help="Hibernate a virtual machine from the VM scale set. Available for VMSS with Flexible OrchestrationMode only. Allowed values:false, true", + ) + + # define Arg Group "VmInstanceIDs" + + _args_schema = cls._args_schema + _args_schema.instance_ids = AAZListArg( + options=["--instance-ids"], + arg_group="VmInstanceIDs", + help="The virtual machine scale set instance ids. Omitting the virtual machine scale set instance ids will result in the operation being performed on all virtual machines in the virtual machine scale set.", + ) + + instance_ids = cls._args_schema.instance_ids + instance_ids.Element = AAZStrArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + yield self.VirtualMachineScaleSetsDeallocate(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + class VirtualMachineScaleSetsDeallocate(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [202]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + if session.http_response.status_code in [200]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200, + 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.Compute/virtualMachineScaleSets/{vmScaleSetName}/deallocate", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "ODataV4Format" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + **self.serialize_url_param( + "vmScaleSetName", self.ctx.args.name, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "hibernate", self.ctx.args.hibernate, + ), + **self.serialize_query_param( + "api-version", "2024-11-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Content-Type", "application/json", + ), + } + return parameters + + @property + def content(self): + _content_value, _builder = self.new_content_builder( + self.ctx.args, + typ=AAZObjectType, + typ_kwargs={"flags": {"client_flatten": True}} + ) + _builder.set_prop("instanceIds", AAZListType, ".instance_ids") + + instance_ids = _builder.get(".instanceIds") + if instance_ids is not None: + instance_ids.set_elements(AAZStrType, ".") + + return self.serialize_content(_content_value) + + def on_200(self, session): + pass + + +class _DeallocateHelper: + """Helper class for Deallocate""" + + +__all__ = ["Deallocate"] diff --git a/src/azure-cli/azure/cli/command_modules/vm/commands.py b/src/azure-cli/azure/cli/command_modules/vm/commands.py index 9525be18d94..344d19d6785 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/commands.py +++ b/src/azure-cli/azure/cli/command_modules/vm/commands.py @@ -421,7 +421,6 @@ def load_command_table(self, _): g.custom_command('application set', 'set_vmss_applications', validator=process_set_applications_namespace, min_api='2021-07-01') g.custom_command('application list', 'list_vmss_applications', min_api='2021-07-01') g.custom_command('create', 'create_vmss', transform=DeploymentOutputLongRunningOperation(self.cli_ctx, 'Starting vmss create'), supports_no_wait=True, table_transformer=deployment_validate_table_format, validator=process_vmss_create_namespace, exception_handler=handle_template_based_exception) - g.custom_command('deallocate', 'deallocate_vmss', supports_no_wait=True) g.custom_command('delete-instances', 'delete_vmss_instances', supports_no_wait=True) g.custom_command('get-instance-view', 'get_vmss_instance_view', table_transformer='{ProvisioningState:statuses[0].displayStatus, PowerState:statuses[1].displayStatus}') g.custom_command('list-instance-connection-info', 'list_vmss_instance_connection_info') diff --git a/src/azure-cli/azure/cli/command_modules/vm/custom.py b/src/azure-cli/azure/cli/command_modules/vm/custom.py index ec1e4854815..4ac00a5ee07 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/custom.py +++ b/src/azure-cli/azure/cli/command_modules/vm/custom.py @@ -3802,24 +3802,6 @@ def _build_identities_info(identities): return (info, identity_types, external_identities, 'SystemAssigned' in identity_types) -def deallocate_vmss(cmd, resource_group_name, vm_scale_set_name, instance_ids=None, no_wait=False, hibernate=None): - client = _compute_client_factory(cmd.cli_ctx) - # This is a walkaround because the REST service of `VirtualMachineScaleSetVMs#begin_deallocate` - # does not accept `hibernate` at present - if instance_ids and len(instance_ids) == 1 and hibernate is None: - return sdk_no_wait(no_wait, client.virtual_machine_scale_set_vms.begin_deallocate, - resource_group_name, vm_scale_set_name, instance_ids[0]) - - VirtualMachineScaleSetVMInstanceIDs = cmd.get_models('VirtualMachineScaleSetVMInstanceIDs') - vm_instance_i_ds = VirtualMachineScaleSetVMInstanceIDs(instance_ids=instance_ids) - if hibernate is not None: - return sdk_no_wait(no_wait, client.virtual_machine_scale_sets.begin_deallocate, - resource_group_name, vm_scale_set_name, vm_instance_i_ds, hibernate=hibernate) - else: - return sdk_no_wait(no_wait, client.virtual_machine_scale_sets.begin_deallocate, - resource_group_name, vm_scale_set_name, vm_instance_i_ds) - - def delete_vmss_instances(cmd, resource_group_name, vm_scale_set_name, instance_ids, no_wait=False): client = _compute_client_factory(cmd.cli_ctx) VirtualMachineScaleSetVMInstanceRequiredIDs = cmd.get_models('VirtualMachineScaleSetVMInstanceRequiredIDs') diff --git a/src/azure-cli/azure/cli/command_modules/vm/operations/vmss.py b/src/azure-cli/azure/cli/command_modules/vm/operations/vmss.py index bd393a750af..a16e6160cf7 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/operations/vmss.py +++ b/src/azure-cli/azure/cli/command_modules/vm/operations/vmss.py @@ -3,16 +3,16 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- # pylint: disable=no-self-use, line-too-long, protected-access, too-few-public-methods, unused-argument +from azure.cli.core.aaz import AAZUndefined, has_value from knack.log import get_logger - from ..aaz.latest.vmss import ListInstances as _VMSSListInstances +from ..aaz.latest.vmss import Deallocate as _VMSSDeallocate logger = get_logger(__name__) class VMSSListInstances(_VMSSListInstances): def _output(self, *args, **kwargs): - from azure.cli.core.aaz import AAZUndefined, has_value # Resolve flatten conflict # When the type field conflicts, the type in inner layer is ignored and the outer layer is applied @@ -25,3 +25,11 @@ def _output(self, *args, **kwargs): result = self.deserialize_output(self.ctx.vars.instance.value, client_flatten=True) next_link = self.deserialize_output(self.ctx.vars.instance.next_link) return result, next_link + + +class VMSSDeallocate(_VMSSDeallocate): + def pre_operations(self): + args = self.ctx.args + + if has_value(args.instance_ids) and len(args.instance_ids) == 1 and not has_value(args.hibernate): + self.ctx.args.instance_ids = args.instance_ids[0] diff --git a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py index 3117838f6c2..3f863e84fb4 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py +++ b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py @@ -5587,17 +5587,49 @@ def _check_vms_power_state(self, *args): self.assertTrue(result['statuses'][1]['code'] in args) @ResourceGroupPreparer(name_prefix='cli_test_vmss_vms') + @AllowLargeResponse(size_kb=99999) def test_vmss_vms(self, resource_group): self.kwargs.update({ 'vmss': self.create_random_name('clitestvmss', 20), 'flex_vmss': self.create_random_name('clitestflexvms', 20), 'count': 2, - 'instance_ids': [] + 'instance_ids': [], + 'nsg': 'nsg1', + 'pubip1': 'pubip1', + }) + + self.cmd('network nsg create -g {rg} -n {nsg}') + # Create a public IP resource with service tag + + # self.cmd("feature register --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress") + # self.cmd("provider register -n Microsoft.Network") + + self.cmd('network public-ip create --name {pubip1} -g {rg} --ip-tags FirstPartyUsage=/NonProd') + + self.cmd('vmss create -g {rg} -n {vmss} --image Canonical:ubuntu-24_04-lts:server:latest --authentication-type password --lb-sku Standard ' + '--admin-username admin123 --admin-password TestTest12#$ --instance-count {count} --orchestration-mode Uniform --public-ip-address {pubip1} --nsg {nsg}') + + + + + # # Create NSG + # self.cmd('network nsg create -g {rg} -n {nsg}') + + # # Add rule to deny high-risk ports (SSH and RDP) + # self.cmd('network nsg rule create -g {rg} --nsg-name {nsg} -n DenyHighRiskPorts --priority 101 ' + # '--access Deny --protocol Tcp --direction Inbound --source-address-prefix Internet ' + # '--source-port-range "*" --destination-address-prefix "*" --destination-port-ranges 22 3389') + + # # Create VMSS with the NSG attached + # self.cmd('vmss create -g {rg} -n {vmss} --image Canonical:ubuntu-24_04-lts:server:latest ' + # '--authentication-type password --lb-sku Standard ' + # '--admin-username admin123 --admin-password TestTest12#$ ' + # '--instance-count {count} --orchestration-mode Uniform --nsg {nsg}') + + - self.cmd('vmss create -g {rg} -n {vmss} --image Canonical:UbuntuServer:18.04-LTS:latest --authentication-type password --lb-sku Standard ' - '--admin-username admin123 --admin-password TestTest12#$ --instance-count {count} --orchestration-mode Uniform') instance_list = self.cmd('vmss list-instances --resource-group {rg} --name {vmss}', checks=[ self.check('type(@)', 'array'),