1# -------------------------------------------------------------------------------------------- 2# Copyright (c) Microsoft Corporation. All rights reserved. 3# Licensed under the MIT License. See License.txt in the project root for license information. 4# -------------------------------------------------------------------------------------------- 5 6# pylint:disable=too-many-lines 7 8import os 9 10try: 11 from urllib.parse import urlparse 12except ImportError: 13 from urlparse import urlparse # pylint: disable=import-error 14 15from knack.log import get_logger 16from knack.util import CLIError 17 18from azure.cli.core.azclierror import ValidationError, ArgumentUsageError 19from azure.cli.core.commands.validators import ( 20 get_default_location_from_resource_group, validate_file_or_dict, validate_parameter_set, validate_tags) 21from azure.cli.core.util import (hash_string, DISALLOWED_USER_NAMES, get_default_admin_username) 22from azure.cli.command_modules.vm._vm_utils import ( 23 check_existence, get_target_network_api, get_storage_blob_uri, list_sku_info) 24from azure.cli.command_modules.vm._template_builder import StorageProfile 25import azure.cli.core.keys as keys 26from azure.core.exceptions import ResourceNotFoundError 27 28from ._client_factory import _compute_client_factory 29from ._actions import _get_latest_image_version 30logger = get_logger(__name__) 31 32 33def validate_asg_names_or_ids(cmd, namespace): 34 from msrestazure.tools import resource_id, is_valid_resource_id 35 from azure.cli.core.profiles import ResourceType 36 from azure.cli.core.commands.client_factory import get_subscription_id 37 ApplicationSecurityGroup = cmd.get_models('ApplicationSecurityGroup', 38 resource_type=ResourceType.MGMT_NETWORK) 39 40 resource_group = namespace.resource_group_name 41 subscription_id = get_subscription_id(cmd.cli_ctx) 42 names_or_ids = getattr(namespace, 'application_security_groups') 43 ids = [] 44 45 if names_or_ids == [""] or not names_or_ids: 46 return 47 48 for val in names_or_ids: 49 if not is_valid_resource_id(val): 50 val = resource_id( 51 subscription=subscription_id, 52 resource_group=resource_group, 53 namespace='Microsoft.Network', type='applicationSecurityGroups', 54 name=val 55 ) 56 ids.append(ApplicationSecurityGroup(id=val)) 57 setattr(namespace, 'application_security_groups', ids) 58 59 60def validate_nsg_name(cmd, namespace): 61 from msrestazure.tools import resource_id 62 from azure.cli.core.commands.client_factory import get_subscription_id 63 vm_id = resource_id(name=namespace.vm_name, resource_group=namespace.resource_group_name, 64 namespace='Microsoft.Compute', type='virtualMachines', 65 subscription=get_subscription_id(cmd.cli_ctx)) 66 namespace.network_security_group_name = namespace.network_security_group_name \ 67 or '{}_NSG_{}'.format(namespace.vm_name, hash_string(vm_id, length=8)) 68 69 70def validate_keyvault(cmd, namespace): 71 namespace.keyvault = _get_resource_id(cmd.cli_ctx, namespace.keyvault, namespace.resource_group_name, 72 'vaults', 'Microsoft.KeyVault') 73 74 75def validate_vm_name_for_monitor_metrics(cmd, namespace): 76 if hasattr(namespace, 'resource'): 77 namespace.resource = _get_resource_id(cmd.cli_ctx, namespace.resource, namespace.resource_group_name, 78 'virtualMachines', 'Microsoft.Compute') 79 elif hasattr(namespace, 'resource_uri'): 80 namespace.resource_uri = _get_resource_id(cmd.cli_ctx, namespace.resource_uri, namespace.resource_group_name, 81 'virtualMachines', 'Microsoft.Compute') 82 del namespace.resource_group_name 83 84 85def _validate_proximity_placement_group(cmd, namespace): 86 from msrestazure.tools import parse_resource_id 87 88 if namespace.proximity_placement_group: 89 namespace.proximity_placement_group = _get_resource_id(cmd.cli_ctx, namespace.proximity_placement_group, 90 namespace.resource_group_name, 91 'proximityPlacementGroups', 'Microsoft.Compute') 92 93 parsed = parse_resource_id(namespace.proximity_placement_group) 94 rg, name = parsed['resource_group'], parsed['name'] 95 96 if not check_existence(cmd.cli_ctx, name, rg, 'Microsoft.Compute', 'proximityPlacementGroups'): 97 raise CLIError("Proximity Placement Group '{}' does not exist.".format(name)) 98 99 100def process_vm_secret_format(cmd, namespace): 101 from msrestazure.tools import is_valid_resource_id 102 from azure.cli.core._output import (get_output_format, set_output_format) 103 104 keyvault_usage = CLIError('usage error: [--keyvault NAME --resource-group NAME | --keyvault ID]') 105 kv = namespace.keyvault 106 rg = namespace.resource_group_name 107 108 if rg: 109 if not kv or is_valid_resource_id(kv): 110 raise keyvault_usage 111 validate_keyvault(cmd, namespace) 112 else: 113 if kv and not is_valid_resource_id(kv): 114 raise keyvault_usage 115 116 warning_msg = "This command does not support the {} output format. Showing JSON format instead." 117 desired_formats = ["json", "jsonc"] 118 119 output_format = get_output_format(cmd.cli_ctx) 120 if output_format not in desired_formats: 121 warning_msg = warning_msg.format(output_format) 122 logger.warning(warning_msg) 123 set_output_format(cmd.cli_ctx, desired_formats[0]) 124 125 126def _get_resource_group_from_vault_name(cli_ctx, vault_name): 127 """ 128 Fetch resource group from vault name 129 :param str vault_name: name of the key vault 130 :return: resource group name or None 131 :rtype: str 132 """ 133 from azure.cli.core.profiles import ResourceType 134 from azure.cli.core.commands.client_factory import get_mgmt_service_client 135 from msrestazure.tools import parse_resource_id 136 client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_KEYVAULT).vaults 137 for vault in client.list(): 138 id_comps = parse_resource_id(vault.id) 139 if id_comps['name'] == vault_name: 140 return id_comps['resource_group'] 141 return None 142 143 144def _get_resource_id(cli_ctx, val, resource_group, resource_type, resource_namespace): 145 from msrestazure.tools import resource_id, is_valid_resource_id 146 from azure.cli.core.commands.client_factory import get_subscription_id 147 if is_valid_resource_id(val): 148 return val 149 150 kwargs = { 151 'name': val, 152 'resource_group': resource_group, 153 'namespace': resource_namespace, 154 'type': resource_type, 155 'subscription': get_subscription_id(cli_ctx) 156 } 157 missing_kwargs = {k: v for k, v in kwargs.items() if not v} 158 159 return resource_id(**kwargs) if not missing_kwargs else None 160 161 162def _get_nic_id(cli_ctx, val, resource_group): 163 return _get_resource_id(cli_ctx, val, resource_group, 164 'networkInterfaces', 'Microsoft.Network') 165 166 167def validate_vm_nic(cmd, namespace): 168 namespace.nic = _get_nic_id(cmd.cli_ctx, namespace.nic, namespace.resource_group_name) 169 170 171def validate_vm_nics(cmd, namespace): 172 rg = namespace.resource_group_name 173 nic_ids = [] 174 175 for n in namespace.nics: 176 nic_ids.append(_get_nic_id(cmd.cli_ctx, n, rg)) 177 namespace.nics = nic_ids 178 179 if hasattr(namespace, 'primary_nic') and namespace.primary_nic: 180 namespace.primary_nic = _get_nic_id(cmd.cli_ctx, namespace.primary_nic, rg) 181 182 183def _validate_secrets(secrets, os_type): 184 """ 185 Validates a parsed JSON array containing secrets for use in VM Creation 186 Secrets JSON structure 187 [{ 188 "sourceVault": { "id": "value" }, 189 "vaultCertificates": [{ 190 "certificateUrl": "value", 191 "certificateStore": "cert store name (only on windows)" 192 }] 193 }] 194 :param dict secrets: Dict fitting the JSON description above 195 :param string os_type: the type of OS (linux or windows) 196 :return: errors if any were found 197 :rtype: list 198 """ 199 is_windows = os_type == 'windows' 200 errors = [] 201 202 try: 203 loaded_secret = [validate_file_or_dict(secret) for secret in secrets] 204 except Exception as err: 205 raise CLIError('Error decoding secrets: {0}'.format(err)) 206 207 for idx_arg, narg_secret in enumerate(loaded_secret): 208 for idx, secret in enumerate(narg_secret): 209 if 'sourceVault' not in secret: 210 errors.append( 211 'Secret is missing sourceVault key at index {0} in arg {1}'.format( 212 idx, idx_arg)) 213 if 'sourceVault' in secret and 'id' not in secret['sourceVault']: 214 errors.append( 215 'Secret is missing sourceVault.id key at index {0} in arg {1}'.format( 216 idx, idx_arg)) 217 if 'vaultCertificates' not in secret or not secret['vaultCertificates']: 218 err = 'Secret is missing vaultCertificates array or it is empty at index {0} in ' \ 219 'arg {1} ' 220 errors.append(err.format(idx, idx_arg)) 221 else: 222 for jdx, cert in enumerate(secret['vaultCertificates']): 223 message = 'Secret is missing {0} within vaultCertificates array at secret ' \ 224 'index {1} and vaultCertificate index {2} in arg {3}' 225 if 'certificateUrl' not in cert: 226 errors.append(message.format('certificateUrl', idx, jdx, idx_arg)) 227 if is_windows and 'certificateStore' not in cert: 228 errors.append(message.format('certificateStore', idx, jdx, idx_arg)) 229 230 if errors: 231 raise CLIError('\n'.join(errors)) 232 233 234# region VM Create Validators 235 236 237def _parse_image_argument(cmd, namespace): 238 """ Systematically determines what type is supplied for the --image parameter. Updates the 239 namespace and returns the type for subsequent processing. """ 240 from msrestazure.tools import is_valid_resource_id 241 from msrestazure.azure_exceptions import CloudError 242 import re 243 244 # 1 - check if a fully-qualified ID (assumes it is an image ID) 245 if is_valid_resource_id(namespace.image): 246 return 'image_id' 247 248 from ._vm_utils import is_shared_gallery_image_id 249 if is_shared_gallery_image_id(namespace.image): 250 return 'shared_gallery_image_id' 251 252 # 2 - attempt to match an URN pattern 253 urn_match = re.match('([^:]*):([^:]*):([^:]*):([^:]*)', namespace.image) 254 if urn_match: 255 namespace.os_publisher = urn_match.group(1) 256 namespace.os_offer = urn_match.group(2) 257 namespace.os_sku = urn_match.group(3) 258 namespace.os_version = urn_match.group(4) 259 260 if not any([namespace.plan_name, namespace.plan_product, namespace.plan_publisher]): 261 image_plan = _get_image_plan_info_if_exists(cmd, namespace) 262 if image_plan: 263 namespace.plan_name = image_plan.name 264 namespace.plan_product = image_plan.product 265 namespace.plan_publisher = image_plan.publisher 266 267 return 'urn' 268 269 # 3 - unmanaged vhd based images? 270 if urlparse(namespace.image).scheme and "://" in namespace.image: 271 return 'uri' 272 273 # 4 - attempt to match an URN alias (most likely) 274 from azure.cli.command_modules.vm._actions import load_images_from_aliases_doc 275 import requests 276 try: 277 images = None 278 images = load_images_from_aliases_doc(cmd.cli_ctx) 279 matched = next((x for x in images if x['urnAlias'].lower() == namespace.image.lower()), None) 280 if matched: 281 namespace.os_publisher = matched['publisher'] 282 namespace.os_offer = matched['offer'] 283 namespace.os_sku = matched['sku'] 284 namespace.os_version = matched['version'] 285 return 'urn' 286 except requests.exceptions.ConnectionError: 287 pass 288 289 # 5 - check if an existing managed disk image resource 290 compute_client = _compute_client_factory(cmd.cli_ctx) 291 try: 292 compute_client.images.get(namespace.resource_group_name, namespace.image) 293 namespace.image = _get_resource_id(cmd.cli_ctx, namespace.image, namespace.resource_group_name, 294 'images', 'Microsoft.Compute') 295 return 'image_id' 296 except CloudError: 297 if images is not None: 298 err = 'Invalid image "{}". Use a valid image URN, custom image name, custom image id, ' \ 299 'VHD blob URI, or pick an image from {}.\nSee vm create -h for more information ' \ 300 'on specifying an image.'.format(namespace.image, [x['urnAlias'] for x in images]) 301 else: 302 err = 'Failed to connect to remote source of image aliases or find a local copy. Invalid image "{}". ' \ 303 'Use a valid image URN, custom image name, custom image id, or VHD blob URI.\nSee vm ' \ 304 'create -h for more information on specifying an image.'.format(namespace.image) 305 raise CLIError(err) 306 307 308def _get_image_plan_info_if_exists(cmd, namespace): 309 from msrestazure.azure_exceptions import CloudError 310 try: 311 compute_client = _compute_client_factory(cmd.cli_ctx) 312 if namespace.os_version.lower() == 'latest': 313 image_version = _get_latest_image_version(cmd.cli_ctx, namespace.location, namespace.os_publisher, 314 namespace.os_offer, namespace.os_sku) 315 else: 316 image_version = namespace.os_version 317 318 image = compute_client.virtual_machine_images.get(namespace.location, 319 namespace.os_publisher, 320 namespace.os_offer, 321 namespace.os_sku, 322 image_version) 323 324 # pylint: disable=no-member 325 return image.plan 326 except CloudError as ex: 327 logger.warning("Querying the image of '%s' failed for an error '%s'. Configuring plan settings " 328 "will be skipped", namespace.image, ex.message) 329 330 331# pylint: disable=inconsistent-return-statements, too-many-return-statements 332def _get_storage_profile_description(profile): 333 if profile == StorageProfile.SACustomImage: 334 return 'create unmanaged OS disk created from generalized VHD' 335 if profile == StorageProfile.SAPirImage: 336 return 'create unmanaged OS disk from Azure Marketplace image' 337 if profile == StorageProfile.SASpecializedOSDisk: 338 return 'attach to existing unmanaged OS disk' 339 if profile == StorageProfile.ManagedCustomImage: 340 return 'create managed OS disk from custom image' 341 if profile == StorageProfile.ManagedPirImage: 342 return 'create managed OS disk from Azure Marketplace image' 343 if profile == StorageProfile.ManagedSpecializedOSDisk: 344 return 'attach existing managed OS disk' 345 if profile == StorageProfile.SharedGalleryImage: 346 return 'create OS disk from shared gallery image' 347 348 349def _validate_location(cmd, namespace, zone_info, size_info): 350 if not namespace.location: 351 get_default_location_from_resource_group(cmd, namespace) 352 if zone_info: 353 sku_infos = list_sku_info(cmd.cli_ctx, namespace.location) 354 temp = next((x for x in sku_infos if x.name.lower() == size_info.lower()), None) 355 # For Stack (compute - 2017-03-30), Resource_sku doesn't implement location_info property 356 if not hasattr(temp, 'location_info'): 357 return 358 if not temp or not [x for x in (temp.location_info or []) if x.zones]: 359 raise CLIError("{}'s location can't be used to create the VM/VMSS because availability zone is not yet " 360 "supported. Please use '--location' to specify a capable one. 'az vm list-skus' can be " 361 "used to find such locations".format(namespace.resource_group_name)) 362 363 364# pylint: disable=too-many-branches, too-many-statements 365def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False): 366 from msrestazure.tools import parse_resource_id 367 368 # specialized is only for image 369 if getattr(namespace, 'specialized', None) is not None and namespace.image is None: 370 raise CLIError('usage error: --specialized is only configurable when --image is specified.') 371 372 # use minimal parameters to resolve the expected storage profile 373 if getattr(namespace, 'attach_os_disk', None) and not namespace.image: 374 if namespace.use_unmanaged_disk: 375 # STORAGE PROFILE #3 376 namespace.storage_profile = StorageProfile.SASpecializedOSDisk 377 else: 378 # STORAGE PROFILE #6 379 namespace.storage_profile = StorageProfile.ManagedSpecializedOSDisk 380 elif namespace.image and not getattr(namespace, 'attach_os_disk', None): 381 image_type = _parse_image_argument(cmd, namespace) 382 if image_type == 'uri': 383 # STORAGE PROFILE #2 384 namespace.storage_profile = StorageProfile.SACustomImage 385 elif image_type == 'image_id': 386 # STORAGE PROFILE #5 387 namespace.storage_profile = StorageProfile.ManagedCustomImage 388 elif image_type == 'shared_gallery_image_id': 389 namespace.storage_profile = StorageProfile.SharedGalleryImage 390 elif image_type == 'urn': 391 if namespace.use_unmanaged_disk: 392 # STORAGE PROFILE #1 393 namespace.storage_profile = StorageProfile.SAPirImage 394 else: 395 # STORAGE PROFILE #4 396 namespace.storage_profile = StorageProfile.ManagedPirImage 397 else: 398 raise CLIError('Unrecognized image type: {}'.format(image_type)) 399 else: 400 # did not specify image XOR attach-os-disk 401 raise CLIError('incorrect usage: --image IMAGE | --attach-os-disk DISK') 402 403 auth_params = ['admin_password', 'admin_username', 'authentication_type', 404 'generate_ssh_keys', 'ssh_dest_key_path', 'ssh_key_value'] 405 406 # perform parameter validation for the specific storage profile 407 # start with the required/forbidden parameters for VM 408 if namespace.storage_profile == StorageProfile.ManagedPirImage: 409 required = ['image'] 410 forbidden = ['os_type', 'attach_os_disk', 'storage_account', 411 'storage_container_name', 'use_unmanaged_disk'] 412 if for_scale_set: 413 forbidden.append('os_disk_name') 414 415 elif namespace.storage_profile == StorageProfile.ManagedCustomImage: 416 required = ['image'] 417 forbidden = ['os_type', 'attach_os_disk', 'storage_account', 418 'storage_container_name', 'use_unmanaged_disk'] 419 if for_scale_set: 420 forbidden.append('os_disk_name') 421 422 elif namespace.storage_profile == StorageProfile.SharedGalleryImage: 423 required = ['image'] 424 forbidden = ['attach_os_disk', 'storage_account', 'storage_container_name', 'use_unmanaged_disk'] 425 426 elif namespace.storage_profile == StorageProfile.ManagedSpecializedOSDisk: 427 required = ['os_type', 'attach_os_disk'] 428 forbidden = ['os_disk_name', 'os_caching', 'storage_account', 'ephemeral_os_disk', 429 'storage_container_name', 'use_unmanaged_disk', 'storage_sku'] + auth_params 430 431 elif namespace.storage_profile == StorageProfile.SAPirImage: 432 required = ['image', 'use_unmanaged_disk'] 433 forbidden = ['os_type', 'attach_os_disk', 'data_disk_sizes_gb', 'ephemeral_os_disk'] 434 435 elif namespace.storage_profile == StorageProfile.SACustomImage: 436 required = ['image', 'os_type', 'use_unmanaged_disk'] 437 forbidden = ['attach_os_disk', 'data_disk_sizes_gb', 'ephemeral_os_disk'] 438 439 elif namespace.storage_profile == StorageProfile.SASpecializedOSDisk: 440 required = ['os_type', 'attach_os_disk', 'use_unmanaged_disk'] 441 forbidden = ['os_disk_name', 'os_caching', 'image', 'storage_account', 'ephemeral_os_disk', 442 'storage_container_name', 'data_disk_sizes_gb', 'storage_sku'] + auth_params 443 444 else: 445 raise CLIError('Unrecognized storage profile: {}'.format(namespace.storage_profile)) 446 447 logger.debug("storage profile '%s'", namespace.storage_profile) 448 449 if for_scale_set: 450 # VMSS lacks some parameters, so scrub these out 451 props_to_remove = ['attach_os_disk', 'storage_account'] 452 for prop in props_to_remove: 453 if prop in required: 454 required.remove(prop) 455 if prop in forbidden: 456 forbidden.remove(prop) 457 458 # set default storage SKU if not provided and using an image based OS 459 if not namespace.storage_sku and namespace.storage_profile in [StorageProfile.SAPirImage, StorageProfile.SACustomImage]: # pylint: disable=line-too-long 460 namespace.storage_sku = ['Standard_LRS'] if for_scale_set else ['Premium_LRS'] 461 462 if namespace.ultra_ssd_enabled is None and namespace.storage_sku: 463 for sku in namespace.storage_sku: 464 if 'ultrassd_lrs' in sku.lower(): 465 namespace.ultra_ssd_enabled = True 466 467 # Now verify the presence of required and absence of forbidden parameters 468 validate_parameter_set( 469 namespace, required, forbidden, 470 description='storage profile: {}:'.format(_get_storage_profile_description(namespace.storage_profile))) 471 472 image_data_disks = [] 473 if namespace.storage_profile == StorageProfile.ManagedCustomImage: 474 # extract additional information from a managed custom image 475 res = parse_resource_id(namespace.image) 476 namespace.aux_subscriptions = [res['subscription']] 477 compute_client = _compute_client_factory(cmd.cli_ctx, subscription_id=res['subscription']) 478 if res['type'].lower() == 'images': 479 image_info = compute_client.images.get(res['resource_group'], res['name']) 480 namespace.os_type = image_info.storage_profile.os_disk.os_type 481 image_data_disks = image_info.storage_profile.data_disks or [] 482 image_data_disks = [{'lun': disk.lun} for disk in image_data_disks] 483 484 elif res['type'].lower() == 'galleries': 485 image_info = compute_client.gallery_images.get(resource_group_name=res['resource_group'], 486 gallery_name=res['name'], 487 gallery_image_name=res['child_name_1']) 488 namespace.os_type = image_info.os_type 489 gallery_image_version = res.get('child_name_2', '') 490 if gallery_image_version.lower() in ['latest', '']: 491 image_version_infos = compute_client.gallery_image_versions.list_by_gallery_image( 492 resource_group_name=res['resource_group'], gallery_name=res['name'], 493 gallery_image_name=res['child_name_1']) 494 image_version_infos = [x for x in image_version_infos if not x.publishing_profile.exclude_from_latest] 495 if not image_version_infos: 496 raise CLIError('There is no latest image version exists for "{}"'.format(namespace.image)) 497 image_version_info = sorted(image_version_infos, key=lambda x: x.publishing_profile.published_date)[-1] 498 else: 499 image_version_info = compute_client.gallery_image_versions.get( 500 resource_group_name=res['resource_group'], gallery_name=res['name'], 501 gallery_image_name=res['child_name_1'], gallery_image_version_name=res['child_name_2']) 502 image_data_disks = image_version_info.storage_profile.data_disk_images or [] 503 image_data_disks = [{'lun': disk.lun} for disk in image_data_disks] 504 505 else: 506 raise CLIError('usage error: unrecognized image information "{}"'.format(namespace.image)) 507 508 # pylint: disable=no-member 509 510 elif namespace.storage_profile == StorageProfile.ManagedSpecializedOSDisk: 511 # accept disk name or ID 512 namespace.attach_os_disk = _get_resource_id( 513 cmd.cli_ctx, namespace.attach_os_disk, namespace.resource_group_name, 'disks', 'Microsoft.Compute') 514 515 if getattr(namespace, 'attach_data_disks', None): 516 if not namespace.use_unmanaged_disk: 517 namespace.attach_data_disks = [_get_resource_id(cmd.cli_ctx, d, namespace.resource_group_name, 'disks', 518 'Microsoft.Compute') for d in namespace.attach_data_disks] 519 520 if not namespace.os_type: 521 if namespace.storage_profile == StorageProfile.SharedGalleryImage: 522 523 if namespace.location is None: 524 from azure.cli.core.azclierror import RequiredArgumentMissingError 525 raise RequiredArgumentMissingError( 526 'Please input the location of the shared gallery image through the parameter --location.') 527 528 from ._vm_utils import parse_shared_gallery_image_id 529 image_info = parse_shared_gallery_image_id(namespace.image) 530 531 from ._client_factory import cf_shared_gallery_image 532 shared_gallery_image_info = cf_shared_gallery_image(cmd.cli_ctx).get( 533 location=namespace.location, gallery_unique_name=image_info[0], gallery_image_name=image_info[1]) 534 namespace.os_type = shared_gallery_image_info.os_type 535 536 else: 537 namespace.os_type = 'windows' if 'windows' in namespace.os_offer.lower() else 'linux' 538 539 from ._vm_utils import normalize_disk_info 540 # attach_data_disks are not exposed yet for VMSS, so use 'getattr' to avoid crash 541 vm_size = (getattr(namespace, 'size', None) or getattr(namespace, 'vm_sku', None)) 542 543 namespace.disk_info = normalize_disk_info(size=vm_size, 544 image_data_disks=image_data_disks, 545 data_disk_sizes_gb=namespace.data_disk_sizes_gb, 546 attach_data_disks=getattr(namespace, 'attach_data_disks', []), 547 storage_sku=namespace.storage_sku, 548 os_disk_caching=namespace.os_caching, 549 data_disk_cachings=namespace.data_caching, 550 ephemeral_os_disk=getattr(namespace, 'ephemeral_os_disk', None), 551 data_disk_delete_option=getattr( 552 namespace, 'data_disk_delete_option', None)) 553 554 555def _validate_vm_create_storage_account(cmd, namespace): 556 from msrestazure.tools import parse_resource_id 557 if namespace.storage_account: 558 storage_id = parse_resource_id(namespace.storage_account) 559 rg = storage_id.get('resource_group', namespace.resource_group_name) 560 if check_existence(cmd.cli_ctx, storage_id['name'], rg, 'Microsoft.Storage', 'storageAccounts'): 561 # 1 - existing storage account specified 562 namespace.storage_account_type = 'existing' 563 logger.debug("using specified existing storage account '%s'", storage_id['name']) 564 else: 565 # 2 - params for new storage account specified 566 namespace.storage_account_type = 'new' 567 logger.debug("specified storage account '%s' not found and will be created", storage_id['name']) 568 else: 569 from azure.cli.core.profiles import ResourceType 570 from azure.cli.core.commands.client_factory import get_mgmt_service_client 571 storage_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_STORAGE).storage_accounts 572 573 # find storage account in target resource group that matches the VM's location 574 sku_tier = 'Standard' 575 for sku in namespace.storage_sku: 576 if 'Premium' in sku: 577 sku_tier = 'Premium' 578 break 579 580 account = next( 581 (a for a in storage_client.list_by_resource_group(namespace.resource_group_name) 582 if a.sku.tier == sku_tier and a.location == namespace.location), None) 583 584 if account: 585 # 3 - nothing specified - find viable storage account in target resource group 586 namespace.storage_account = account.name 587 namespace.storage_account_type = 'existing' 588 logger.debug("suitable existing storage account '%s' will be used", account.name) 589 else: 590 # 4 - nothing specified - create a new storage account 591 namespace.storage_account_type = 'new' 592 logger.debug('no suitable storage account found. One will be created.') 593 594 595def _validate_vm_create_availability_set(cmd, namespace): 596 from msrestazure.tools import parse_resource_id, resource_id 597 from azure.cli.core.commands.client_factory import get_subscription_id 598 if namespace.availability_set: 599 as_id = parse_resource_id(namespace.availability_set) 600 name = as_id['name'] 601 rg = as_id.get('resource_group', namespace.resource_group_name) 602 603 if not check_existence(cmd.cli_ctx, name, rg, 'Microsoft.Compute', 'availabilitySets'): 604 raise CLIError("Availability set '{}' does not exist.".format(name)) 605 606 namespace.availability_set = resource_id( 607 subscription=get_subscription_id(cmd.cli_ctx), 608 resource_group=rg, 609 namespace='Microsoft.Compute', 610 type='availabilitySets', 611 name=name) 612 logger.debug("adding to specified availability set '%s'", namespace.availability_set) 613 614 615def _validate_vm_create_vmss(cmd, namespace): 616 from msrestazure.tools import parse_resource_id, resource_id 617 from azure.cli.core.commands.client_factory import get_subscription_id 618 if namespace.vmss: 619 as_id = parse_resource_id(namespace.vmss) 620 name = as_id['name'] 621 rg = as_id.get('resource_group', namespace.resource_group_name) 622 623 if not check_existence(cmd.cli_ctx, name, rg, 'Microsoft.Compute', 'virtualMachineScaleSets'): 624 raise CLIError("virtual machine scale set '{}' does not exist.".format(name)) 625 626 namespace.vmss = resource_id( 627 subscription=get_subscription_id(cmd.cli_ctx), 628 resource_group=rg, 629 namespace='Microsoft.Compute', 630 type='virtualMachineScaleSets', 631 name=name) 632 logger.debug("adding to specified virtual machine scale set '%s'", namespace.vmss) 633 634 635def _validate_vm_create_dedicated_host(cmd, namespace): 636 """ 637 "host": { 638 "$ref": "#/definitions/SubResource", 639 "description": "Specifies information about the dedicated host that the virtual machine resides in. 640 <br><br>Minimum api-version: 2018-10-01." 641 }, 642 "hostGroup": { 643 "$ref": "#/definitions/SubResource", 644 "description": "Specifies information about the dedicated host group that the virtual machine resides in. 645 <br><br>Minimum api-version: 2020-06-01. <br><br>NOTE: User cannot specify both host and hostGroup properties." 646 } 647 648 :param cmd: 649 :param namespace: 650 :return: 651 """ 652 from msrestazure.tools import resource_id, is_valid_resource_id 653 from azure.cli.core.commands.client_factory import get_subscription_id 654 655 if namespace.dedicated_host and namespace.dedicated_host_group: 656 raise CLIError('usage error: User cannot specify both --host and --host-group properties.') 657 658 if namespace.dedicated_host and not is_valid_resource_id(namespace.dedicated_host): 659 raise CLIError('usage error: --host is not a valid resource ID.') 660 661 if namespace.dedicated_host_group: 662 if not is_valid_resource_id(namespace.dedicated_host_group): 663 namespace.dedicated_host_group = resource_id( 664 subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name, 665 namespace='Microsoft.Compute', type='hostGroups', name=namespace.dedicated_host_group 666 ) 667 668 669def _validate_vm_vmss_create_vnet(cmd, namespace, for_scale_set=False): 670 from msrestazure.tools import is_valid_resource_id 671 vnet = namespace.vnet_name 672 subnet = namespace.subnet 673 rg = namespace.resource_group_name 674 location = namespace.location 675 nics = getattr(namespace, 'nics', None) 676 677 if vnet and '/' in vnet: 678 raise CLIError("incorrect usage: --subnet ID | --subnet NAME --vnet-name NAME") 679 680 if not vnet and not subnet and not nics: 681 logger.debug('no subnet specified. Attempting to find an existing Vnet and subnet...') 682 683 # if nothing specified, try to find an existing vnet and subnet in the target resource group 684 client = get_network_client(cmd.cli_ctx).virtual_networks 685 686 # find VNET in target resource group that matches the VM's location with a matching subnet 687 for vnet_match in (v for v in client.list(rg) if v.location == location and v.subnets): 688 689 # 1 - find a suitable existing vnet/subnet 690 result = None 691 if not for_scale_set: 692 result = next((s for s in vnet_match.subnets if s.name.lower() != 'gatewaysubnet'), None) 693 else: 694 def _check_subnet(s): 695 if s.name.lower() == 'gatewaysubnet': 696 return False 697 subnet_mask = s.address_prefix.split('/')[-1] 698 return _subnet_capacity_check(subnet_mask, namespace.instance_count, 699 not namespace.disable_overprovision) 700 701 result = next((s for s in vnet_match.subnets if _check_subnet(s)), None) 702 if not result: 703 continue 704 namespace.subnet = result.name 705 namespace.vnet_name = vnet_match.name 706 namespace.vnet_type = 'existing' 707 logger.debug("existing vnet '%s' and subnet '%s' found", namespace.vnet_name, namespace.subnet) 708 return 709 710 if subnet: 711 subnet_is_id = is_valid_resource_id(subnet) 712 if (subnet_is_id and vnet) or (not subnet_is_id and not vnet): 713 raise CLIError("incorrect usage: --subnet ID | --subnet NAME --vnet-name NAME") 714 715 subnet_exists = \ 716 check_existence(cmd.cli_ctx, subnet, rg, 'Microsoft.Network', 'subnets', vnet, 'virtualNetworks') 717 718 if subnet_is_id and not subnet_exists: 719 raise CLIError("Subnet '{}' does not exist.".format(subnet)) 720 if subnet_exists: 721 # 2 - user specified existing vnet/subnet 722 namespace.vnet_type = 'existing' 723 logger.debug("using specified vnet '%s' and subnet '%s'", namespace.vnet_name, namespace.subnet) 724 return 725 # 3 - create a new vnet/subnet 726 namespace.vnet_type = 'new' 727 logger.debug('no suitable subnet found. One will be created.') 728 729 730def _subnet_capacity_check(subnet_mask, vmss_instance_count, over_provision): 731 mask = int(subnet_mask) 732 # '2' are the reserved broadcasting addresses 733 # '*1.5' so we have enough leeway for over-provision 734 factor = 1.5 if over_provision else 1 735 return ((1 << (32 - mask)) - 2) > int(vmss_instance_count * factor) 736 737 738def _validate_vm_vmss_accelerated_networking(cli_ctx, namespace): 739 if namespace.accelerated_networking is None: 740 size = getattr(namespace, 'size', None) or getattr(namespace, 'vm_sku', None) 741 size = size.lower() 742 743 # Use the following code to refresh the list 744 # skus = list_sku_info(cli_ctx, namespace.location) 745 # aval_sizes = [x.name.lower() for x in skus if x.resource_type == 'virtualMachines' and 746 # any(c.name == 'AcceleratedNetworkingEnabled' and c.value == 'True' for c in x.capabilities)] 747 748 aval_sizes = ['standard_b12ms', 'standard_b16ms', 'standard_b20ms', 'standard_ds2_v2', 'standard_ds3_v2', 749 'standard_ds4_v2', 'standard_ds5_v2', 'standard_ds11-1_v2', 'standard_ds11_v2', 750 'standard_ds12-1_v2', 'standard_ds12-2_v2', 'standard_ds12_v2', 'standard_ds13-2_v2', 751 'standard_ds13-4_v2', 'standard_ds13_v2', 'standard_ds14-4_v2', 'standard_ds14-8_v2', 752 'standard_ds14_v2', 'standard_ds15_v2', 'standard_ds2_v2_promo', 'standard_ds3_v2_promo', 753 'standard_ds4_v2_promo', 'standard_ds5_v2_promo', 'standard_ds11_v2_promo', 754 'standard_ds12_v2_promo', 'standard_ds13_v2_promo', 'standard_ds14_v2_promo', 'standard_f2s', 755 'standard_f4s', 'standard_f8s', 'standard_f16s', 'standard_d4s_v3', 'standard_d8s_v3', 756 'standard_d16s_v3', 'standard_d32s_v3', 'standard_d2_v2', 'standard_d3_v2', 'standard_d4_v2', 757 'standard_d5_v2', 'standard_d11_v2', 'standard_d12_v2', 'standard_d13_v2', 'standard_d14_v2', 758 'standard_d15_v2', 'standard_d2_v2_promo', 'standard_d3_v2_promo', 'standard_d4_v2_promo', 759 'standard_d5_v2_promo', 'standard_d11_v2_promo', 'standard_d12_v2_promo', 'standard_d13_v2_promo', 760 'standard_d14_v2_promo', 'standard_f2', 'standard_f4', 'standard_f8', 'standard_f16', 761 'standard_d4_v3', 'standard_d8_v3', 'standard_d16_v3', 'standard_d32_v3', 'standard_d48_v3', 762 'standard_d64_v3', 'standard_d48s_v3', 'standard_d64s_v3', 'standard_e4_v3', 'standard_e8_v3', 763 'standard_e16_v3', 'standard_e20_v3', 'standard_e32_v3', 'standard_e48_v3', 'standard_e64i_v3', 764 'standard_e64_v3', 'standard_e4-2s_v3', 'standard_e4s_v3', 'standard_e8-2s_v3', 765 'standard_e8-4s_v3', 'standard_e8s_v3', 'standard_e16-4s_v3', 'standard_e16-8s_v3', 766 'standard_e16s_v3', 'standard_e20s_v3', 'standard_e32-8s_v3', 'standard_e32-16s_v3', 767 'standard_e32s_v3', 'standard_e48s_v3', 'standard_e64-16s_v3', 'standard_e64-32s_v3', 768 'standard_e64is_v3', 'standard_e64s_v3', 'standard_l8s_v2', 'standard_l16s_v2', 769 'standard_l32s_v2', 'standard_l48s_v2', 'standard_l64s_v2', 'standard_l80s_v2', 'standard_e4_v4', 770 'standard_e8_v4', 'standard_e16_v4', 'standard_e20_v4', 'standard_e32_v4', 'standard_e48_v4', 771 'standard_e64_v4', 'standard_e4d_v4', 'standard_e8d_v4', 'standard_e16d_v4', 'standard_e20d_v4', 772 'standard_e32d_v4', 'standard_e48d_v4', 'standard_e64d_v4', 'standard_e4-2s_v4', 773 'standard_e4s_v4', 'standard_e8-2s_v4', 'standard_e8-4s_v4', 'standard_e8s_v4', 774 'standard_e16-4s_v4', 'standard_e16-8s_v4', 'standard_e16s_v4', 'standard_e20s_v4', 775 'standard_e32-8s_v4', 'standard_e32-16s_v4', 'standard_e32s_v4', 'standard_e48s_v4', 776 'standard_e64-16s_v4', 'standard_e64-32s_v4', 'standard_e64s_v4', 'standard_e4-2ds_v4', 777 'standard_e4ds_v4', 'standard_e8-2ds_v4', 'standard_e8-4ds_v4', 'standard_e8ds_v4', 778 'standard_e16-4ds_v4', 'standard_e16-8ds_v4', 'standard_e16ds_v4', 'standard_e20ds_v4', 779 'standard_e32-8ds_v4', 'standard_e32-16ds_v4', 'standard_e32ds_v4', 'standard_e48ds_v4', 780 'standard_e64-16ds_v4', 'standard_e64-32ds_v4', 'standard_e64ds_v4', 'standard_d4d_v4', 781 'standard_d8d_v4', 'standard_d16d_v4', 'standard_d32d_v4', 'standard_d48d_v4', 'standard_d64d_v4', 782 'standard_d4_v4', 'standard_d8_v4', 'standard_d16_v4', 'standard_d32_v4', 'standard_d48_v4', 783 'standard_d64_v4', 'standard_d4ds_v4', 'standard_d8ds_v4', 'standard_d16ds_v4', 784 'standard_d32ds_v4', 'standard_d48ds_v4', 'standard_d64ds_v4', 'standard_d4s_v4', 785 'standard_d8s_v4', 'standard_d16s_v4', 'standard_d32s_v4', 'standard_d48s_v4', 'standard_d64s_v4', 786 'standard_f4s_v2', 'standard_f8s_v2', 'standard_f16s_v2', 'standard_f32s_v2', 'standard_f48s_v2', 787 'standard_f64s_v2', 'standard_f72s_v2', 'standard_m208ms_v2', 'standard_m208s_v2', 788 'standard_m416-208s_v2', 'standard_m416s_v2', 'standard_m416-208ms_v2', 'standard_m416ms_v2', 789 'standard_m64', 'standard_m64m', 'standard_m128', 'standard_m128m', 'standard_m8-2ms', 790 'standard_m8-4ms', 'standard_m8ms', 'standard_m16-4ms', 'standard_m16-8ms', 'standard_m16ms', 791 'standard_m32-8ms', 'standard_m32-16ms', 'standard_m32ls', 'standard_m32ms', 'standard_m32ts', 792 'standard_m64-16ms', 'standard_m64-32ms', 'standard_m64ls', 'standard_m64ms', 'standard_m64s', 793 'standard_m128-32ms', 'standard_m128-64ms', 'standard_m128ms', 'standard_m128s', 794 'standard_d4a_v4', 'standard_d8a_v4', 'standard_d16a_v4', 'standard_d32a_v4', 'standard_d48a_v4', 795 'standard_d64a_v4', 'standard_d96a_v4', 'standard_d4as_v4', 'standard_d8as_v4', 796 'standard_d16as_v4', 'standard_d32as_v4', 'standard_d48as_v4', 'standard_d64as_v4', 797 'standard_d96as_v4', 'standard_e4a_v4', 'standard_e8a_v4', 'standard_e16a_v4', 'standard_e20a_v4', 798 'standard_e32a_v4', 'standard_e48a_v4', 'standard_e64a_v4', 'standard_e96a_v4', 799 'standard_e4as_v4', 'standard_e8as_v4', 'standard_e16as_v4', 'standard_e20as_v4', 800 'standard_e32as_v4', 'standard_e48as_v4', 'standard_e64as_v4', 'standard_e96as_v4'] 801 if size not in aval_sizes: 802 return 803 804 new_4core_sizes = ['Standard_D3_v2', 'Standard_D3_v2_Promo', 'Standard_D3_v2_ABC', 'Standard_DS3_v2', 805 'Standard_DS3_v2_Promo', 'Standard_D12_v2', 'Standard_D12_v2_Promo', 'Standard_D12_v2_ABC', 806 'Standard_DS12_v2', 'Standard_DS12_v2_Promo', 'Standard_F8s_v2', 'Standard_F4', 807 'Standard_F4_ABC', 'Standard_F4s', 'Standard_E8_v3', 'Standard_E8s_v3', 'Standard_D8_v3', 808 'Standard_D8s_v3'] 809 new_4core_sizes = [x.lower() for x in new_4core_sizes] 810 if size not in new_4core_sizes: 811 compute_client = _compute_client_factory(cli_ctx) 812 sizes = compute_client.virtual_machine_sizes.list(namespace.location) 813 size_info = next((s for s in sizes if s.name.lower() == size), None) 814 if size_info is None or size_info.number_of_cores < 8: 815 return 816 817 # VMs need to be a supported image in the marketplace 818 # Ubuntu 16.04 | 18.04, SLES 12 SP3, RHEL 7.4, CentOS 7.4, Flatcar, Debian "Stretch" with backports kernel 819 # Oracle Linux 7.4, Windows Server 2016, Windows Server 2012R2 820 publisher, offer, sku = namespace.os_publisher, namespace.os_offer, namespace.os_sku 821 if not publisher: 822 return 823 publisher, offer, sku = publisher.lower(), offer.lower(), sku.lower() 824 825 if publisher == 'coreos' or offer == 'coreos': 826 from azure.cli.core.parser import InvalidArgumentValueError 827 raise InvalidArgumentValueError("As CoreOS is deprecated and there is no image in the marketplace any more," 828 " please use Flatcar Container Linux instead.") 829 830 distros = [('canonical', 'UbuntuServer', '^16.04|^18.04'), 831 ('suse', 'sles', '^12-sp3'), ('redhat', 'rhel', '^7.4'), 832 ('openlogic', 'centos', '^7.4'), ('kinvolk', 'flatcar-container-linux-free', None), 833 ('kinvolk', 'flatcar-container-linux', None), ('credativ', 'debian', '-backports'), 834 ('oracle', 'oracle-linux', '^7.4'), ('MicrosoftWindowsServer', 'WindowsServer', '^2016'), 835 ('MicrosoftWindowsServer', 'WindowsServer', '^2012-R2')] 836 import re 837 for p, o, s in distros: 838 if p.lower() == publisher and (o is None or o.lower() == offer) and (s is None or re.match(s, sku, re.I)): 839 namespace.accelerated_networking = True 840 841 842def _validate_vmss_create_subnet(namespace): 843 if namespace.vnet_type == 'new': 844 if namespace.subnet_address_prefix is None: 845 cidr = namespace.vnet_address_prefix.split('/', 1)[0] 846 i = 0 847 for i in range(24, 16, -1): 848 if _subnet_capacity_check(i, namespace.instance_count, not namespace.disable_overprovision): 849 break 850 if i < 16: 851 err = "instance count '{}' is out of range of 2^16 subnet size'" 852 raise CLIError(err.format(namespace.instance_count)) 853 namespace.subnet_address_prefix = '{}/{}'.format(cidr, i) 854 855 if namespace.app_gateway_type and namespace.app_gateway_subnet_address_prefix is None: 856 namespace.app_gateway_subnet_address_prefix = _get_next_subnet_addr_suffix( 857 namespace.vnet_address_prefix, namespace.subnet_address_prefix, 24) 858 859 860def _get_next_subnet_addr_suffix(vnet_cidr, subnet_cidr, new_mask): 861 def _convert_to_int(address, bit_mask_len): 862 a, b, c, d = [int(x) for x in address.split('.')] 863 result = '{0:08b}{1:08b}{2:08b}{3:08b}'.format(a, b, c, d) 864 return int(result[:-bit_mask_len], 2) 865 866 error_msg = "usage error: --subnet-address-prefix value should be a subrange of --vnet-address-prefix's" 867 # extract vnet information needed to verify the defaults we are coming out 868 vnet_ip_address, mask = vnet_cidr.split('/') 869 vnet_bit_mask_len = 32 - int(mask) 870 vnet_int = _convert_to_int(vnet_ip_address, vnet_bit_mask_len) 871 872 subnet_ip_address, mask = subnet_cidr.split('/') 873 subnet_bit_mask_len = 32 - int(mask) 874 875 if vnet_bit_mask_len <= subnet_bit_mask_len: 876 raise CLIError(error_msg) 877 878 candidate_int = _convert_to_int(subnet_ip_address, subnet_bit_mask_len) + 1 879 if (candidate_int >> (vnet_bit_mask_len - subnet_bit_mask_len)) > vnet_int: # overflows? 880 candidate_int = candidate_int - 2 # try the other way around 881 if (candidate_int >> (vnet_bit_mask_len - subnet_bit_mask_len)) > vnet_int: 882 raise CLIError(error_msg) 883 884 # format back to the cidr 885 candaidate_str = '{0:32b}'.format(candidate_int << subnet_bit_mask_len) 886 return '{0}.{1}.{2}.{3}/{4}'.format(int(candaidate_str[0:8], 2), int(candaidate_str[8:16], 2), 887 int(candaidate_str[16:24], 2), int(candaidate_str[24:32], 2), 888 new_mask) 889 890 891def _validate_vm_create_nsg(cmd, namespace): 892 893 if namespace.nsg: 894 if check_existence(cmd.cli_ctx, namespace.nsg, namespace.resource_group_name, 895 'Microsoft.Network', 'networkSecurityGroups'): 896 namespace.nsg_type = 'existing' 897 logger.debug("using specified NSG '%s'", namespace.nsg) 898 else: 899 namespace.nsg_type = 'new' 900 logger.debug("specified NSG '%s' not found. It will be created.", namespace.nsg) 901 elif namespace.nsg == '': 902 namespace.nsg_type = None 903 logger.debug('no NSG will be used') 904 elif namespace.nsg is None: 905 namespace.nsg_type = 'new' 906 logger.debug('new NSG will be created') 907 908 909def _validate_vmss_create_nsg(cmd, namespace): 910 if namespace.nsg: 911 namespace.nsg = _get_resource_id(cmd.cli_ctx, namespace.nsg, namespace.resource_group_name, 912 'networkSecurityGroups', 'Microsoft.Network') 913 914 915def _validate_vm_vmss_create_public_ip(cmd, namespace): 916 if namespace.public_ip_address: 917 if check_existence(cmd.cli_ctx, namespace.public_ip_address, namespace.resource_group_name, 918 'Microsoft.Network', 'publicIPAddresses'): 919 namespace.public_ip_address_type = 'existing' 920 logger.debug("using existing specified public IP '%s'", namespace.public_ip_address) 921 else: 922 namespace.public_ip_address_type = 'new' 923 logger.debug("specified public IP '%s' not found. It will be created.", namespace.public_ip_address) 924 elif namespace.public_ip_address == '': 925 namespace.public_ip_address_type = None 926 logger.debug('no public IP address will be used') 927 elif namespace.public_ip_address is None: 928 namespace.public_ip_address_type = 'new' 929 logger.debug('new public IP address will be created') 930 931 from azure.cli.core.profiles import ResourceType 932 PublicIPAddressSkuName, IPAllocationMethod = cmd.get_models('PublicIPAddressSkuName', 'IPAllocationMethod', 933 resource_type=ResourceType.MGMT_NETWORK) 934 # Use standard public IP address automatically when using zones. 935 if hasattr(namespace, 'zone') and namespace.zone is not None: 936 namespace.public_ip_sku = PublicIPAddressSkuName.standard.value 937 938 # Public-IP SKU is only exposed for VM. VMSS has no such needs so far 939 if getattr(namespace, 'public_ip_sku', None): 940 if namespace.public_ip_sku == PublicIPAddressSkuName.standard.value: 941 if not namespace.public_ip_address_allocation: 942 namespace.public_ip_address_allocation = IPAllocationMethod.static.value 943 944 945def _validate_vmss_create_public_ip(cmd, namespace): 946 if namespace.load_balancer_type is None and namespace.app_gateway_type is None: 947 if namespace.public_ip_address: 948 raise CLIError('--public-ip-address can only be used when creating a new load ' 949 'balancer or application gateway frontend.') 950 namespace.public_ip_address = '' 951 _validate_vm_vmss_create_public_ip(cmd, namespace) 952 953 954def validate_delete_options(resources, delete_option): 955 """ Extracts multiple space-separated delete_option in key[=value] format """ 956 if resources and isinstance(delete_option, list): 957 if len(delete_option) == 1 and len(delete_option[0].split('=', 1)) == 1: 958 return delete_option[0] 959 delete_option_dict = {} 960 for item in delete_option: 961 delete_option_dict.update(validate_delete_option(item)) 962 return delete_option_dict 963 return None 964 965 966def validate_delete_option(string): 967 """ Extracts a single delete_option in key[=value] format """ 968 from azure.cli.core.azclierror import InvalidArgumentValueError 969 result = {} 970 if string: 971 comps = string.split('=', 1) 972 if len(comps) == 2: 973 result = {comps[0]: comps[1]} 974 else: 975 raise InvalidArgumentValueError( 976 "Invalid value for delete option. Use a singular value to apply on all resources, or use " 977 "<Name>=<Value> to configure the delete behavior for individual resources.") 978 return result 979 980 981def _validate_vm_create_nics(cmd, namespace): 982 from msrestazure.tools import resource_id 983 from azure.cli.core.commands.client_factory import get_subscription_id 984 nic_ids = namespace.nics 985 delete_option = validate_delete_options(nic_ids, getattr(namespace, 'nic_delete_option', None)) 986 nics = [] 987 988 if not nic_ids: 989 namespace.nic_type = 'new' 990 logger.debug('new NIC will be created') 991 return 992 993 if not isinstance(nic_ids, list): 994 nic_ids = [nic_ids] 995 996 for n in nic_ids: 997 nic = {'id': n if '/' in n else resource_id(name=n, 998 resource_group=namespace.resource_group_name, 999 namespace='Microsoft.Network', 1000 type='networkInterfaces', 1001 subscription=get_subscription_id(cmd.cli_ctx)), 1002 'properties': {'primary': nic_ids[0] == n} 1003 } 1004 if delete_option: 1005 nic['properties']['deleteOption'] = delete_option if isinstance(delete_option, str) else \ 1006 delete_option.get(n, None) 1007 nics.append(nic) 1008 1009 namespace.nics = nics 1010 namespace.nic_type = 'existing' 1011 namespace.public_ip_address_type = None 1012 logger.debug('existing NIC(s) will be used') 1013 1014 1015def _validate_vm_vmss_create_auth(namespace, cmd=None): 1016 if namespace.storage_profile in [StorageProfile.ManagedSpecializedOSDisk, 1017 StorageProfile.SASpecializedOSDisk]: 1018 return 1019 1020 if namespace.admin_username is None: 1021 namespace.admin_username = get_default_admin_username() 1022 if namespace.admin_username and namespace.os_type: 1023 namespace.admin_username = _validate_admin_username(namespace.admin_username, namespace.os_type) 1024 1025 # if not namespace.os_type: 1026 # raise CLIError("Unable to resolve OS type. Specify '--os-type' argument.") 1027 1028 if not namespace.authentication_type: 1029 # if both ssh key and password, infer that authentication_type is all. 1030 if namespace.ssh_key_value and namespace.admin_password: 1031 namespace.authentication_type = 'all' 1032 else: 1033 # apply default auth type (password for Windows, ssh for Linux) by examining the OS type 1034 namespace.authentication_type = 'password' \ 1035 if ((namespace.os_type and namespace.os_type.lower() == 'windows') or 1036 namespace.admin_password) else 'ssh' 1037 1038 if namespace.os_type and namespace.os_type.lower() == 'windows' and namespace.authentication_type == 'ssh': 1039 raise CLIError('SSH not supported for Windows VMs.') 1040 1041 # validate proper arguments supplied based on the authentication type 1042 if namespace.authentication_type == 'password': 1043 if namespace.ssh_key_value or namespace.ssh_dest_key_path: 1044 raise CLIError('SSH key cannot be used with password authentication type.') 1045 1046 # if password not given, attempt to prompt user for password. 1047 if not namespace.admin_password: 1048 _prompt_for_password(namespace) 1049 1050 # validate password 1051 _validate_admin_password(namespace.admin_password, namespace.os_type) 1052 1053 elif namespace.authentication_type == 'ssh': 1054 1055 if namespace.admin_password: 1056 raise CLIError('Admin password cannot be used with SSH authentication type.') 1057 1058 validate_ssh_key(namespace, cmd) 1059 1060 if not namespace.ssh_dest_key_path: 1061 namespace.ssh_dest_key_path = '/home/{}/.ssh/authorized_keys'.format(namespace.admin_username) 1062 1063 elif namespace.authentication_type == 'all': 1064 if namespace.os_type and namespace.os_type.lower() == 'windows': 1065 raise CLIError('SSH not supported for Windows VMs. Use password authentication.') 1066 1067 if not namespace.admin_password: 1068 _prompt_for_password(namespace) 1069 _validate_admin_password(namespace.admin_password, namespace.os_type) 1070 1071 validate_ssh_key(namespace, cmd) 1072 if not namespace.ssh_dest_key_path: 1073 namespace.ssh_dest_key_path = '/home/{}/.ssh/authorized_keys'.format(namespace.admin_username) 1074 1075 1076def _prompt_for_password(namespace): 1077 from knack.prompting import prompt_pass, NoTTYException 1078 try: 1079 namespace.admin_password = prompt_pass('Admin Password: ', confirm=True) 1080 except NoTTYException: 1081 raise CLIError('Please specify password in non-interactive mode.') 1082 1083 1084def _validate_admin_username(username, os_type): 1085 import re 1086 if not username: 1087 raise CLIError("admin user name can not be empty") 1088 is_linux = (os_type.lower() == 'linux') 1089 # pylint: disable=line-too-long 1090 pattern = (r'[\\\/"\[\]:|<>+=;,?*@#()!A-Z]+' if is_linux else r'[\\\/"\[\]:|<>+=;,?*@]+') 1091 linux_err = r'admin user name cannot contain upper case character A-Z, special characters \/"[]:|<>+=;,?*@#()! or start with $ or -' 1092 win_err = r'admin user name cannot contain special characters \/"[]:|<>+=;,?*@# or ends with .' 1093 if re.findall(pattern, username): 1094 raise CLIError(linux_err if is_linux else win_err) 1095 if is_linux and re.findall(r'^[$-]+', username): 1096 raise CLIError(linux_err) 1097 if not is_linux and username.endswith('.'): 1098 raise CLIError(win_err) 1099 if username.lower() in DISALLOWED_USER_NAMES: 1100 raise CLIError("This user name '{}' meets the general requirements, but is specifically disallowed for this image. Please try a different value.".format(username)) 1101 return username 1102 1103 1104def _validate_admin_password(password, os_type): 1105 import re 1106 is_linux = (os_type.lower() == 'linux') 1107 max_length = 72 if is_linux else 123 1108 min_length = 12 1109 1110 contains_lower = re.findall('[a-z]+', password) 1111 contains_upper = re.findall('[A-Z]+', password) 1112 contains_digit = re.findall('[0-9]+', password) 1113 contains_special_char = re.findall(r'[ `~!@#$%^&*()=+_\[\]{}\|;:.\/\'\",<>?]+', password) 1114 count = len([x for x in [contains_lower, contains_upper, 1115 contains_digit, contains_special_char] if x]) 1116 1117 # pylint: disable=line-too-long 1118 error_msg = ("The password length must be between {} and {}. Password must have the 3 of the following: " 1119 "1 lower case character, 1 upper case character, 1 number and 1 special character.").format(min_length, max_length) 1120 if len(password) not in range(min_length, max_length + 1) or count < 3: 1121 raise CLIError(error_msg) 1122 1123 1124def validate_ssh_key(namespace, cmd=None): 1125 from azure.core.exceptions import HttpResponseError 1126 if hasattr(namespace, 'ssh_key_name') and namespace.ssh_key_name: 1127 client = _compute_client_factory(cmd.cli_ctx) 1128 # --ssh-key-name 1129 if not namespace.ssh_key_value and not namespace.generate_ssh_keys: 1130 # Use existing key, key must exist 1131 try: 1132 ssh_key_resource = client.ssh_public_keys.get(namespace.resource_group_name, namespace.ssh_key_name) 1133 except HttpResponseError: 1134 raise ValidationError('SSH key {} does not exist!'.format(namespace.ssh_key_name)) 1135 namespace.ssh_key_value = [ssh_key_resource.public_key] 1136 logger.info('Get a key from --ssh-key-name successfully') 1137 elif namespace.ssh_key_value: 1138 raise ValidationError('--ssh-key-name and --ssh-key-values cannot be used together') 1139 elif namespace.generate_ssh_keys: 1140 parameters = {} 1141 parameters['location'] = namespace.location 1142 public_key = _validate_ssh_key_helper("", namespace.generate_ssh_keys) 1143 parameters['public_key'] = public_key 1144 client.ssh_public_keys.create(resource_group_name=namespace.resource_group_name, 1145 ssh_public_key_name=namespace.ssh_key_name, 1146 parameters=parameters) 1147 namespace.ssh_key_value = [public_key] 1148 elif namespace.ssh_key_value: 1149 if namespace.generate_ssh_keys and len(namespace.ssh_key_value) > 1: 1150 logger.warning("Ignoring --generate-ssh-keys as multiple ssh key values have been specified.") 1151 namespace.generate_ssh_keys = False 1152 1153 processed_ssh_key_values = [] 1154 for ssh_key_value in namespace.ssh_key_value: 1155 processed_ssh_key_values.append(_validate_ssh_key_helper(ssh_key_value, namespace.generate_ssh_keys)) 1156 namespace.ssh_key_value = processed_ssh_key_values 1157 # if no ssh keys processed, try to generate new key / use existing at root. 1158 else: 1159 namespace.ssh_key_value = [_validate_ssh_key_helper("", namespace.generate_ssh_keys)] 1160 1161 1162def _validate_ssh_key_helper(ssh_key_value, should_generate_ssh_keys): 1163 string_or_file = (ssh_key_value or 1164 os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa.pub')) 1165 content = string_or_file 1166 if os.path.exists(string_or_file): 1167 logger.info('Use existing SSH public key file: %s', string_or_file) 1168 with open(string_or_file, 'r') as f: 1169 content = f.read() 1170 elif not keys.is_valid_ssh_rsa_public_key(content): 1171 if should_generate_ssh_keys: 1172 # figure out appropriate file names: 1173 # 'base_name'(with private keys), and 'base_name.pub'(with public keys) 1174 public_key_filepath = string_or_file 1175 if public_key_filepath[-4:].lower() == '.pub': 1176 private_key_filepath = public_key_filepath[:-4] 1177 else: 1178 private_key_filepath = public_key_filepath + '.private' 1179 content = keys.generate_ssh_keys(private_key_filepath, public_key_filepath) 1180 logger.warning("SSH key files '%s' and '%s' have been generated under ~/.ssh to " 1181 "allow SSH access to the VM. If using machines without " 1182 "permanent storage, back up your keys to a safe location.", 1183 private_key_filepath, public_key_filepath) 1184 else: 1185 raise CLIError('An RSA key file or key value must be supplied to SSH Key Value. ' 1186 'You can use --generate-ssh-keys to let CLI generate one for you') 1187 return content 1188 1189 1190def _validate_vm_vmss_msi(cmd, namespace, from_set_command=False): 1191 if from_set_command or namespace.assign_identity is not None: 1192 identities = namespace.assign_identity or [] 1193 from ._vm_utils import MSI_LOCAL_ID 1194 for i, _ in enumerate(identities): 1195 if identities[i] != MSI_LOCAL_ID: 1196 identities[i] = _get_resource_id(cmd.cli_ctx, identities[i], namespace.resource_group_name, 1197 'userAssignedIdentities', 'Microsoft.ManagedIdentity') 1198 if not namespace.identity_scope and getattr(namespace.identity_role, 'is_default', None) is None: 1199 raise CLIError("usage error: '--role {}' is not applicable as the '--scope' is not provided".format( 1200 namespace.identity_role)) 1201 user_assigned_identities = [x for x in identities if x != MSI_LOCAL_ID] 1202 if user_assigned_identities and not cmd.supported_api_version(min_api='2017-12-01'): 1203 raise CLIError('usage error: user assigned identity is only available under profile ' 1204 'with minimum Compute API version of 2017-12-01') 1205 if namespace.identity_scope: 1206 if identities and MSI_LOCAL_ID not in identities: 1207 raise CLIError("usage error: '--scope'/'--role' is only applicable when assign system identity") 1208 # keep 'identity_role' for output as logical name is more readable 1209 setattr(namespace, 'identity_role_id', _resolve_role_id(cmd.cli_ctx, namespace.identity_role, 1210 namespace.identity_scope)) 1211 elif namespace.identity_scope or getattr(namespace.identity_role, 'is_default', None) is None: 1212 raise CLIError('usage error: --assign-identity [--scope SCOPE] [--role ROLE]') 1213 1214 1215def _resolve_role_id(cli_ctx, role, scope): 1216 import re 1217 import uuid 1218 from azure.cli.core.commands.client_factory import get_mgmt_service_client 1219 from azure.cli.core.profiles import ResourceType 1220 client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION).role_definitions 1221 1222 role_id = None 1223 if re.match(r'/subscriptions/.+/providers/Microsoft.Authorization/roleDefinitions/', 1224 role, re.I): 1225 role_id = role 1226 else: 1227 try: 1228 uuid.UUID(role) 1229 role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format( 1230 client.config.subscription_id, role) 1231 except ValueError: 1232 pass 1233 if not role_id: # retrieve role id 1234 role_defs = list(client.list(scope, "roleName eq '{}'".format(role))) 1235 if not role_defs: 1236 raise CLIError("Role '{}' doesn't exist.".format(role)) 1237 if len(role_defs) > 1: 1238 ids = [r.id for r in role_defs] 1239 err = "More than one role matches the given name '{}'. Please pick an id from '{}'" 1240 raise CLIError(err.format(role, ids)) 1241 role_id = role_defs[0].id 1242 return role_id 1243 1244 1245def process_vm_create_namespace(cmd, namespace): 1246 validate_tags(namespace) 1247 _validate_location(cmd, namespace, namespace.zone, namespace.size) 1248 validate_edge_zone(cmd, namespace) 1249 if namespace.count is not None: 1250 _validate_count(namespace) 1251 validate_asg_names_or_ids(cmd, namespace) 1252 _validate_vm_create_storage_profile(cmd, namespace) 1253 if namespace.storage_profile in [StorageProfile.SACustomImage, 1254 StorageProfile.SAPirImage]: 1255 _validate_vm_create_storage_account(cmd, namespace) 1256 1257 _validate_vm_create_availability_set(cmd, namespace) 1258 _validate_vm_create_vmss(cmd, namespace) 1259 _validate_vm_vmss_create_vnet(cmd, namespace) 1260 _validate_vm_create_nsg(cmd, namespace) 1261 _validate_vm_vmss_create_public_ip(cmd, namespace) 1262 _validate_vm_create_nics(cmd, namespace) 1263 _validate_vm_vmss_accelerated_networking(cmd.cli_ctx, namespace) 1264 _validate_vm_vmss_create_auth(namespace, cmd) 1265 1266 _validate_proximity_placement_group(cmd, namespace) 1267 _validate_vm_create_dedicated_host(cmd, namespace) 1268 1269 if namespace.secrets: 1270 _validate_secrets(namespace.secrets, namespace.os_type) 1271 _validate_vm_vmss_msi(cmd, namespace) 1272 if namespace.boot_diagnostics_storage: 1273 namespace.boot_diagnostics_storage = get_storage_blob_uri(cmd.cli_ctx, namespace.boot_diagnostics_storage) 1274 1275 _validate_capacity_reservation_group(cmd, namespace) 1276 1277# endregion 1278 1279 1280def process_vm_update_namespace(cmd, namespace): 1281 _validate_capacity_reservation_group(cmd, namespace) 1282 1283 1284# region VMSS Create Validators 1285def _get_default_address_pool(cli_ctx, resource_group, balancer_name, balancer_type): 1286 option_name = '--backend-pool-name' 1287 client = getattr(get_network_client(cli_ctx), balancer_type, None) 1288 if not client: 1289 raise CLIError('unrecognized balancer type: {}'.format(balancer_type)) 1290 1291 balancer = client.get(resource_group, balancer_name) 1292 values = [x.name for x in balancer.backend_address_pools] 1293 if len(values) > 1: 1294 raise CLIError("Multiple possible values found for '{0}': {1}\nSpecify '{0}' " 1295 "explicitly.".format(option_name, ', '.join(values))) 1296 if not values: 1297 raise CLIError("No existing values found for '{0}'. Create one first and try " 1298 "again.".format(option_name)) 1299 return values[0] 1300 1301 1302# Client end hack per: https://github.com/Azure/azure-cli/issues/9943 1303def _validate_vmss_single_placement_group(namespace): 1304 if namespace.zones or namespace.instance_count > 100: 1305 if namespace.single_placement_group is None: 1306 namespace.single_placement_group = False 1307 1308 1309def _validate_vmss_create_load_balancer_or_app_gateway(cmd, namespace): 1310 from msrestazure.tools import parse_resource_id 1311 from azure.cli.core.profiles import ResourceType 1312 from azure.core.exceptions import HttpResponseError 1313 std_lb_is_available = cmd.supported_api_version(min_api='2017-08-01', resource_type=ResourceType.MGMT_NETWORK) 1314 1315 if namespace.load_balancer and namespace.application_gateway: 1316 raise CLIError('incorrect usage: --load-balancer NAME_OR_ID | ' 1317 '--application-gateway NAME_OR_ID') 1318 1319 # Resolve the type of balancer (if any) being used 1320 balancer_type = 'None' 1321 if namespace.load_balancer is None and namespace.application_gateway is None: 1322 if std_lb_is_available: 1323 balancer_type = 'loadBalancer' 1324 else: # needed for Stack profile 2017_03_09 1325 balancer_type = 'loadBalancer' if namespace.single_placement_group is not False else 'applicationGateway' 1326 logger.debug("W/o STD LB, defaulting to '%s' under because single placement group is disabled", 1327 balancer_type) 1328 1329 elif namespace.load_balancer: 1330 balancer_type = 'loadBalancer' 1331 elif namespace.application_gateway: 1332 balancer_type = 'applicationGateway' 1333 1334 if balancer_type == 'applicationGateway': 1335 1336 if namespace.application_gateway: 1337 client = get_network_client(cmd.cli_ctx).application_gateways 1338 try: 1339 rg = parse_resource_id(namespace.application_gateway).get( 1340 'resource_group', namespace.resource_group_name) 1341 ag_name = parse_resource_id(namespace.application_gateway)['name'] 1342 client.get(rg, ag_name) 1343 namespace.app_gateway_type = 'existing' 1344 namespace.backend_pool_name = namespace.backend_pool_name or \ 1345 _get_default_address_pool(cmd.cli_ctx, rg, ag_name, 'application_gateways') 1346 logger.debug("using specified existing application gateway '%s'", namespace.application_gateway) 1347 except HttpResponseError: 1348 namespace.app_gateway_type = 'new' 1349 logger.debug("application gateway '%s' not found. It will be created.", namespace.application_gateway) 1350 elif namespace.application_gateway == '': 1351 namespace.app_gateway_type = None 1352 logger.debug('no application gateway will be used') 1353 elif namespace.application_gateway is None: 1354 namespace.app_gateway_type = 'new' 1355 logger.debug('new application gateway will be created') 1356 1357 # AppGateway frontend 1358 required = [] 1359 if namespace.app_gateway_type == 'new': 1360 required.append('app_gateway_sku') 1361 required.append('app_gateway_capacity') 1362 if namespace.vnet_type != 'new': 1363 required.append('app_gateway_subnet_address_prefix') 1364 elif namespace.app_gateway_type == 'existing': 1365 required.append('backend_pool_name') 1366 forbidden = ['nat_pool_name', 'load_balancer', 'health_probe'] 1367 validate_parameter_set(namespace, required, forbidden, description='network balancer: application gateway') 1368 1369 elif balancer_type == 'loadBalancer': 1370 # LoadBalancer frontend 1371 required = [] 1372 forbidden = ['app_gateway_subnet_address_prefix', 'application_gateway', 'app_gateway_sku', 1373 'app_gateway_capacity'] 1374 validate_parameter_set(namespace, required, forbidden, description='network balancer: load balancer') 1375 1376 if namespace.load_balancer: 1377 rg = parse_resource_id(namespace.load_balancer).get('resource_group', namespace.resource_group_name) 1378 lb_name = parse_resource_id(namespace.load_balancer)['name'] 1379 lb = get_network_lb(cmd.cli_ctx, namespace.resource_group_name, lb_name) 1380 if lb: 1381 namespace.load_balancer_type = 'existing' 1382 namespace.backend_pool_name = namespace.backend_pool_name or \ 1383 _get_default_address_pool(cmd.cli_ctx, rg, lb_name, 'load_balancers') 1384 if not namespace.nat_pool_name: 1385 if len(lb.inbound_nat_pools) > 1: 1386 raise CLIError("Multiple possible values found for '{0}': {1}\nSpecify '{0}' explicitly.".format( # pylint: disable=line-too-long 1387 '--nat-pool-name', ', '.join([n.name for n in lb.inbound_nat_pools]))) 1388 if not lb.inbound_nat_pools: # Associated scaleset will be missing ssh/rdp, so warn here. 1389 logger.warning("No inbound nat pool was configured on '%s'", namespace.load_balancer) 1390 else: 1391 namespace.nat_pool_name = lb.inbound_nat_pools[0].name 1392 logger.debug("using specified existing load balancer '%s'", namespace.load_balancer) 1393 else: 1394 namespace.load_balancer_type = 'new' 1395 logger.debug("load balancer '%s' not found. It will be created.", namespace.load_balancer) 1396 elif namespace.load_balancer == '': 1397 namespace.load_balancer_type = None 1398 logger.debug('no load balancer will be used') 1399 elif namespace.load_balancer is None: 1400 namespace.load_balancer_type = 'new' 1401 logger.debug('new load balancer will be created') 1402 1403 if namespace.load_balancer_type == 'new' and namespace.single_placement_group is False and std_lb_is_available: 1404 LBSkuName = cmd.get_models('LoadBalancerSkuName', resource_type=ResourceType.MGMT_NETWORK) 1405 if namespace.load_balancer_sku is None: 1406 namespace.load_balancer_sku = LBSkuName.standard.value 1407 logger.debug("use Standard sku as single placement group is turned off") 1408 elif namespace.load_balancer_sku == LBSkuName.basic.value: 1409 if namespace.zones: 1410 err = "'Standard' load balancer is required for zonal scale-sets" 1411 elif namespace.instance_count > 100: 1412 err = "'Standard' load balancer is required for scale-sets with 100+ instances" 1413 else: 1414 err = "'Standard' load balancer is required because 'single placement group' is turned off" 1415 1416 raise CLIError('usage error:{}'.format(err)) 1417 1418 1419def get_network_client(cli_ctx): 1420 from azure.cli.core.profiles import ResourceType 1421 from azure.cli.core.commands.client_factory import get_mgmt_service_client 1422 return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_NETWORK, api_version=get_target_network_api(cli_ctx)) 1423 1424 1425def get_network_lb(cli_ctx, resource_group_name, lb_name): 1426 from azure.core.exceptions import HttpResponseError 1427 network_client = get_network_client(cli_ctx) 1428 try: 1429 return network_client.load_balancers.get(resource_group_name, lb_name) 1430 except HttpResponseError: 1431 return None 1432 1433 1434def process_vmss_create_namespace(cmd, namespace): 1435 from azure.cli.core.azclierror import InvalidArgumentValueError 1436 # uniform_str = 'Uniform' 1437 flexible_str = 'Flexible' 1438 if namespace.orchestration_mode.lower() == flexible_str.lower(): 1439 1440 # The commentted parameters are also forbidden, but they have default values. 1441 # I don't know whether they are provided by user. 1442 1443 namespace.load_balancer_sku = 'Standard' # lb sku MUST be standard 1444 # namespace.public_ip_per_vm = True # default to true for VMSS Flex 1445 # namespace.disable_overprovision = True # overprovisioning must be false for vmss flex preview 1446 # namespace.single_placement_group = False # SPG must be false for VMSS flex 1447 namespace.upgrade_policy_mode = None 1448 namespace.use_unmanaged_disk = None 1449 1450 banned_params = [ 1451 # namespace.accelerated_networking, 1452 # namespace.admin_password, 1453 # namespace.admin_username, 1454 # namespace.application_gateway, 1455 # namespace.app_gateway_capacity, 1456 # namespace.app_gateway_sku, 1457 # namespace.app_gateway_subnet_address_prefix, 1458 # namespace.application_security_groups, 1459 # namespace.assign_identity, 1460 # namespace.authentication_type, 1461 # namespace.backend_pool_name, 1462 # namespace.backend_port, 1463 # namespace.computer_name_prefix, 1464 # namespace.custom_data, 1465 # namespace.data_caching, 1466 # namespace.data_disk_sizes_gb, 1467 # namespace.disable_overprovision, 1468 # namespace.dns_servers, 1469 # namespace.ephemeral_os_disk, 1470 # namespace.eviction_policy, 1471 # namespace.generate_ssh_keys, 1472 namespace.health_probe, 1473 namespace.host_group, 1474 # namespace.image, 1475 # namespace.instance_count, 1476 # namespace.load_balancer, 1477 namespace.nat_pool_name, 1478 # namespace.load_balancer_sku, 1479 # namespace.license_type, 1480 # namespace.max_price, 1481 # namespace.nsg, 1482 # namespace.os_caching, 1483 # namespace.os_disk_name, 1484 # namespace.os_type, 1485 # namespace.plan_name, 1486 # namespace.plan_product, 1487 # namespace.plan_promotion_code, 1488 # namespace.plan_publisher, 1489 # namespace.priority, 1490 # namespace.public_ip_address, 1491 # namespace.public_ip_address_allocation, 1492 # namespace.public_ip_address_dns_name, 1493 # namespace.public_ip_per_vm, 1494 # namespace.identity_role, 1495 # namespace.identity_scope, 1496 namespace.scale_in_policy, 1497 # namespace.secrets, 1498 # namespace.ssh_dest_key_path, 1499 # namespace.ssh_key_value, 1500 # namespace.storage_container_name, 1501 # namespace.storage_sku, 1502 # namespace.subnet, 1503 # namespace.subnet_address_prefix, 1504 # namespace.terminate_notification_time, 1505 # namespace.ultra_ssd_enabled, 1506 # namespace.upgrade_policy_mode, 1507 # namespace.use_unmanaged_disk, 1508 # namespace.vm_domain_name, 1509 # namespace.vm_sku, 1510 # namespace.vnet_address_prefix, 1511 # namespace.vnet_name, 1512 namespace.user_data 1513 ] 1514 if any(param is not None for param in banned_params): 1515 raise CLIError('usage error: In VM mode, only name, resource-group, location, ' 1516 'tags, zones, platform-fault-domain-count, single-placement-group and ppg are allowed') 1517 1518 if namespace.image: 1519 1520 if namespace.vm_sku is None: 1521 from azure.cli.core.cloud import AZURE_US_GOV_CLOUD 1522 if cmd.cli_ctx.cloud.name != AZURE_US_GOV_CLOUD.name: 1523 namespace.vm_sku = 'Standard_DS1_v2' 1524 else: 1525 namespace.vm_sku = 'Standard_D1_v2' 1526 1527 if namespace.single_placement_group: 1528 raise ArgumentUsageError( 1529 'usage error: single placement group can only be set to False in Flexible VMSS') 1530 namespace.single_placement_group = False 1531 1532 if namespace.network_api_version is None: 1533 namespace.network_api_version = '2020-11-01' 1534 1535 if namespace.platform_fault_domain_count is None: 1536 namespace.platform_fault_domain_count = 1 1537 1538 if namespace.computer_name_prefix is None: 1539 namespace.computer_name_prefix = namespace.vmss_name[:8] 1540 1541 # if namespace.platform_fault_domain_count is None: 1542 # raise CLIError("usage error: --platform-fault-domain-count is required in Flexible mode") 1543 1544 if namespace.tags is not None: 1545 validate_tags(namespace) 1546 _validate_location(cmd, namespace, namespace.zones, namespace.vm_sku) 1547 # validate_edge_zone(cmd, namespace) 1548 if namespace.application_security_groups is not None: 1549 validate_asg_names_or_ids(cmd, namespace) 1550 1551 if getattr(namespace, 'attach_os_disk', None) or namespace.image is not None: 1552 _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=True) 1553 1554 if namespace.vnet_name or namespace.subnet or namespace.image: 1555 _validate_vm_vmss_create_vnet(cmd, namespace, for_scale_set=True) 1556 _validate_vmss_create_subnet(namespace) 1557 1558 if namespace.load_balancer or namespace.application_gateway or namespace.image: 1559 _validate_vmss_create_load_balancer_or_app_gateway(cmd, namespace) 1560 1561 if namespace.public_ip_address or namespace.image: 1562 _validate_vmss_create_public_ip(cmd, namespace) 1563 1564 if namespace.nsg is not None: 1565 _validate_vmss_create_nsg(cmd, namespace) 1566 if namespace.accelerated_networking is not None: 1567 _validate_vm_vmss_accelerated_networking(cmd.cli_ctx, namespace) 1568 if any([namespace.admin_password, namespace.ssh_dest_key_path, namespace.generate_ssh_keys, 1569 namespace.authentication_type, namespace.os_type]): 1570 _validate_vm_vmss_create_auth(namespace, cmd) 1571 if namespace.assign_identity == '[system]': 1572 raise InvalidArgumentValueError('usage error: only user assigned indetity is suppoprted for Flex mode.') 1573 if namespace.assign_identity is not None: 1574 _validate_vm_vmss_msi(cmd, namespace) # -- UserAssignedOnly 1575 _validate_proximity_placement_group(cmd, namespace) 1576 _validate_vmss_terminate_notification(cmd, namespace) 1577 if namespace.automatic_repairs_grace_period is not None: 1578 _validate_vmss_create_automatic_repairs(cmd, namespace) 1579 _validate_vmss_create_host_group(cmd, namespace) 1580 1581 if namespace.secrets is not None: 1582 _validate_secrets(namespace.secrets, namespace.os_type) 1583 1584 if namespace.eviction_policy and not namespace.priority: 1585 raise ArgumentUsageError('usage error: --priority PRIORITY [--eviction-policy POLICY]') 1586 1587 return 1588 1589 # Uniform mode 1590 if namespace.disable_overprovision is None: 1591 namespace.disable_overprovision = False 1592 validate_tags(namespace) 1593 if namespace.vm_sku is None: 1594 from azure.cli.core.cloud import AZURE_US_GOV_CLOUD 1595 if cmd.cli_ctx.cloud.name != AZURE_US_GOV_CLOUD.name: 1596 namespace.vm_sku = 'Standard_DS1_v2' 1597 else: 1598 namespace.vm_sku = 'Standard_D1_v2' 1599 _validate_location(cmd, namespace, namespace.zones, namespace.vm_sku) 1600 validate_edge_zone(cmd, namespace) 1601 validate_asg_names_or_ids(cmd, namespace) 1602 _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=True) 1603 _validate_vm_vmss_create_vnet(cmd, namespace, for_scale_set=True) 1604 1605 _validate_vmss_single_placement_group(namespace) 1606 _validate_vmss_create_load_balancer_or_app_gateway(cmd, namespace) 1607 _validate_vmss_create_subnet(namespace) 1608 _validate_vmss_create_public_ip(cmd, namespace) 1609 _validate_vmss_create_nsg(cmd, namespace) 1610 _validate_vm_vmss_accelerated_networking(cmd.cli_ctx, namespace) 1611 _validate_vm_vmss_create_auth(namespace, cmd) 1612 _validate_vm_vmss_msi(cmd, namespace) 1613 _validate_proximity_placement_group(cmd, namespace) 1614 _validate_vmss_terminate_notification(cmd, namespace) 1615 _validate_vmss_create_automatic_repairs(cmd, namespace) 1616 _validate_vmss_create_host_group(cmd, namespace) 1617 1618 if namespace.secrets: 1619 _validate_secrets(namespace.secrets, namespace.os_type) 1620 1621 if not namespace.public_ip_per_vm and namespace.vm_domain_name: 1622 raise CLIError('usage error: --vm-domain-name can only be used when --public-ip-per-vm is enabled') 1623 1624 if namespace.eviction_policy and not namespace.priority: 1625 raise CLIError('usage error: --priority PRIORITY [--eviction-policy POLICY]') 1626 1627 _validate_capacity_reservation_group(cmd, namespace) 1628 1629 1630def validate_vmss_update_namespace(cmd, namespace): # pylint: disable=unused-argument 1631 if not namespace.instance_id: 1632 if namespace.protect_from_scale_in is not None or namespace.protect_from_scale_set_actions is not None: 1633 raise CLIError("usage error: protection policies can only be applied to VM instances within a VMSS." 1634 " Please use --instance-id to specify a VM instance") 1635 _validate_vmss_update_terminate_notification_related(cmd, namespace) 1636 _validate_vmss_update_automatic_repairs(cmd, namespace) 1637 _validate_capacity_reservation_group(cmd, namespace) 1638# endregion 1639 1640 1641# region disk, snapshot, image validators 1642def validate_vm_disk(cmd, namespace): 1643 namespace.disk = _get_resource_id(cmd.cli_ctx, namespace.disk, 1644 namespace.resource_group_name, 'disks', 'Microsoft.Compute') 1645 1646 1647def validate_vmss_disk(cmd, namespace): 1648 if namespace.disk: 1649 namespace.disk = _get_resource_id(cmd.cli_ctx, namespace.disk, 1650 namespace.resource_group_name, 'disks', 'Microsoft.Compute') 1651 if bool(namespace.disk) == bool(namespace.size_gb): 1652 raise CLIError('usage error: --disk EXIST_DISK --instance-id ID | --size-gb GB') 1653 if bool(namespace.disk) != bool(namespace.instance_id): 1654 raise CLIError('usage error: --disk EXIST_DISK --instance-id ID') 1655 1656 1657def process_disk_or_snapshot_create_namespace(cmd, namespace): 1658 from msrestazure.azure_exceptions import CloudError 1659 validate_tags(namespace) 1660 validate_edge_zone(cmd, namespace) 1661 if namespace.source: 1662 usage_error = 'usage error: --source {SNAPSHOT | DISK} | --source VHD_BLOB_URI [--source-storage-account-id ID]' 1663 try: 1664 namespace.source_blob_uri, namespace.source_disk, namespace.source_snapshot = _figure_out_storage_source( 1665 cmd.cli_ctx, namespace.resource_group_name, namespace.source) 1666 if not namespace.source_blob_uri and namespace.source_storage_account_id: 1667 raise CLIError(usage_error) 1668 except CloudError: 1669 raise CLIError(usage_error) 1670 1671 1672def process_image_create_namespace(cmd, namespace): 1673 from msrestazure.tools import parse_resource_id 1674 validate_tags(namespace) 1675 validate_edge_zone(cmd, namespace) 1676 source_from_vm = False 1677 try: 1678 # try capturing from VM, a most common scenario 1679 res_id = _get_resource_id(cmd.cli_ctx, namespace.source, namespace.resource_group_name, 1680 'virtualMachines', 'Microsoft.Compute') 1681 res = parse_resource_id(res_id) 1682 if res['type'] == 'virtualMachines': 1683 compute_client = _compute_client_factory(cmd.cli_ctx, subscription_id=res['subscription']) 1684 vm_info = compute_client.virtual_machines.get(res['resource_group'], res['name']) 1685 source_from_vm = True 1686 except ResourceNotFoundError: 1687 pass 1688 1689 if source_from_vm: 1690 # pylint: disable=no-member 1691 namespace.os_type = vm_info.storage_profile.os_disk.os_type 1692 namespace.source_virtual_machine = res_id 1693 if namespace.data_disk_sources: 1694 raise CLIError("'--data-disk-sources' is not allowed when capturing " 1695 "images from virtual machines") 1696 else: 1697 namespace.os_blob_uri, namespace.os_disk, namespace.os_snapshot = _figure_out_storage_source(cmd.cli_ctx, namespace.resource_group_name, namespace.source) # pylint: disable=line-too-long 1698 namespace.data_blob_uris = [] 1699 namespace.data_disks = [] 1700 namespace.data_snapshots = [] 1701 if namespace.data_disk_sources: 1702 for data_disk_source in namespace.data_disk_sources: 1703 source_blob_uri, source_disk, source_snapshot = _figure_out_storage_source( 1704 cmd.cli_ctx, namespace.resource_group_name, data_disk_source) 1705 if source_blob_uri: 1706 namespace.data_blob_uris.append(source_blob_uri) 1707 if source_disk: 1708 namespace.data_disks.append(source_disk) 1709 if source_snapshot: 1710 namespace.data_snapshots.append(source_snapshot) 1711 if not namespace.os_type: 1712 raise CLIError("usage error: os type is required to create the image, " 1713 "please specify '--os-type OS_TYPE'") 1714 1715 1716def _figure_out_storage_source(cli_ctx, resource_group_name, source): 1717 source_blob_uri = None 1718 source_disk = None 1719 source_snapshot = None 1720 if urlparse(source).scheme: # a uri? 1721 source_blob_uri = source 1722 elif '/disks/' in source.lower(): 1723 source_disk = source 1724 elif '/snapshots/' in source.lower(): 1725 source_snapshot = source 1726 else: 1727 compute_client = _compute_client_factory(cli_ctx) 1728 # pylint: disable=no-member 1729 try: 1730 info = compute_client.snapshots.get(resource_group_name, source) 1731 source_snapshot = info.id 1732 except ResourceNotFoundError: 1733 info = compute_client.disks.get(resource_group_name, source) 1734 source_disk = info.id 1735 1736 return (source_blob_uri, source_disk, source_snapshot) 1737 1738 1739def process_disk_encryption_namespace(cmd, namespace): 1740 namespace.disk_encryption_keyvault = _get_resource_id(cmd.cli_ctx, namespace.disk_encryption_keyvault, 1741 namespace.resource_group_name, 1742 'vaults', 'Microsoft.KeyVault') 1743 1744 if namespace.key_encryption_keyvault: 1745 if not namespace.key_encryption_key: 1746 raise CLIError("Incorrect usage '--key-encryption-keyvault': " 1747 "'--key-encryption-key' is required") 1748 namespace.key_encryption_keyvault = _get_resource_id(cmd.cli_ctx, namespace.key_encryption_keyvault, 1749 namespace.resource_group_name, 1750 'vaults', 'Microsoft.KeyVault') 1751 1752 1753def process_assign_identity_namespace(cmd, namespace): 1754 _validate_vm_vmss_msi(cmd, namespace, from_set_command=True) 1755 1756 1757def process_remove_identity_namespace(cmd, namespace): 1758 if namespace.identities: 1759 from ._vm_utils import MSI_LOCAL_ID 1760 for i in range(len(namespace.identities)): 1761 if namespace.identities[i] != MSI_LOCAL_ID: 1762 namespace.identities[i] = _get_resource_id(cmd.cli_ctx, namespace.identities[i], 1763 namespace.resource_group_name, 1764 'userAssignedIdentities', 1765 'Microsoft.ManagedIdentity') 1766 1767 1768def process_gallery_image_version_namespace(cmd, namespace): 1769 TargetRegion, EncryptionImages, OSDiskImageEncryption, DataDiskImageEncryption = cmd.get_models( 1770 'TargetRegion', 'EncryptionImages', 'OSDiskImageEncryption', 'DataDiskImageEncryption') 1771 storage_account_types_list = [item.lower() for item in ['Standard_LRS', 'Standard_ZRS', 'Premium_LRS']] 1772 storage_account_types_str = ", ".join(storage_account_types_list) 1773 1774 if namespace.target_regions: 1775 if hasattr(namespace, 'target_region_encryption') and namespace.target_region_encryption: 1776 if len(namespace.target_regions) != len(namespace.target_region_encryption): 1777 raise CLIError( 1778 'usage error: Length of --target-region-encryption should be as same as length of target regions') 1779 regions_info = [] 1780 for i, t in enumerate(namespace.target_regions): 1781 parts = t.split('=', 2) 1782 replica_count = None 1783 storage_account_type = None 1784 1785 # Region specified, but also replica count or storage account type 1786 if len(parts) == 2: 1787 try: 1788 replica_count = int(parts[1]) 1789 except ValueError: 1790 storage_account_type = parts[1] 1791 if parts[1].lower() not in storage_account_types_list: 1792 raise CLIError("usage error: {} is an invalid target region argument. The second part is " 1793 "neither an integer replica count or a valid storage account type. " 1794 "Storage account types must be one of {}." 1795 .format(t, storage_account_types_str)) 1796 1797 # Region specified, but also replica count and storage account type 1798 elif len(parts) == 3: 1799 try: 1800 replica_count = int(parts[1]) # raises ValueError if this is not a replica count, try other order. 1801 storage_account_type = parts[2] 1802 if storage_account_type not in storage_account_types_list: 1803 raise CLIError("usage error: {} is an invalid target region argument. The third part is " 1804 "not a valid storage account type. Storage account types must be one of {}." 1805 .format(t, storage_account_types_str)) 1806 except ValueError: 1807 raise CLIError("usage error: {} is an invalid target region argument. " 1808 "The second part must be a valid integer replica count.".format(t)) 1809 1810 # Parse target region encryption, example: ['des1,0,des2,1,des3', 'null', 'des4'] 1811 encryption = None 1812 if hasattr(namespace, 'target_region_encryption') and namespace.target_region_encryption: 1813 terms = namespace.target_region_encryption[i].split(',') 1814 # OS disk 1815 os_disk_image = terms[0] 1816 if os_disk_image == 'null': 1817 os_disk_image = None 1818 else: 1819 des_id = _disk_encryption_set_format(cmd, namespace, os_disk_image) 1820 os_disk_image = OSDiskImageEncryption(disk_encryption_set_id=des_id) 1821 # Data disk 1822 if len(terms) > 1: 1823 data_disk_images = terms[1:] 1824 data_disk_images_len = len(data_disk_images) 1825 if data_disk_images_len % 2 != 0: 1826 raise CLIError('usage error: LUN and disk encryption set for data disk should appear ' 1827 'in pair in --target-region-encryption. Example: osdes,0,datades0,1,datades1') 1828 data_disk_image_encryption_list = [] 1829 for j in range(int(data_disk_images_len / 2)): 1830 lun = data_disk_images[j * 2] 1831 des_id = data_disk_images[j * 2 + 1] 1832 des_id = _disk_encryption_set_format(cmd, namespace, des_id) 1833 data_disk_image_encryption_list.append(DataDiskImageEncryption( 1834 lun=lun, disk_encryption_set_id=des_id)) 1835 data_disk_images = data_disk_image_encryption_list 1836 else: 1837 data_disk_images = None 1838 encryption = EncryptionImages(os_disk_image=os_disk_image, data_disk_images=data_disk_images) 1839 1840 # At least the region is specified 1841 if len(parts) >= 1: 1842 regions_info.append(TargetRegion(name=parts[0], regional_replica_count=replica_count, 1843 storage_account_type=storage_account_type, 1844 encryption=encryption)) 1845 1846 namespace.target_regions = regions_info 1847 1848 1849def _disk_encryption_set_format(cmd, namespace, name): 1850 """ 1851 Transform name to ID. If it's already a valid ID, do nothing. 1852 :param name: string 1853 :return: ID 1854 """ 1855 from msrestazure.tools import resource_id, is_valid_resource_id 1856 from azure.cli.core.commands.client_factory import get_subscription_id 1857 if name is not None and not is_valid_resource_id(name): 1858 name = resource_id( 1859 subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name, 1860 namespace='Microsoft.Compute', type='diskEncryptionSets', name=name) 1861 return name 1862# endregion 1863 1864 1865def process_vm_vmss_stop(cmd, namespace): # pylint: disable=unused-argument 1866 if "vmss" in cmd.name: 1867 logger.warning("About to power off the VMSS instances...\nThey will continue to be billed. " 1868 "To deallocate VMSS instances, run: az vmss deallocate.") 1869 else: 1870 logger.warning("About to power off the specified VM...\nIt will continue to be billed. " 1871 "To deallocate a VM, run: az vm deallocate.") 1872 1873 1874def _validate_vmss_update_terminate_notification_related(cmd, namespace): # pylint: disable=unused-argument 1875 """ 1876 Validate vmss update enable_terminate_notification and terminate_notification_time. 1877 If terminate_notification_time is specified, enable_terminate_notification should not be false 1878 If enable_terminate_notification is true, must specify terminate_notification_time 1879 """ 1880 if namespace.enable_terminate_notification is False and namespace.terminate_notification_time is not None: 1881 raise CLIError("usage error: please enable --enable-terminate-notification") 1882 if namespace.enable_terminate_notification is True and namespace.terminate_notification_time is None: 1883 raise CLIError("usage error: please set --terminate-notification-time") 1884 _validate_vmss_terminate_notification(cmd, namespace) 1885 1886 1887def _validate_vmss_terminate_notification(cmd, namespace): # pylint: disable=unused-argument 1888 """ 1889 Transform minutes to ISO 8601 formmat 1890 """ 1891 if namespace.terminate_notification_time is not None: 1892 namespace.terminate_notification_time = 'PT' + namespace.terminate_notification_time + 'M' 1893 1894 1895def _validate_vmss_create_automatic_repairs(cmd, namespace): # pylint: disable=unused-argument 1896 if namespace.automatic_repairs_grace_period is not None: 1897 if namespace.load_balancer is None or namespace.health_probe is None: 1898 raise CLIError("usage error: --load-balancer and --health-probe are required " 1899 "when creating vmss with automatic repairs") 1900 _validate_vmss_automatic_repairs(cmd, namespace) 1901 1902 1903def _validate_vmss_update_automatic_repairs(cmd, namespace): # pylint: disable=unused-argument 1904 if namespace.enable_automatic_repairs is False and namespace.automatic_repairs_grace_period is not None: 1905 raise CLIError("usage error: please enable --enable-automatic-repairs") 1906 if namespace.enable_automatic_repairs is True and namespace.automatic_repairs_grace_period is None: 1907 raise CLIError("usage error: please set --automatic-repairs-grace-period") 1908 _validate_vmss_automatic_repairs(cmd, namespace) 1909 1910 1911def _validate_vmss_automatic_repairs(cmd, namespace): # pylint: disable=unused-argument 1912 """ 1913 Transform minutes to ISO 8601 formmat 1914 """ 1915 if namespace.automatic_repairs_grace_period is not None: 1916 namespace.automatic_repairs_grace_period = 'PT' + namespace.automatic_repairs_grace_period + 'M' 1917 1918 1919def _validate_vmss_create_host_group(cmd, namespace): 1920 from msrestazure.tools import resource_id, is_valid_resource_id 1921 from azure.cli.core.commands.client_factory import get_subscription_id 1922 if namespace.host_group: 1923 if not is_valid_resource_id(namespace.host_group): 1924 namespace.host_group = resource_id( 1925 subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name, 1926 namespace='Microsoft.Compute', type='hostGroups', name=namespace.host_group 1927 ) 1928 1929 1930def _validate_count(namespace): 1931 if namespace.count < 2 or namespace.count > 250: 1932 raise ValidationError( 1933 '--count should be in [2, 250]. Please make sure your subscription has enough quota of resources') 1934 banned_params = [ 1935 namespace.attach_data_disks, 1936 namespace.attach_os_disk, 1937 namespace.boot_diagnostics_storage, 1938 namespace.computer_name, 1939 namespace.dedicated_host, 1940 namespace.dedicated_host_group, 1941 namespace.nics, 1942 namespace.os_disk_name, 1943 namespace.private_ip_address, 1944 namespace.public_ip_address, 1945 namespace.public_ip_address_dns_name, 1946 namespace.storage_account, 1947 namespace.storage_container_name, 1948 namespace.use_unmanaged_disk, 1949 ] 1950 params_str = [ 1951 '--attach-data-disks', 1952 '--attach-os-disk', 1953 '--boot-diagnostics-storage', 1954 '--computer-name', 1955 '--host', 1956 '--host-group', 1957 '--nics', 1958 '--os-disk-name', 1959 '--private-ip-address', 1960 '--public-ip-address', 1961 '--public-ip-address-dns-name', 1962 '--storage-account', 1963 '--storage-container-name', 1964 '--subnet', 1965 '--use-unmanaged-disk', 1966 '--vnet-name' 1967 ] 1968 if any(param for param in banned_params): 1969 raise ValidationError('When --count is specified, {} are not allowed'.format(', '.join(params_str))) 1970 1971 1972def validate_edge_zone(cmd, namespace): # pylint: disable=unused-argument 1973 if namespace.edge_zone: 1974 namespace.edge_zone = { 1975 'name': namespace.edge_zone, 1976 'type': 'EdgeZone' 1977 } 1978 1979 1980def _validate_capacity_reservation_group(cmd, namespace): 1981 1982 if namespace.capacity_reservation_group and namespace.capacity_reservation_group != 'None': 1983 1984 from msrestazure.tools import is_valid_resource_id, resource_id 1985 from azure.cli.core.commands.client_factory import get_subscription_id 1986 if not is_valid_resource_id(namespace.capacity_reservation_group): 1987 namespace.capacity_reservation_group = resource_id( 1988 subscription=get_subscription_id(cmd.cli_ctx), 1989 resource_group=namespace.resource_group_name, 1990 namespace='Microsoft.Compute', 1991 type='CapacityReservationGroups', 1992 name=namespace.capacity_reservation_group 1993 ) 1994