Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
12 changes: 12 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ def cf_shared_gallery_image_version(cli_ctx, *_):
return cf_vm_cl(cli_ctx).shared_gallery_image_versions


def cf_community_gallery(cli_ctx, *_):
return cf_vm_cl(cli_ctx).community_galleries


def cf_community_gallery_image(cli_ctx, *_):
return cf_vm_cl(cli_ctx).community_gallery_images


def cf_community_gallery_image_version(cli_ctx, *_):
return cf_vm_cl(cli_ctx).community_gallery_image_versions


def cf_capacity_reservation_groups(cli_ctx, *_):
return cf_vm_cl(cli_ctx).capacity_reservation_groups

Expand Down
6 changes: 6 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 @@ -1467,6 +1467,9 @@
- name: Create a VM from shared gallery image (private preview feature, please contact shared image gallery team by email [email protected] to register for preview if you're interested in using this feature).
text: >
az vm create -n MyVm -g MyResourceGroup --image /SharedGalleries/{gallery_unique_name}/Images/{image}/Versions/{version}
- name: Create a VM from community gallery image (private preview feature, please contact community image gallery team by email [email protected] to register for preview if you're interested in using this feature).
text: >
az vm create -n MyVm -g MyResourceGroup --image /CommunityGalleries/{gallery_unique_name}/Images/{image}/Versions/{version}
"""

helps['vm deallocate'] = """
Expand Down Expand Up @@ -2841,6 +2844,9 @@
- name: Create a VMSS from shared gallery image. (private preview feature, please contact shared image gallery team by email [email protected] to register for preview if you're interested in using this feature).
text: >
az vmss create -n MyVmss -g MyResourceGroup --image /SharedGalleries/{gallery_unique_name}/Images/{image}/Versions/{version}
- name: Create a VMSS from community gallery image. (private preview feature, please contact community image gallery team by email [email protected] to register for preview if you're interested in using this feature).
text: >
az vmss create -n MyVmss -g MyResourceGroup --image /CommunityGalleries/{gallery_unique_name}/Images/{image}/Versions/{version}
- name: Create a Windows VMSS with patch mode 'Manual' (Currently patch mode 'AutomaticByPlatform' is not supported during VMSS creation as health extension which is required for 'AutomaticByPlatform' mode cannot be set during VMSS creation).
text: >
az vmss create -n MyVmss -g MyResourceGroup --image Win2019Datacenter --enable-agent --enable-auto-update false --patch-mode Manual --orchestration-mode Flexible
Expand Down
1 change: 1 addition & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ def load_arguments(self, _):
c.argument('assign_identity', nargs='*', arg_group='Managed Service Identity', help="accept system or user assigned identities separated by spaces. Use '[system]' to refer system assigned identity, or a resource id to refer user assigned identity. Check out help for more examples")
c.ignore('aux_subscriptions')
c.argument('edge_zone', edge_zone_type)
c.argument('accept_term', action='store_true', help="Accept the license agreement and privacy statement.")

with self.argument_context(scope, arg_group='Authentication') as c:
c.argument('generate_ssh_keys', action='store_true', help='Generate SSH public and private key files if missing. The keys will be stored in the ~/.ssh directory')
Expand Down
31 changes: 31 additions & 0 deletions src/azure-cli/azure/cli/command_modules/vm/_template_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class StorageProfile(Enum):
ManagedCustomImage = 5
ManagedSpecializedOSDisk = 6
SharedGalleryImage = 7
CommunityGalleryImage = 8


def build_deployment_resource(name, template, dependencies=None):
Expand Down Expand Up @@ -463,6 +464,19 @@ def _build_storage_profile():
"imageReference": {
'sharedGalleryImageId': image_reference
}
},
'CommunityGalleryImage': {
"osDisk": {
"caching": os_caching,
"managedDisk": {
"storageAccountType": disk_info['os'].get('storageAccountType'),
},
"name": os_disk_name,
"createOption": "fromImage"
},
"imageReference": {
'communityGalleryImageId': image_reference
}
}
}
if os_disk_encryption_set is not None:
Expand All @@ -475,6 +489,9 @@ def _build_storage_profile():
storage_profiles['SharedGalleryImage']['osDisk']['managedDisk']['diskEncryptionSet'] = {
'id': os_disk_encryption_set,
}
storage_profiles['CommunityGalleryImage']['osDisk']['managedDisk']['diskEncryptionSet'] = {
'id': os_disk_encryption_set,
}

profile = storage_profiles[storage_profile.name]
if os_disk_size_gb:
Expand Down Expand Up @@ -958,6 +975,20 @@ def build_vmss_resource(cmd, name, computer_name_prefix, location, tags, overpro
storage_properties['osDisk']['managedDisk']['diskEncryptionSet'] = {
'id': os_disk_encryption_set
}
if storage_profile == StorageProfile.CommunityGalleryImage:
storage_properties['osDisk'] = {
'caching': os_caching,
'managedDisk': {'storageAccountType': disk_info['os'].get('storageAccountType')},
"name": os_disk_name,
"createOption": "fromImage"
}
storage_properties['imageReference'] = {
'communityGalleryImageId': image
}
if os_disk_encryption_set is not None:
storage_properties['osDisk']['managedDisk']['diskEncryptionSet'] = {
'id': os_disk_encryption_set
}

if disk_info:
data_disks = [v for k, v in disk_info.items() if k != 'os']
Expand Down
91 changes: 75 additions & 16 deletions src/azure-cli/azure/cli/command_modules/vm/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from knack.util import CLIError

from azure.cli.core.azclierror import (ValidationError, ArgumentUsageError, RequiredArgumentMissingError,
MutuallyExclusiveArgumentError)
MutuallyExclusiveArgumentError, CLIInternalError)
from azure.cli.core.commands.validators import (
get_default_location_from_resource_group, validate_file_or_dict, validate_parameter_set, validate_tags)
from azure.cli.core.util import (hash_string, DISALLOWED_USER_NAMES, get_default_admin_username)
Expand Down Expand Up @@ -235,6 +235,7 @@ def _validate_secrets(secrets, os_type):
# region VM Create Validators


# pylint: disable=too-many-return-statements
def _parse_image_argument(cmd, namespace):
""" Systematically determines what type is supplied for the --image parameter. Updates the
namespace and returns the type for subsequent processing. """
Expand All @@ -246,10 +247,13 @@ def _parse_image_argument(cmd, namespace):
if is_valid_resource_id(namespace.image):
return 'image_id'

from ._vm_utils import is_shared_gallery_image_id
from ._vm_utils import is_shared_gallery_image_id, is_community_gallery_image_id
if is_shared_gallery_image_id(namespace.image):
return 'shared_gallery_image_id'

if is_community_gallery_image_id(namespace.image):
return 'community_gallery_image_id'

# 2 - attempt to match an URN pattern
urn_match = re.match('([^:]*):([^:]*):([^:]*):([^:]*)', namespace.image)
if urn_match:
Expand Down Expand Up @@ -351,6 +355,8 @@ def _get_storage_profile_description(profile):
return 'attach existing managed OS disk'
if profile == StorageProfile.SharedGalleryImage:
return 'create OS disk from shared gallery image'
if profile == StorageProfile.CommunityGalleryImage:
return 'create OS disk from community gallery image'


def _validate_location(cmd, namespace, zone_info, size_info):
Expand All @@ -368,7 +374,7 @@ def _validate_location(cmd, namespace, zone_info, size_info):
"used to find such locations".format(namespace.resource_group_name))


# pylint: disable=too-many-branches, too-many-statements
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False):
from msrestazure.tools import parse_resource_id

Expand Down Expand Up @@ -396,6 +402,8 @@ def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False):
namespace.storage_profile = StorageProfile.ManagedCustomImage
elif image_type == 'shared_gallery_image_id':
namespace.storage_profile = StorageProfile.SharedGalleryImage
elif image_type == 'community_gallery_image_id':
namespace.storage_profile = StorageProfile.CommunityGalleryImage
elif image_type == 'urn':
if namespace.use_unmanaged_disk:
# STORAGE PROFILE #1
Expand Down Expand Up @@ -432,6 +440,10 @@ def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False):
required = ['image']
forbidden = ['attach_os_disk', 'storage_account', 'storage_container_name', 'use_unmanaged_disk']

elif namespace.storage_profile == StorageProfile.CommunityGalleryImage:
required = ['image']
forbidden = ['attach_os_disk', 'storage_account', 'storage_container_name', 'use_unmanaged_disk']

elif namespace.storage_profile == StorageProfile.ManagedSpecializedOSDisk:
required = ['os_type', 'attach_os_disk']
forbidden = ['os_disk_name', 'os_caching', 'storage_account', 'ephemeral_os_disk',
Expand Down Expand Up @@ -526,23 +538,45 @@ def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False):
namespace.attach_data_disks = [_get_resource_id(cmd.cli_ctx, d, namespace.resource_group_name, 'disks',
'Microsoft.Compute') for d in namespace.attach_data_disks]

if not namespace.os_type:
if namespace.storage_profile == StorageProfile.SharedGalleryImage:
if namespace.storage_profile == StorageProfile.SharedGalleryImage:

if namespace.location is None:
raise RequiredArgumentMissingError(
'Please input the location of the shared gallery image through the parameter --location.')
if namespace.location is None:
raise RequiredArgumentMissingError(
'Please input the location of the shared gallery image through the parameter --location.')

from ._vm_utils import parse_shared_gallery_image_id
image_info = parse_shared_gallery_image_id(namespace.image)
from ._vm_utils import parse_shared_gallery_image_id
image_info = parse_shared_gallery_image_id(namespace.image)

from ._client_factory import cf_shared_gallery_image
shared_gallery_image_info = cf_shared_gallery_image(cmd.cli_ctx).get(
location=namespace.location, gallery_unique_name=image_info[0], gallery_image_name=image_info[1])
namespace.os_type = shared_gallery_image_info.os_type
from ._client_factory import cf_shared_gallery_image
shared_gallery_image_info = cf_shared_gallery_image(cmd.cli_ctx).get(
location=namespace.location, gallery_unique_name=image_info[0], gallery_image_name=image_info[1])

else:
namespace.os_type = 'windows' if 'windows' in namespace.os_offer.lower() else 'linux'
if namespace.os_type and namespace.os_type.lower() != shared_gallery_image_info.os_type.lower():
raise ArgumentUsageError("The --os-type is not the correct os type of this shared gallery image, "
"the os type of this image should be {}".format(shared_gallery_image_info.os_type))
namespace.os_type = shared_gallery_image_info.os_type

if namespace.storage_profile == StorageProfile.CommunityGalleryImage:

if namespace.location is None:
raise RequiredArgumentMissingError(
'Please input the location of the community gallery image through the parameter --location.')

from ._vm_utils import parse_community_gallery_image_id
image_info = parse_community_gallery_image_id(namespace.image)

from ._client_factory import cf_community_gallery_image
community_gallery_image_info = cf_community_gallery_image(cmd.cli_ctx).get(
location=namespace.location, public_gallery_name=image_info[0], gallery_image_name=image_info[1])

if namespace.os_type and namespace.os_type.lower() != community_gallery_image_info.os_type.lower():
raise ArgumentUsageError(
"The --os-type is not the correct os type of this community gallery image, "
"the os type of this image should be {}".format(community_gallery_image_info.os_type))
namespace.os_type = community_gallery_image_info.os_type

if not namespace.os_type:
namespace.os_type = 'windows' if 'windows' in namespace.os_offer.lower() else 'linux'

from ._vm_utils import normalize_disk_info
# attach_data_disks are not exposed yet for VMSS, so use 'getattr' to avoid crash
Expand Down Expand Up @@ -1320,6 +1354,7 @@ def process_vm_create_namespace(cmd, namespace):

_validate_capacity_reservation_group(cmd, namespace)
_validate_vm_nic_delete_option(namespace)
_validate_community_gallery_legal_agreement_acceptance(cmd, namespace)

# endregion

Expand Down Expand Up @@ -1617,6 +1652,7 @@ def process_vmss_create_namespace(cmd, namespace):
raise ArgumentUsageError('usage error: --priority PRIORITY [--eviction-policy POLICY]')

_validate_capacity_reservation_group(cmd, namespace)
_validate_community_gallery_legal_agreement_acceptance(cmd, namespace)


def validate_vmss_update_namespace(cmd, namespace): # pylint: disable=unused-argument
Expand Down Expand Up @@ -2060,3 +2096,26 @@ def _validate_vm_vmss_update_ephemeral_placement(cmd, namespace): # pylint: dis
if source == 'vmss' and not vm_sku:
raise ArgumentUsageError('usage error: --ephemeral-os-disk-placement is only configurable when '
'--vm-sku is specified.')


def _validate_community_gallery_legal_agreement_acceptance(cmd, namespace):
from ._vm_utils import is_community_gallery_image_id, parse_community_gallery_image_id
if not is_community_gallery_image_id(namespace.image) or namespace.accept_term:
return

community_gallery_name, _ = parse_community_gallery_image_id(namespace.image)
from ._client_factory import cf_community_gallery
try:
community_gallery_info = cf_community_gallery(cmd.cli_ctx).get(namespace.location, community_gallery_name)
eula = community_gallery_info.additional_properties['communityMetadata']['eula']
except Exception as err:
raise CLIInternalError('Get the eula from community gallery failed: {0}'.format(err))

from knack.prompting import prompt_y_n
msg = "To create the VM/VMSS from community gallery image, you must accept the license agreement and " \
"privacy statement: {}. (If you want to accept the legal terms by default, " \
"please use the option '--accept-term' when creating VM/VMSS)".format(eula)

if not prompt_y_n(msg, default="y"):
import sys
sys.exit(0)
28 changes: 28 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 @@ -397,6 +397,34 @@ def parse_shared_gallery_image_id(image_reference):
return image_info.group(1), image_info.group(2)


def is_community_gallery_image_id(image_reference):
if not image_reference:
return False

community_gallery_id_pattern = re.compile(r'^/CommunityGalleries/[^/]*/Images/[^/]*/Versions/.*$', re.IGNORECASE)
if community_gallery_id_pattern.match(image_reference):
return True

return False


def parse_community_gallery_image_id(image_reference):
from azure.cli.core.azclierror import InvalidArgumentValueError

if not image_reference:
raise InvalidArgumentValueError(
'Please pass in the community gallery image id through the parameter --image')

image_info = re.search(r'^/CommunityGalleries/([^/]*)/Images/([^/]*)/Versions/.*$', image_reference, re.IGNORECASE)
if not image_info or len(image_info.groups()) < 2:
Comment on lines +418 to +419
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check image version ?

Suggested change
image_info = re.search(r'^/CommunityGalleries/([^/]*)/Images/([^/]*)/Versions/.*$', image_reference, re.IGNORECASE)
if not image_info or len(image_info.groups()) < 2:
image_info = re.search(r'^/CommunityGalleries/([^/]*)/Images/([^/]*)/Versions/(.*)$', image_reference, re.IGNORECASE)
if not image_info or len(image_info.groups()) < 3:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At present, we need to parse the public_gallery_name and gallery_image_name from community gallery image id to query the community gallery image info, but version is not used for the time being, so we do not further match version.

from ._vm_utils import parse_community_gallery_image_id
image_info = parse_community_gallery_image_id(namespace.image)
from ._client_factory import cf_community_gallery_image
community_gallery_image_info = cf_community_gallery_image(cmd.cli_ctx).get(
location=namespace.location, public_gallery_name=image_info[0], gallery_image_name=image_info[1])

I think we can add version matching when we need to parse the version. What do you think?

raise InvalidArgumentValueError(
'The community gallery image id is invalid. The valid format should be '
'"/CommunityGalleries/{gallery_unique_name}/Images/{gallery_image_name}/Versions/{image_version}"')

# Return the gallery unique name and gallery image name parsed from community gallery image id
return image_info.group(1), image_info.group(2)


class ArmTemplateBuilder20190401(ArmTemplateBuilder):

def __init__(self):
Expand Down
4 changes: 2 additions & 2 deletions src/azure-cli/azure/cli/command_modules/vm/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
enable_hotpatching=None, platform_fault_domain=None, security_type=None, enable_secure_boot=None,
enable_vtpm=None, count=None, edge_zone=None, nic_delete_option=None, os_disk_delete_option=None,
data_disk_delete_option=None, user_data=None, capacity_reservation_group=None, enable_hibernation=None,
v_cpus_available=None, v_cpus_per_core=None):
v_cpus_available=None, v_cpus_per_core=None, accept_term=None):

from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
Expand Down Expand Up @@ -2846,7 +2846,7 @@ def create_vmss(cmd, vmss_name, resource_group_name, image=None,
user_data=None, network_api_version=None, enable_spot_restore=None, spot_restore_timeout=None,
capacity_reservation_group=None, enable_auto_update=None, patch_mode=None, enable_agent=None,
security_type=None, enable_secure_boot=None, enable_vtpm=None, automatic_repairs_action=None,
v_cpus_available=None, v_cpus_per_core=None):
v_cpus_available=None, v_cpus_per_core=None, accept_term=None):

from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
Expand Down
Loading