Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,9 @@
supported-profiles: latest
text: >
az vm create -n MyVm -g MyResourceGroup --image Centos --zone 1
- name: Create multiple VMs. In this example, 3 VMs are created. They are MyVm0, MyVm1, MyVm2.
text: >
az vm create -n MyVm -g MyResourceGroup --image centos --count 3
"""

helps['vm deallocate'] = """
Expand Down
2 changes: 2 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ def load_arguments(self, _):
c.argument('enable_hotpatching', arg_type=get_three_state_flag(), help='Patch VMs without requiring a reboot. --enable-agent must be set and --patch-mode must be set to AutomaticByPlatform', min_api='2020-12-01')
c.argument('platform_fault_domain', min_api='2020-06-01',
help='Specify the scale set logical fault domain into which the virtual machine will be created. By default, the virtual machine will be automatically assigned to a fault domain that best maintains balance across available fault domains. This is applicable only if the virtualMachineScaleSet property of this virtual machine is set. The virtual machine scale set that is referenced, must have platform fault domain count. This property cannot be updated once the virtual machine is created. Fault domain assignment can be viewed in the virtual machine instance view')
c.argument('count', type=int, is_preview=True,
help='Number of virtual machines to create. Value range is [2, 250], inclusive. Don\'t specify this parameter if you want to create a normal single VM. The VMs are created in parallel. The output of this command is an array of VMs instead of one single VM. Each VM has its own public IP, NIC. VNET and NSG are shared. It is recommended that no existing public IP, NIC, VNET and NSG are in resource group. When --count is specified, --attach-data-disks, --attach-os-disk, --boot-diagnostics-storage, --computer-name, --host, --host-group, --nics, --os-disk-name, --private-ip-address, --public-ip-address, --public-ip-address-dns-name, --storage-account, --storage-container-name, --subnet, --use-unmanaged-disk, --vnet-name are not allowed.')

with self.argument_context('vm create', arg_group='Storage') as c:
c.argument('attach_os_disk', help='Attach an existing OS disk to the VM. Can use the name or ID of a managed disk or the URI to an unmanaged disk VHD.')
Expand Down
47 changes: 41 additions & 6 deletions src/azure-cli/azure/cli/command_modules/vm/_template_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def build_storage_account_resource(_, name, location, tags, sku):
return storage_account


def build_public_ip_resource(cmd, name, location, tags, address_allocation, dns_name, sku, zone):
def build_public_ip_resource(cmd, name, location, tags, address_allocation, dns_name, sku, zone, count=None):
public_ip_properties = {'publicIPAllocationMethod': address_allocation}

if dns_name:
Expand All @@ -94,6 +94,14 @@ def build_public_ip_resource(cmd, name, location, tags, address_allocation, dns_
'properties': public_ip_properties
}

if count:
public_ip['name'] = "[concat('{}', copyIndex())]".format(name)
public_ip['copy'] = {
'name': 'publicipcopy',
'mode': 'parallel',
'count': count
}

# when multiple zones are provided(through a x-zone scale set), we don't propagate to PIP becasue it doesn't
# support x-zone; rather we will rely on the Standard LB to work with such scale sets
if zone and len(zone) == 1:
Expand All @@ -105,8 +113,8 @@ def build_public_ip_resource(cmd, name, location, tags, address_allocation, dns_


def build_nic_resource(_, name, location, tags, vm_name, subnet_id, private_ip_address=None,
nsg_id=None, public_ip_id=None, application_security_groups=None, accelerated_networking=None):

nsg_id=None, public_ip_id=None, application_security_groups=None, accelerated_networking=None,
count=None):
private_ip_allocation = 'Static' if private_ip_address else 'Dynamic'
ip_config_properties = {
'privateIPAllocationMethod': private_ip_allocation,
Expand All @@ -118,15 +126,20 @@ def build_nic_resource(_, name, location, tags, vm_name, subnet_id, private_ip_a

if public_ip_id:
ip_config_properties['publicIPAddress'] = {'id': public_ip_id}
if count:
ip_config_properties['publicIPAddress']['id'] = "[concat('{}', copyIndex())]".format(public_ip_id)

ipconfig_name = 'ipconfig{}'.format(vm_name)
nic_properties = {
'ipConfigurations': [
{
'name': 'ipconfig{}'.format(vm_name),
'name': ipconfig_name,
'properties': ip_config_properties
}
]
}
if count:
nic_properties['ipConfigurations'][0]['name'] = "[concat('{}', copyIndex())]".format(ipconfig_name)

if nsg_id:
nic_properties['networkSecurityGroup'] = {'id': nsg_id}
Expand All @@ -150,6 +163,15 @@ def build_nic_resource(_, name, location, tags, vm_name, subnet_id, private_ip_a
'dependsOn': [],
'properties': nic_properties
}

if count:
nic['name'] = "[concat('{}', copyIndex())]".format(name)
nic['copy'] = {
'name': 'niccopy',
'mode': 'parallel',
'count': count
}

return nic


Expand Down Expand Up @@ -256,20 +278,26 @@ def build_vm_resource( # pylint: disable=too-many-locals, too-many-statements
computer_name=None, dedicated_host=None, priority=None, max_price=None, eviction_policy=None,
enable_agent=None, vmss=None, os_disk_encryption_set=None, data_disk_encryption_sets=None, specialized=None,
encryption_at_host=None, dedicated_host_group=None, enable_auto_update=None, patch_mode=None,
enable_hotpatching=None, platform_fault_domain=None):
enable_hotpatching=None, platform_fault_domain=None, count=None):

os_caching = disk_info['os'].get('caching')

def _build_os_profile():

special_chars = '`~!@#$%^&*()=+_[]{}\\|;:\'\",<>/?'

# _computer_name is used to avoid shadow names
_computer_name = computer_name or ''.join(filter(lambda x: x not in special_chars, name))

os_profile = {
# Use name as computer_name if it's not provided. Remove special characters from name.
'computerName': computer_name or ''.join(filter(lambda x: x not in special_chars, name)),
'computerName': _computer_name,
'adminUsername': admin_username
}

if count:
os_profile['computerName'] = "[concat('{}', copyIndex())]".format(_computer_name)

if admin_password:
os_profile['adminPassword'] = "[parameters('adminPassword')]"

Expand Down Expand Up @@ -490,6 +518,13 @@ def _build_storage_profile():
}
if zone:
vm['zones'] = zone
if count:
vm['copy'] = {
'name': 'vmcopy',
'mode': 'parallel',
'count': count
}
vm['name'] = "[concat('{}', copyIndex())]".format(name)
return vm


Expand Down
45 changes: 45 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,8 @@ def _resolve_role_id(cli_ctx, role, scope):
def process_vm_create_namespace(cmd, namespace):
validate_tags(namespace)
_validate_location(cmd, namespace, namespace.zone, namespace.size)
if namespace.count is not None:
_validate_count(namespace)
validate_asg_names_or_ids(cmd, namespace)
_validate_vm_create_storage_profile(cmd, namespace)
if namespace.storage_profile in [StorageProfile.SACustomImage,
Expand Down Expand Up @@ -1760,3 +1762,46 @@ def _validate_vmss_create_host_group(cmd, namespace):
subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name,
namespace='Microsoft.Compute', type='hostGroups', name=namespace.host_group
)


def _validate_count(namespace):
if namespace.count < 2 or namespace.count > 250:
raise ValidationError('--count should be in [2, 250]')
banned_params = [
namespace.attach_data_disks,
namespace.attach_os_disk,
namespace.boot_diagnostics_storage,
namespace.computer_name,
namespace.dedicated_host,
namespace.dedicated_host_group,
namespace.nics,
namespace.os_disk_name,
namespace.private_ip_address,
namespace.public_ip_address,
namespace.public_ip_address_dns_name,
namespace.storage_account,
namespace.storage_container_name,
namespace.subnet,
namespace.use_unmanaged_disk,
namespace.vnet_name
]
params_str = [
'--attach-data-disks',
'--attach-os-disk',
'--boot-diagnostics-storage',
'--computer-name',
'--host',
'--host-group',
'--nics',
'--os-disk-name',
'--private-ip-address',
'--public-ip-address',
'--public-ip-address-dns-name',
'--storage-account',
'--storage-container-name',
'--subnet',
'--use-unmanaged-disk',
'--vnet-name'
]
if any(param for param in banned_params):
raise ValidationError('When --count is specified, {} are not allowed'.format(', '.join(params_str)))
10 changes: 10 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_vm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import json
import os
import re

from azure.cli.core.commands.arm import ArmTemplateBuilder

try:
from urllib.parse import urlparse
except ImportError:
Expand Down Expand Up @@ -352,3 +355,10 @@ def _update(info, lun, value):
except ValueError:
raise CLIError("A sku ID is incorrect.\n{}".format(usage_msg))
_update(info_dict, lun, value)


class ArmTemplateBuilder20190401(ArmTemplateBuilder):

def __init__(self):
super().__init__()
self.template['$schema'] = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
66 changes: 48 additions & 18 deletions src/azure-cli/azure/cli/command_modules/vm/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
priority=None, max_price=None, eviction_policy=None, enable_agent=None, workspace=None, vmss=None,
os_disk_encryption_set=None, data_disk_encryption_sets=None, specialized=None,
encryption_at_host=None, enable_auto_update=None, patch_mode=None, ssh_key_name=None,
enable_hotpatching=None, platform_fault_domain=None):
enable_hotpatching=None, platform_fault_domain=None, count=None):
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
from azure.cli.core.commands.arm import ArmTemplateBuilder
Expand All @@ -733,6 +733,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
build_msi_role_assignment,
build_vm_linux_log_analytics_workspace_agent,
build_vm_windows_log_analytics_workspace_agent)
from azure.cli.command_modules.vm._vm_utils import ArmTemplateBuilder20190401
from msrestazure.tools import resource_id, is_valid_resource_id, parse_resource_id

subscription_id = get_subscription_id(cmd.cli_ctx)
Expand Down Expand Up @@ -766,7 +767,10 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
storage_container_name = storage_container_name or 'vhds'

# Build up the ARM template
master_template = ArmTemplateBuilder()
if count is None:
master_template = ArmTemplateBuilder()
else:
master_template = ArmTemplateBuilder20190401()

vm_dependencies = []
if storage_account_type == 'new':
Expand All @@ -779,7 +783,11 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
nic_name = None
if nic_type == 'new':
nic_name = '{}VMNic'.format(vm_name)
vm_dependencies.append('Microsoft.Network/networkInterfaces/{}'.format(nic_name))
nic_full_name = 'Microsoft.Network/networkInterfaces/{}'.format(nic_name)
if count:
vm_dependencies.extend([nic_full_name + str(i) for i in range(count)])
else:
vm_dependencies.append(nic_full_name)

nic_dependencies = []
if vnet_type == 'new':
Expand Down Expand Up @@ -824,12 +832,15 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_

if public_ip_address_type == 'new':
public_ip_address = public_ip_address or '{}PublicIP'.format(vm_name)
nic_dependencies.append('Microsoft.Network/publicIpAddresses/{}'.format(
public_ip_address))
public_ip_address_full_name = 'Microsoft.Network/publicIpAddresses/{}'.format(public_ip_address)
if count:
nic_dependencies.extend([public_ip_address_full_name + str(i) for i in range(count)])
else:
nic_dependencies.append(public_ip_address_full_name)
master_template.add_resource(build_public_ip_resource(cmd, public_ip_address, location, tags,
public_ip_address_allocation,
public_ip_address_dns_name,
public_ip_sku, zone))
public_ip_sku, zone, count))

subnet_id = subnet if is_valid_resource_id(subnet) else \
'{}/virtualNetworks/{}/subnets/{}'.format(network_id_template, vnet_name, subnet)
Expand All @@ -844,12 +855,21 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
public_ip_address_id = public_ip_address if is_valid_resource_id(public_ip_address) \
else '{}/publicIPAddresses/{}'.format(network_id_template, public_ip_address)

nics = [
{'id': '{}/networkInterfaces/{}'.format(network_id_template, nic_name)}
]
nics_id = '{}/networkInterfaces/{}'.format(network_id_template, nic_name)

if count:
nics = [
{'id': "[concat('{}', copyIndex())]".format(nics_id)}
]
else:
nics = [
{'id': nics_id}
]

nic_resource = build_nic_resource(
cmd, nic_name, location, tags, vm_name, subnet_id, private_ip_address, nsg_id,
public_ip_address_id, application_security_groups, accelerated_networking=accelerated_networking)
public_ip_address_id, application_security_groups, accelerated_networking=accelerated_networking,
count=count)
nic_resource['dependsOn'] = nic_dependencies
master_template.add_resource(nic_resource)
else:
Expand Down Expand Up @@ -895,7 +915,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
data_disk_encryption_sets=data_disk_encryption_sets, specialized=specialized,
encryption_at_host=encryption_at_host, dedicated_host_group=dedicated_host_group,
enable_auto_update=enable_auto_update, patch_mode=patch_mode, enable_hotpatching=enable_hotpatching,
platform_fault_domain=platform_fault_domain)
platform_fault_domain=platform_fault_domain, count=count)

vm_resource['dependsOn'] = vm_dependencies

Expand Down Expand Up @@ -969,18 +989,28 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
return sdk_no_wait(no_wait, client.create_or_update, resource_group_name, deployment_name, properties)
LongRunningOperation(cmd.cli_ctx)(client.create_or_update(resource_group_name, deployment_name, properties))

vm = get_vm_details(cmd, resource_group_name, vm_name)
if assign_identity is not None:
if enable_local_identity and not identity_scope:
_show_missing_access_warning(resource_group_name, vm_name, 'vm')
setattr(vm, 'identity', _construct_identity_info(identity_scope, identity_role, vm.identity.principal_id,
vm.identity.user_assigned_identities))
if count:
vm_names = [vm_name + str(i) for i in range(count)]
else:
vm_names = [vm_name]
vms = []
# Use vm_name2 to avoid R1704: Redefining argument with the local name 'vm_name' (redefined-argument-from-local)
for vm_name2 in vm_names:
vm = get_vm_details(cmd, resource_group_name, vm_name2)
if assign_identity is not None:
if enable_local_identity and not identity_scope:
_show_missing_access_warning(resource_group_name, vm_name2, 'vm')
setattr(vm, 'identity', _construct_identity_info(identity_scope, identity_role, vm.identity.principal_id,
vm.identity.user_assigned_identities))
vms.append(vm)

if workspace is not None:
workspace_name = parse_resource_id(workspace_id)['name']
_set_data_source_for_workspace(cmd, os_type, resource_group_name, workspace_name)

return vm
if len(vms) == 1:
return vms[0]
return vms


def auto_shutdown_vm(cmd, resource_group_name, vm_name, off=None, email=None, webhook=None, time=None,
Expand Down
Loading