932 lines
34 KiB
Python
932 lines
34 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2014 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Command for creating disks."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import annotations
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import argparse
|
|
import re
|
|
import textwrap
|
|
from typing import Any, Optional
|
|
|
|
from googlecloudsdk.api_lib.compute import base_classes
|
|
from googlecloudsdk.api_lib.compute import constants
|
|
from googlecloudsdk.api_lib.compute import csek_utils
|
|
from googlecloudsdk.api_lib.compute import disks_util
|
|
from googlecloudsdk.api_lib.compute import image_utils
|
|
from googlecloudsdk.api_lib.compute import kms_utils
|
|
from googlecloudsdk.api_lib.compute import utils
|
|
from googlecloudsdk.api_lib.compute import zone_utils
|
|
from googlecloudsdk.api_lib.compute.regions import utils as region_utils
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions
|
|
from googlecloudsdk.command_lib.compute import completers
|
|
from googlecloudsdk.command_lib.compute import flags
|
|
from googlecloudsdk.command_lib.compute import scope as compute_scope
|
|
from googlecloudsdk.command_lib.compute.disks import create
|
|
from googlecloudsdk.command_lib.compute.disks import flags as disks_flags
|
|
from googlecloudsdk.command_lib.compute.resource_policies import flags as resource_flags
|
|
from googlecloudsdk.command_lib.compute.resource_policies import util as resource_util
|
|
from googlecloudsdk.command_lib.kms import resource_args as kms_resource_args
|
|
from googlecloudsdk.command_lib.util.apis import arg_utils
|
|
from googlecloudsdk.command_lib.util.args import labels_util
|
|
from googlecloudsdk.core import log
|
|
import six
|
|
|
|
DETAILED_HELP = {
|
|
'brief':
|
|
'Create Compute Engine persistent disks',
|
|
'DESCRIPTION':
|
|
"""\
|
|
*{command}* creates one or more Compute Engine
|
|
persistent disks. When creating virtual machine instances,
|
|
disks can be attached to the instances through the
|
|
`gcloud compute instances create` command. Disks can also be
|
|
attached to instances that are already running using
|
|
`gcloud compute instances attach-disk`.
|
|
|
|
Disks are zonal resources, so they reside in a particular zone
|
|
for their entire lifetime. The contents of a disk can be moved
|
|
to a different zone by snapshotting the disk (using
|
|
`gcloud compute disks snapshot`) and creating a new disk using
|
|
`--source-snapshot` in the desired zone. The contents of a
|
|
disk can also be moved across project or zone by creating an
|
|
image (using `gcloud compute images create`) and creating a
|
|
new disk using `--image` in the desired project and/or
|
|
zone.
|
|
|
|
For a comprehensive guide, including details on minimum and maximum
|
|
disk size, refer to:
|
|
https://cloud.google.com/compute/docs/disks
|
|
""",
|
|
'EXAMPLES':
|
|
"""\
|
|
When creating disks, be sure to include the `--zone` option. To create
|
|
disks 'my-disk-1' and 'my-disk-2' in zone us-east1-a:
|
|
|
|
$ {command} my-disk-1 my-disk-2 --zone=us-east1-a
|
|
""",
|
|
}
|
|
|
|
|
|
def _SourceArgs(
|
|
parser,
|
|
support_source_snapshot_region,
|
|
source_instant_snapshot_enabled=False,
|
|
):
|
|
"""Add mutually exclusive source args."""
|
|
source_parent_group = parser.add_group()
|
|
source_group = source_parent_group.add_mutually_exclusive_group()
|
|
|
|
def AddImageHelp():
|
|
"""Returns detailed help for `--image` argument."""
|
|
template = """\
|
|
An image to apply to the disks being created. When using
|
|
this option, the size of the disks must be at least as large as
|
|
the image size. Use ``--size'' to adjust the size of the disks.
|
|
|
|
This flag is mutually exclusive with ``--source-snapshot'' and
|
|
``--image-family''.
|
|
"""
|
|
return template
|
|
|
|
source_group.add_argument('--image', help=AddImageHelp)
|
|
|
|
image_utils.AddImageProjectFlag(source_parent_group)
|
|
|
|
source_group.add_argument(
|
|
'--image-family',
|
|
help="""\
|
|
The image family for the operating system that the boot disk will be
|
|
initialized with. Compute Engine offers multiple Linux
|
|
distributions, some of which are available as both regular and
|
|
Shielded VM images. When a family is specified instead of an image,
|
|
the latest non-deprecated image associated with that family is
|
|
used. It is best practice to use --image-family when the latest
|
|
version of an image is needed.
|
|
""")
|
|
image_utils.AddImageFamilyScopeFlag(source_parent_group)
|
|
if support_source_snapshot_region:
|
|
disks_flags.SOURCE_SNAPSHOT_ARG_ALPHA.AddArgument(
|
|
parser, mutex_group=source_group
|
|
)
|
|
else:
|
|
disks_flags.SOURCE_SNAPSHOT_ARG.AddArgument(source_group)
|
|
if source_instant_snapshot_enabled:
|
|
disks_flags.AddSourceInstantSnapshotProject(parser)
|
|
|
|
disks_flags.SOURCE_INSTANT_SNAPSHOT_ARG.AddArgument(source_group)
|
|
disks_flags.SOURCE_DISK_ARG.AddArgument(parser, mutex_group=source_group)
|
|
disks_flags.ASYNC_PRIMARY_DISK_ARG.AddArgument(
|
|
parser, mutex_group=source_group
|
|
)
|
|
disks_flags.AddPrimaryDiskProject(parser)
|
|
disks_flags.AddLocationHintArg(parser)
|
|
|
|
|
|
def _CommonArgs(
|
|
messages,
|
|
parser,
|
|
include_physical_block_size_support=False,
|
|
vss_erase_enabled=False,
|
|
support_pd_interface=False,
|
|
support_user_licenses=False,
|
|
support_source_snapshot_region=False,
|
|
support_gmi_restore=False,
|
|
source_instant_snapshot_enabled=False,
|
|
):
|
|
"""Add arguments used for parsing in all command tracks."""
|
|
Create.disks_arg.AddArgument(parser, operation_type='create')
|
|
parser.add_argument(
|
|
'--description',
|
|
help='An optional, textual description for the disks being created.')
|
|
|
|
parser.add_argument(
|
|
'--size',
|
|
type=arg_parsers.BinarySize(
|
|
lower_bound='1GB',
|
|
suggested_binary_size_scales=['GB', 'GiB', 'TB', 'TiB', 'PiB', 'PB']),
|
|
help="""\
|
|
Size of the disks. The value must be a whole
|
|
number followed by a size unit of ``GB'' for gigabyte, or ``TB''
|
|
for terabyte. If no size unit is specified, GB is
|
|
assumed. For example, ``10GB'' will produce 10 gigabyte
|
|
disks. Disk size must be a multiple of 1 GB. If disk size is
|
|
not specified, the default size of {}GB for pd-standard disks, {}GB for
|
|
pd-balanced disks, {}GB for pd-ssd disks, and {}GB for pd-extreme will
|
|
be used. For details about disk size limits, refer to:
|
|
https://cloud.google.com/compute/docs/disks
|
|
""".format(
|
|
constants.DEFAULT_DISK_SIZE_GB_MAP[constants.DISK_TYPE_PD_STANDARD],
|
|
constants.DEFAULT_DISK_SIZE_GB_MAP[constants.DISK_TYPE_PD_BALANCED],
|
|
constants.DEFAULT_DISK_SIZE_GB_MAP[constants.DISK_TYPE_PD_SSD],
|
|
constants.DEFAULT_DISK_SIZE_GB_MAP[constants.DISK_TYPE_PD_EXTREME]))
|
|
|
|
parser.add_argument(
|
|
'--type',
|
|
completer=completers.DiskTypesCompleter,
|
|
help="""\
|
|
Specifies the type of disk to create. To get a
|
|
list of available disk types, run `gcloud compute disk-types list`.
|
|
The default disk type is pd-standard.
|
|
""")
|
|
|
|
if support_pd_interface:
|
|
parser.add_argument(
|
|
'--interface',
|
|
help="""\
|
|
Specifies the disk interface to use for attaching this disk. Valid values
|
|
are `SCSI` and `NVME`. The default is `SCSI`.
|
|
""")
|
|
|
|
parser.display_info.AddFormat(
|
|
'table(name, zone.basename(), sizeGb, type.basename(), status)')
|
|
|
|
parser.add_argument(
|
|
'--licenses',
|
|
type=arg_parsers.ArgList(),
|
|
metavar='LICENSE',
|
|
help=(
|
|
'A list of URIs to license resources. The provided licenses will '
|
|
'be added onto the created disks to indicate the licensing and '
|
|
'billing policies.'
|
|
),
|
|
)
|
|
|
|
_SourceArgs(
|
|
parser, support_source_snapshot_region, source_instant_snapshot_enabled
|
|
)
|
|
|
|
disks_flags.AddProvisionedIopsFlag(parser, arg_parsers)
|
|
disks_flags.AddArchitectureFlag(parser, messages)
|
|
|
|
disks_flags.AddProvisionedThroughputFlag(parser, arg_parsers)
|
|
|
|
disks_flags.STORAGE_POOL_ARG.AddArgument(parser)
|
|
|
|
disks_flags.AddAccessModeFlag(parser, messages)
|
|
|
|
if support_gmi_restore:
|
|
disks_flags.AddSourceMachineImageNameArg(parser)
|
|
disks_flags.AddSourceMachineImageDiskDeviceNameArg(parser)
|
|
|
|
if support_user_licenses:
|
|
parser.add_argument(
|
|
'--user-licenses',
|
|
type=arg_parsers.ArgList(),
|
|
metavar='LICENSE',
|
|
help=('List of URIs to license resources. User-provided licenses '
|
|
'can be edited after disk is created.'))
|
|
|
|
csek_utils.AddCsekKeyArgs(parser)
|
|
labels_util.AddCreateLabelsFlags(parser)
|
|
|
|
if include_physical_block_size_support:
|
|
parser.add_argument(
|
|
'--physical-block-size',
|
|
choices=['4096', '16384'],
|
|
default='4096',
|
|
help="""\
|
|
Physical block size of the persistent disk in bytes.
|
|
Valid values are 4096(default) and 16384.
|
|
""")
|
|
if vss_erase_enabled:
|
|
flags.AddEraseVssSignature(parser, resource='a source snapshot')
|
|
|
|
resource_flags.AddResourcePoliciesArgs(parser, 'added to', 'disk')
|
|
|
|
|
|
def _AddReplicaZonesArg(parser):
|
|
parser.add_argument(
|
|
'--replica-zones',
|
|
type=arg_parsers.ArgList(min_length=2, max_length=2),
|
|
metavar='ZONE',
|
|
help=('A comma-separated list of exactly 2 zones that a regional disk '
|
|
'will be replicated to. Required when creating regional disk. '
|
|
'The zones must be in the same region as specified in the '
|
|
'`--region` flag. See available zones with '
|
|
'`gcloud compute zones list`.'))
|
|
|
|
|
|
def _ParseGuestOsFeaturesToMessages(args, client_messages):
|
|
"""Parse GuestOS features."""
|
|
guest_os_feature_messages = []
|
|
if args.guest_os_features:
|
|
for feature in args.guest_os_features:
|
|
gf_type = client_messages.GuestOsFeature.TypeValueValuesEnum(feature)
|
|
guest_os_feature = client_messages.GuestOsFeature()
|
|
guest_os_feature.type = gf_type
|
|
guest_os_feature_messages.append(guest_os_feature)
|
|
|
|
return guest_os_feature_messages
|
|
|
|
|
|
def _GetSourceInstantSnapshotProjectFromPath(
|
|
source_instant_snapshot: Optional[str],
|
|
) -> Optional[str]:
|
|
"""Gets the source instant-snapshot project from the path."""
|
|
if not source_instant_snapshot:
|
|
return None
|
|
match = re.search(r'projects/([^/]+)', source_instant_snapshot)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def _GetInstantSnapshotReference(
|
|
args: argparse.Namespace, compute_holder: Any, source_project: Optional[str]
|
|
) -> Optional[str]:
|
|
"""Resolves the instant snapshot reference to a URI."""
|
|
instant_snapshot_ref = (
|
|
disks_flags.SOURCE_INSTANT_SNAPSHOT_ARG.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
source_project=source_project,
|
|
)
|
|
)
|
|
return instant_snapshot_ref.SelfLink() if instant_snapshot_ref else None
|
|
|
|
|
|
def _GetSourceInstantSnapshotUriWithSourceProjectSpecified(
|
|
args: argparse.Namespace, compute_holder: Any
|
|
) -> Optional[str]:
|
|
"""Gets the URI when source_instant_snapshot_project is specified."""
|
|
actual_source_project = getattr(args, 'source_instant_snapshot_project', None)
|
|
expected_source_project = _GetSourceInstantSnapshotProjectFromPath(
|
|
args.source_instant_snapshot
|
|
)
|
|
# Checks if source projects match.
|
|
if (
|
|
expected_source_project
|
|
and actual_source_project != expected_source_project
|
|
):
|
|
# Throw an error here
|
|
raise exceptions.BadArgumentException(
|
|
'--source_instant_snapshot_project',
|
|
'The project specified in --source-instant-snapshot-project does'
|
|
' not match the project in the --source-instant-snapshot URI.'
|
|
' Please ensure these values are consistent.',
|
|
)
|
|
|
|
elif (
|
|
expected_source_project
|
|
and actual_source_project == expected_source_project
|
|
):
|
|
return _GetInstantSnapshotReference(
|
|
args, compute_holder, actual_source_project
|
|
)
|
|
elif not expected_source_project:
|
|
return _GetInstantSnapshotReference(
|
|
args, compute_holder, source_project=actual_source_project
|
|
)
|
|
return None
|
|
|
|
|
|
def _GetSourceInstantSnapshotUri(
|
|
args: argparse.Namespace, compute_holder: Any
|
|
) -> Optional[str]:
|
|
"""Determines the source instant snapshot URI."""
|
|
if args.source_instant_snapshot:
|
|
# Check if source_instant_snapshot_project is not specified
|
|
if not getattr(args, 'source_instant_snapshot_project', None):
|
|
return _GetInstantSnapshotReference(args, compute_holder, None)
|
|
return _GetSourceInstantSnapshotUriWithSourceProjectSpecified(
|
|
args, compute_holder
|
|
)
|
|
return None
|
|
|
|
|
|
@base.DefaultUniverseOnly
|
|
@base.ReleaseTracks(base.ReleaseTrack.GA)
|
|
class Create(base.Command):
|
|
"""Create Compute Engine persistent disks."""
|
|
|
|
@classmethod
|
|
def Args(cls, parser):
|
|
messages = cls._GetApiHolder(no_http=True).client.messages
|
|
Create.disks_arg = disks_flags.MakeDiskArg(plural=True)
|
|
_CommonArgs(messages, parser)
|
|
image_utils.AddGuestOsFeaturesArg(parser, messages)
|
|
_AddReplicaZonesArg(parser)
|
|
kms_resource_args.AddKmsKeyResourceArg(
|
|
parser, 'disk', region_fallthrough=True)
|
|
disks_flags.AddEnableConfidentialComputeFlag(parser)
|
|
|
|
def ParseLicenses(self, args):
|
|
"""Parse license.
|
|
|
|
Subclasses may override it to customize parsing.
|
|
|
|
Args:
|
|
args: The argument namespace
|
|
|
|
Returns:
|
|
List of licenses.
|
|
"""
|
|
if args.licenses:
|
|
return args.licenses
|
|
return []
|
|
|
|
def ValidateAndParseDiskRefs(self, args, compute_holder):
|
|
return _ValidateAndParseDiskRefsRegionalReplica(args, compute_holder)
|
|
|
|
def GetFromImage(self, args):
|
|
return args.image or args.image_family
|
|
|
|
def GetFromSourceInstantSnapshot(self, args):
|
|
return args.source_instant_snapshot
|
|
|
|
def GetFromSourceMachineImage(self, args, support_gmi_restore):
|
|
if support_gmi_restore:
|
|
return getattr(args, 'source_machine_image', None)
|
|
return None
|
|
|
|
def GetDiskSizeGb(self, args, from_image, support_gmi_restore):
|
|
size_gb = utils.BytesToGb(args.size)
|
|
|
|
if size_gb:
|
|
# Legacy disk type cannot be smaller than 10 GB and it is enforced in
|
|
# gcloud.
|
|
if args.type in constants.LEGACY_DISK_TYPE_LIST and size_gb < 10:
|
|
raise exceptions.InvalidArgumentException(
|
|
'--size',
|
|
'Value must be greater than or equal to 10 GB; reveived {0} GB'
|
|
.format(size_gb),
|
|
)
|
|
# if disk size is given, use it.
|
|
pass
|
|
elif (
|
|
args.source_snapshot
|
|
or from_image
|
|
or args.source_disk
|
|
or self.GetFromSourceInstantSnapshot(args)
|
|
or self.GetFromSourceMachineImage(args, support_gmi_restore)
|
|
):
|
|
# if source is a snapshot/image/disk/instant-snapshot, it is ok not to
|
|
# set size_gb since disk size can be obtained from the source.
|
|
pass
|
|
elif args.type in constants.DEFAULT_DISK_SIZE_GB_MAP:
|
|
# Get default disk size from disk_type.
|
|
size_gb = constants.DEFAULT_DISK_SIZE_GB_MAP[args.type]
|
|
elif args.type:
|
|
# If disk type is specified, then leaves it to backend to decide the size.
|
|
pass
|
|
else:
|
|
# If disk type is unspecified or unknown, we use the default size of
|
|
# pd-standard.
|
|
size_gb = constants.DEFAULT_DISK_SIZE_GB_MAP[
|
|
constants.DISK_TYPE_PD_STANDARD]
|
|
utils.WarnIfDiskSizeIsTooSmall(size_gb, args.type)
|
|
return size_gb
|
|
|
|
def GetProjectToSourceImageDict(self, args, disk_refs, compute_holder,
|
|
from_image):
|
|
project_to_source_image = {}
|
|
|
|
image_expander = image_utils.ImageExpander(compute_holder.client,
|
|
compute_holder.resources)
|
|
|
|
for disk_ref in disk_refs:
|
|
if from_image:
|
|
if disk_ref.project not in project_to_source_image:
|
|
source_image_uri, _ = image_expander.ExpandImageFlag(
|
|
user_project=disk_ref.project,
|
|
image=args.image,
|
|
image_family=args.image_family,
|
|
image_project=args.image_project,
|
|
return_image_resource=False,
|
|
image_family_scope=args.image_family_scope,
|
|
support_image_family_scope=True)
|
|
project_to_source_image[disk_ref.project] = argparse.Namespace()
|
|
project_to_source_image[disk_ref.project].uri = source_image_uri
|
|
else:
|
|
project_to_source_image[disk_ref.project] = argparse.Namespace()
|
|
project_to_source_image[disk_ref.project].uri = None
|
|
return project_to_source_image
|
|
|
|
def WarnAboutScopeDeprecationsAndMaintenance(self, disk_refs, client):
|
|
# Check if the zone is deprecated or has maintenance coming.
|
|
zone_resource_fetcher = zone_utils.ZoneResourceFetcher(client)
|
|
zone_resource_fetcher.WarnForZonalCreation(
|
|
(ref for ref in disk_refs if ref.Collection() == 'compute.disks'))
|
|
# Check if the region is deprecated or has maintenance coming.
|
|
region_resource_fetcher = region_utils.RegionResourceFetcher(client)
|
|
region_resource_fetcher.WarnForRegionalCreation(
|
|
(ref for ref in disk_refs if ref.Collection() == 'compute.regionDisks'))
|
|
|
|
def GetSnapshotUri(
|
|
self, args, compute_holder, support_source_snapshot_region
|
|
):
|
|
if not support_source_snapshot_region:
|
|
snapshot_ref = disks_flags.SOURCE_SNAPSHOT_ARG.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
)
|
|
else:
|
|
snapshot_ref = disks_flags.SOURCE_SNAPSHOT_ARG_ALPHA.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
scope_lister=flags.GetDefaultScopeLister(compute_holder.client),
|
|
default_scope=compute_scope.ScopeEnum.GLOBAL,
|
|
)
|
|
if snapshot_ref:
|
|
return snapshot_ref.SelfLink()
|
|
return None
|
|
|
|
def GetSourceDiskUri(self, args, disk_ref, compute_holder):
|
|
source_disk_ref = None
|
|
if args.source_disk:
|
|
if args.source_disk_zone:
|
|
source_disk_ref = disks_flags.SOURCE_DISK_ARG.ResolveAsResource(
|
|
args, compute_holder.resources)
|
|
else:
|
|
if disk_ref.Collection() == 'compute.disks':
|
|
source_disk_ref = disks_flags.SOURCE_DISK_ARG.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
default_scope=compute_scope.ScopeEnum.ZONE)
|
|
elif disk_ref.Collection() == 'compute.regionDisks':
|
|
source_disk_ref = disks_flags.SOURCE_DISK_ARG.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
default_scope=compute_scope.ScopeEnum.REGION)
|
|
if source_disk_ref:
|
|
return source_disk_ref.SelfLink()
|
|
return None
|
|
|
|
def GetAsyncPrimaryDiskUri(self, args, compute_holder):
|
|
primary_disk_ref = None
|
|
if args.primary_disk:
|
|
primary_disk_project = getattr(args, 'primary_disk_project', None)
|
|
primary_disk_ref = disks_flags.ASYNC_PRIMARY_DISK_ARG.ResolveAsResource(
|
|
args, compute_holder.resources, source_project=primary_disk_project
|
|
)
|
|
if primary_disk_ref:
|
|
return primary_disk_ref.SelfLink()
|
|
return None
|
|
|
|
def GetStoragePoolUri(self, args, compute_holder):
|
|
if args.storage_pool:
|
|
storage_pool_ref = disks_flags.STORAGE_POOL_ARG.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
default_scope=compute_scope.ScopeEnum.ZONE,
|
|
)
|
|
if storage_pool_ref:
|
|
return storage_pool_ref.SelfLink()
|
|
return None
|
|
|
|
def GetLabels(self, args, client):
|
|
labels = None
|
|
args_labels = getattr(args, 'labels', None)
|
|
if args_labels:
|
|
labels = client.messages.Disk.LabelsValue(additionalProperties=[
|
|
client.messages.Disk.LabelsValue.AdditionalProperty(
|
|
key=key, value=value)
|
|
for key, value in sorted(six.iteritems(args.labels))
|
|
])
|
|
return labels
|
|
|
|
def GetReplicaZones(self, args, compute_holder, disk_ref):
|
|
result = []
|
|
for zone in args.replica_zones:
|
|
zone_ref = compute_holder.resources.Parse(
|
|
zone,
|
|
collection='compute.zones',
|
|
params={'project': disk_ref.project})
|
|
result.append(zone_ref.SelfLink())
|
|
return result
|
|
|
|
@classmethod
|
|
def _GetApiHolder(cls, no_http=False):
|
|
return base_classes.ComputeApiHolder(cls.ReleaseTrack(), no_http)
|
|
|
|
def Run(self, args):
|
|
return self._Run(args, supports_kms_keys=True)
|
|
|
|
def _Run(
|
|
self,
|
|
args,
|
|
supports_kms_keys=False,
|
|
supports_physical_block=False,
|
|
support_multiwriter_disk=False,
|
|
support_vss_erase=False,
|
|
support_pd_interface=False,
|
|
support_user_licenses=False,
|
|
support_enable_confidential_compute=True,
|
|
support_source_snapshot_region=False,
|
|
support_gmi_restore=False,
|
|
):
|
|
compute_holder = self._GetApiHolder()
|
|
client = compute_holder.client
|
|
|
|
self.show_unformated_message = not (
|
|
args.IsSpecified('image')
|
|
or args.IsSpecified('image_family')
|
|
or args.IsSpecified('source_snapshot')
|
|
or args.IsSpecified('source_disk')
|
|
)
|
|
self.show_unformated_message = self.show_unformated_message and not (
|
|
args.IsSpecified('source_instant_snapshot')
|
|
)
|
|
|
|
disk_refs = self.ValidateAndParseDiskRefs(args, compute_holder)
|
|
from_image = self.GetFromImage(args)
|
|
size_gb = self.GetDiskSizeGb(args, from_image, support_gmi_restore)
|
|
self.WarnAboutScopeDeprecationsAndMaintenance(disk_refs, client)
|
|
project_to_source_image = self.GetProjectToSourceImageDict(
|
|
args, disk_refs, compute_holder, from_image
|
|
)
|
|
snapshot_uri = self.GetSnapshotUri(
|
|
args, compute_holder, support_source_snapshot_region
|
|
)
|
|
|
|
labels = self.GetLabels(args, client)
|
|
|
|
csek_keys = csek_utils.CsekKeyStore.FromArgs(args, True)
|
|
|
|
for project in project_to_source_image:
|
|
source_image_uri = project_to_source_image[project].uri
|
|
project_to_source_image[project].keys = (
|
|
csek_utils.MaybeLookupKeyMessagesByUri(
|
|
csek_keys, compute_holder.resources,
|
|
[source_image_uri, snapshot_uri], client.apitools_client))
|
|
|
|
guest_os_feature_messages = _ParseGuestOsFeaturesToMessages(
|
|
args, client.messages)
|
|
|
|
requests = []
|
|
for disk_ref in disk_refs:
|
|
type_uri = disks_util.GetDiskTypeUri(args.type, disk_ref, compute_holder)
|
|
|
|
kwargs = {}
|
|
if csek_keys:
|
|
disk_key_or_none = csek_keys.LookupKey(disk_ref,
|
|
args.require_csek_key_create)
|
|
disk_key_message_or_none = csek_utils.MaybeToMessage(
|
|
disk_key_or_none, client.apitools_client)
|
|
kwargs['diskEncryptionKey'] = disk_key_message_or_none
|
|
kwargs['sourceImageEncryptionKey'] = (
|
|
project_to_source_image[disk_ref.project].keys[0])
|
|
kwargs['sourceSnapshotEncryptionKey'] = (
|
|
project_to_source_image[disk_ref.project].keys[1])
|
|
if labels:
|
|
kwargs['labels'] = labels
|
|
|
|
if supports_kms_keys:
|
|
kwargs['diskEncryptionKey'] = kms_utils.MaybeGetKmsKey(
|
|
args, client.messages, kwargs.get('diskEncryptionKey', None))
|
|
|
|
# Those features are only exposed in alpha/beta, it would be nice to have
|
|
# code supporting them only in alpha and beta versions of the command.
|
|
# TODO(b/65161039): Stop checking release path in the middle of GA code.
|
|
if support_pd_interface and args.interface:
|
|
kwargs['interface'] = arg_utils.ChoiceToEnum(
|
|
args.interface, client.messages.Disk.InterfaceValueValuesEnum)
|
|
# end of alpha/beta features.
|
|
|
|
if args.primary_disk:
|
|
primary_disk = client.messages.DiskAsyncReplication()
|
|
primary_disk.disk = self.GetAsyncPrimaryDiskUri(args, compute_holder)
|
|
kwargs['asyncPrimaryDisk'] = primary_disk
|
|
|
|
if supports_physical_block and args.IsSpecified('physical_block_size'):
|
|
physical_block_size_bytes = int(args.physical_block_size)
|
|
else:
|
|
physical_block_size_bytes = None
|
|
|
|
resource_policies = getattr(args, 'resource_policies', None)
|
|
if resource_policies:
|
|
if disk_ref.Collection() == 'compute.regionDisks':
|
|
disk_region = disk_ref.region
|
|
else:
|
|
disk_region = utils.ZoneNameToRegionName(disk_ref.zone)
|
|
parsed_resource_policies = []
|
|
for policy in resource_policies:
|
|
resource_policy_ref = resource_util.ParseResourcePolicy(
|
|
compute_holder.resources,
|
|
policy,
|
|
project=disk_ref.project,
|
|
region=disk_region)
|
|
parsed_resource_policies.append(resource_policy_ref.SelfLink())
|
|
kwargs['resourcePolicies'] = parsed_resource_policies
|
|
|
|
disk = client.messages.Disk(
|
|
name=disk_ref.Name(),
|
|
description=args.description,
|
|
sizeGb=size_gb,
|
|
sourceSnapshot=snapshot_uri,
|
|
sourceImage=project_to_source_image[disk_ref.project].uri,
|
|
type=type_uri,
|
|
physicalBlockSizeBytes=physical_block_size_bytes,
|
|
**kwargs)
|
|
disk.sourceDisk = self.GetSourceDiskUri(args, disk_ref, compute_holder)
|
|
disk.sourceInstantSnapshot = _GetSourceInstantSnapshotUri(
|
|
args, compute_holder)
|
|
|
|
if (support_multiwriter_disk and
|
|
disk_ref.Collection() in ['compute.disks', 'compute.regionDisks'] and
|
|
args.IsSpecified('multi_writer')):
|
|
disk.multiWriter = args.multi_writer
|
|
|
|
if support_enable_confidential_compute and args.IsSpecified(
|
|
'confidential_compute'
|
|
):
|
|
disk.enableConfidentialCompute = args.confidential_compute
|
|
|
|
if guest_os_feature_messages:
|
|
disk.guestOsFeatures = guest_os_feature_messages
|
|
|
|
if support_vss_erase and args.IsSpecified('erase_windows_vss_signature'):
|
|
disk.eraseWindowsVssSignature = args.erase_windows_vss_signature
|
|
|
|
disk.licenses = self.ParseLicenses(args)
|
|
|
|
if args.IsSpecified('provisioned_iops'):
|
|
if type_uri and disks_util.IsProvisioningTypeIops(type_uri):
|
|
disk.provisionedIops = args.provisioned_iops
|
|
else:
|
|
raise exceptions.InvalidArgumentException(
|
|
'--provisioned-iops',
|
|
'--provisioned-iops cannot be used with the given disk type.')
|
|
|
|
if args.IsSpecified(
|
|
'provisioned_throughput'):
|
|
if type_uri and disks_util.IsProvisioningTypeThroughput(type_uri):
|
|
disk.provisionedThroughput = args.provisioned_throughput
|
|
else:
|
|
raise exceptions.InvalidArgumentException(
|
|
'--provisioned-throughput',
|
|
'--provisioned-throughput cannot be used with the given disk '
|
|
'type.')
|
|
|
|
if args.IsSpecified('architecture'):
|
|
disk.architecture = disk.ArchitectureValueValuesEnum(args.architecture)
|
|
|
|
if args.IsSpecified('access_mode'):
|
|
disk.accessMode = disk.AccessModeValueValuesEnum(args.access_mode)
|
|
|
|
if support_user_licenses and args.IsSpecified('user_licenses'):
|
|
disk.userLicenses = args.user_licenses
|
|
|
|
if args.IsSpecified('location_hint'):
|
|
disk.locationHint = args.location_hint
|
|
|
|
if args.IsSpecified('storage_pool'):
|
|
disk.storagePool = self.GetStoragePoolUri(args, compute_holder)
|
|
|
|
if support_gmi_restore:
|
|
_SetSourceMachineImageOptions(args, disk)
|
|
|
|
if disk_ref.Collection() == 'compute.disks':
|
|
request = client.messages.ComputeDisksInsertRequest(
|
|
disk=disk, project=disk_ref.project, zone=disk_ref.zone)
|
|
|
|
request = (client.apitools_client.disks, 'Insert', request)
|
|
elif disk_ref.Collection() == 'compute.regionDisks':
|
|
if args.IsSpecified('replica_zones'):
|
|
disk.replicaZones = self.GetReplicaZones(
|
|
args, compute_holder, disk_ref
|
|
)
|
|
request = client.messages.ComputeRegionDisksInsertRequest(
|
|
disk=disk, project=disk_ref.project, region=disk_ref.region)
|
|
|
|
request = (client.apitools_client.regionDisks, 'Insert', request)
|
|
|
|
requests.append(request)
|
|
|
|
return client.MakeRequests(requests)
|
|
|
|
def Epilog(self, resources_were_displayed=True):
|
|
message = """\
|
|
|
|
New disks are unformatted. You must format and mount a disk before it
|
|
can be used. You can find instructions on how to do this at:
|
|
|
|
https://cloud.google.com/compute/docs/disks/add-persistent-disk#formatting
|
|
"""
|
|
if self.show_unformated_message:
|
|
log.status.Print(textwrap.dedent(message))
|
|
|
|
|
|
@base.DefaultUniverseOnly
|
|
@base.ReleaseTracks(base.ReleaseTrack.BETA)
|
|
class CreateBeta(Create):
|
|
"""Create Compute Engine persistent disks."""
|
|
|
|
@classmethod
|
|
def Args(cls, parser):
|
|
messages = cls._GetApiHolder(no_http=True).client.messages
|
|
Create.disks_arg = disks_flags.MakeDiskArg(plural=True)
|
|
_CommonArgs(
|
|
messages,
|
|
parser,
|
|
include_physical_block_size_support=True,
|
|
vss_erase_enabled=True,
|
|
support_pd_interface=True,
|
|
support_source_snapshot_region=True,
|
|
source_instant_snapshot_enabled=False,
|
|
)
|
|
image_utils.AddGuestOsFeaturesArg(parser, messages)
|
|
_AddReplicaZonesArg(parser)
|
|
kms_resource_args.AddKmsKeyResourceArg(
|
|
parser, 'disk', region_fallthrough=True)
|
|
disks_flags.AddMultiWriterFlag(parser)
|
|
disks_flags.AddEnableConfidentialComputeFlag(parser)
|
|
|
|
def Run(self, args):
|
|
return self._Run(
|
|
args,
|
|
supports_kms_keys=True,
|
|
supports_physical_block=True,
|
|
support_vss_erase=True,
|
|
support_multiwriter_disk=True,
|
|
support_pd_interface=True,
|
|
support_enable_confidential_compute=True,
|
|
support_source_snapshot_region=True,
|
|
)
|
|
|
|
|
|
@base.DefaultUniverseOnly
|
|
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
|
|
class CreateAlpha(CreateBeta):
|
|
"""Create Compute Engine persistent disks."""
|
|
|
|
@classmethod
|
|
def Args(cls, parser):
|
|
messages = cls._GetApiHolder(no_http=True).client.messages
|
|
Create.disks_arg = disks_flags.MakeDiskArg(plural=True)
|
|
_CommonArgs(
|
|
messages,
|
|
parser,
|
|
include_physical_block_size_support=True,
|
|
vss_erase_enabled=True,
|
|
support_pd_interface=True,
|
|
support_user_licenses=True,
|
|
support_source_snapshot_region=True,
|
|
support_gmi_restore=True,
|
|
source_instant_snapshot_enabled=True
|
|
)
|
|
image_utils.AddGuestOsFeaturesArg(parser, messages)
|
|
_AddReplicaZonesArg(parser)
|
|
kms_resource_args.AddKmsKeyResourceArg(
|
|
parser, 'disk', region_fallthrough=True)
|
|
disks_flags.AddMultiWriterFlag(parser)
|
|
disks_flags.AddEnableConfidentialComputeFlag(parser)
|
|
|
|
def Run(self, args):
|
|
return self._Run(
|
|
args,
|
|
supports_kms_keys=True,
|
|
supports_physical_block=True,
|
|
support_multiwriter_disk=True,
|
|
support_vss_erase=True,
|
|
support_pd_interface=True,
|
|
support_user_licenses=True,
|
|
support_enable_confidential_compute=True,
|
|
support_source_snapshot_region=True,
|
|
support_gmi_restore=True,
|
|
)
|
|
|
|
|
|
def _ValidateAndParseDiskRefsRegionalReplica(
|
|
args, compute_holder
|
|
):
|
|
"""Validate flags and parse disks references.
|
|
|
|
Subclasses may override it to customize parsing.
|
|
|
|
Args:
|
|
args: The argument namespace
|
|
compute_holder: base_classes.ComputeApiHolder instance
|
|
|
|
Returns:
|
|
List of compute.regionDisks resources.
|
|
"""
|
|
if (
|
|
not args.IsSpecified('replica_zones')
|
|
and args.IsSpecified('region')
|
|
and not (args.IsSpecified('source_instant_snapshot'))
|
|
):
|
|
raise exceptions.RequiredArgumentException(
|
|
'--replica-zones',
|
|
'--replica-zones is required for regional disk creation')
|
|
if args.replica_zones is not None:
|
|
return create.ParseRegionDisksResources(compute_holder.resources,
|
|
args.DISK_NAME, args.replica_zones,
|
|
args.project, args.region)
|
|
|
|
disk_refs = Create.disks_arg.ResolveAsResource(
|
|
args,
|
|
compute_holder.resources,
|
|
scope_lister=flags.GetDefaultScopeLister(compute_holder.client))
|
|
|
|
# --replica-zones is required for regional disks unless a source instant
|
|
# snapshot is specified - also when region is selected in prompt.
|
|
for disk_ref in disk_refs:
|
|
if (
|
|
disk_ref.Collection() == 'compute.regionDisks'
|
|
and not args.IsSpecified('source_instant_snapshot')
|
|
):
|
|
raise exceptions.RequiredArgumentException(
|
|
'--replica-zones',
|
|
'--replica-zones is required for regional disk creation [{}]'.format(
|
|
disk_ref.SelfLink()))
|
|
|
|
return disk_refs
|
|
|
|
|
|
def _SetSourceMachineImageOptions(args, disk):
|
|
"""Sets source machine image options on the disk.
|
|
|
|
Args:
|
|
args: The arguments namespace.
|
|
disk: The disk message.
|
|
|
|
Raises:
|
|
exceptions.RequiredArgumentException: If only one of the source machine
|
|
image arguments is specified.
|
|
"""
|
|
|
|
has_source_machine_image = args.IsSpecified('source_machine_image')
|
|
has_disk_device_name = args.IsSpecified(
|
|
'source_machine_image_disk_device_name'
|
|
)
|
|
if has_source_machine_image ^ has_disk_device_name:
|
|
missing_option = (
|
|
'--source-machine-image-disk-device-name'
|
|
if has_source_machine_image
|
|
else '--source-machine-image'
|
|
)
|
|
provided_option = (
|
|
'--source-machine-image'
|
|
if has_source_machine_image
|
|
else '--source-machine-image-disk-device-name'
|
|
)
|
|
raise exceptions.RequiredArgumentException(
|
|
missing_option,
|
|
f'{missing_option} must be specified when {provided_option} is'
|
|
' specified.',
|
|
)
|
|
elif has_source_machine_image and has_disk_device_name:
|
|
disk.sourceMachineImageDiskDeviceName = (
|
|
args.source_machine_image_disk_device_name
|
|
)
|
|
disk.sourceMachineImage = args.source_machine_image
|
|
|
|
Create.detailed_help = DETAILED_HELP
|