1223 lines
42 KiB
Python
1223 lines
42 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.
|
|
"""Convenience functions for dealing with instances and instance templates."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import collections
|
|
import re
|
|
|
|
from googlecloudsdk.api_lib.compute import constants
|
|
from googlecloudsdk.api_lib.compute import containers_utils
|
|
from googlecloudsdk.api_lib.compute import csek_utils
|
|
from googlecloudsdk.api_lib.compute import metadata_utils
|
|
from googlecloudsdk.api_lib.compute import utils
|
|
from googlecloudsdk.api_lib.compute import zone_utils
|
|
from googlecloudsdk.calliope import exceptions as calliope_exceptions
|
|
from googlecloudsdk.command_lib.compute import flags as compute_flags
|
|
from googlecloudsdk.command_lib.compute import resource_manager_tags_utils
|
|
from googlecloudsdk.command_lib.compute import scope as compute_scopes
|
|
from googlecloudsdk.command_lib.compute.instances import flags
|
|
from googlecloudsdk.command_lib.compute.sole_tenancy import util as sole_tenancy_util
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import resources as cloud_resources
|
|
from googlecloudsdk.core.util import times
|
|
import six
|
|
|
|
EMAIL_REGEX = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)')
|
|
|
|
_DEFAULT_DEVICE_NAME_CONTAINER_WARNING = (
|
|
'Default device-name for disk name [{0}] will be [{0}] because it is being '
|
|
'mounted to a container with [`--container-mount-disk`]')
|
|
|
|
|
|
def GetCpuRamVmFamilyFromCustomName(name):
|
|
"""Gets the CPU and memory specs from the custom machine type name.
|
|
|
|
Args:
|
|
name: the custom machine type name for the 'instance create' call
|
|
|
|
Returns:
|
|
A three-tuple with the vm family, number of cpu and amount of memory for the
|
|
custom machine type.
|
|
custom_family, the name of the VM family
|
|
custom_cpu, the number of cpu desired for the custom machine type instance
|
|
custom_memory_mib, the amount of ram desired in MiB for the custom machine
|
|
type instance
|
|
None for both variables otherwise
|
|
"""
|
|
check_custom = re.search('([a-zA-Z0-9]+)-custom-([0-9]+)-([0-9]+)', name)
|
|
if check_custom:
|
|
custom_family = check_custom.group(1)
|
|
custom_cpu = check_custom.group(2)
|
|
custom_memory_mib = check_custom.group(3)
|
|
return custom_family, custom_cpu, custom_memory_mib
|
|
return None, None, None
|
|
|
|
|
|
def GetNameForCustom(custom_cpu, custom_memory_mib, ext=False, vm_type=False):
|
|
"""Creates a custom machine type name from the desired CPU and memory specs.
|
|
|
|
Args:
|
|
custom_cpu: the number of cpu desired for the custom machine type
|
|
custom_memory_mib: the amount of ram desired in MiB for the custom machine
|
|
type instance
|
|
ext: extended custom machine type should be used if true
|
|
vm_type: VM instance generation
|
|
|
|
Returns:
|
|
The custom machine type name for the 'instance create' call
|
|
"""
|
|
if vm_type:
|
|
machine_type = '{0}-custom-{1}-{2}'.format(vm_type, custom_cpu,
|
|
custom_memory_mib)
|
|
else:
|
|
machine_type = 'custom-{0}-{1}'.format(custom_cpu, custom_memory_mib)
|
|
if ext:
|
|
machine_type += '-ext'
|
|
return machine_type
|
|
|
|
|
|
def InterpretMachineType(machine_type,
|
|
custom_cpu,
|
|
custom_memory,
|
|
ext=True,
|
|
vm_type=False,
|
|
confidential_vm_type=None):
|
|
"""Interprets the machine type for the instance.
|
|
|
|
Args:
|
|
machine_type: name of existing machine type, eg. n1-standard
|
|
custom_cpu: number of CPU cores for custom machine type,
|
|
custom_memory: amount of RAM memory in bytes for custom machine type,
|
|
ext: extended custom machine type should be used if true,
|
|
vm_type: VM instance generation
|
|
confidential_vm_type: If not None, use default machine type based on
|
|
confidential-VM encryption type.
|
|
|
|
Returns:
|
|
A string representing the URL naming a machine-type.
|
|
|
|
Raises:
|
|
calliope_exceptions.RequiredArgumentException when only one of the two
|
|
custom machine type flags are used.
|
|
calliope_exceptions.InvalidArgumentException when both the machine type and
|
|
custom machine type flags are used to generate a new instance.
|
|
"""
|
|
# Setting the machine type
|
|
if machine_type:
|
|
machine_type_name = machine_type
|
|
elif confidential_vm_type is not None:
|
|
machine_type_name = constants.DEFAULT_MACHINE_TYPE_FOR_CONFIDENTIAL_VMS[
|
|
confidential_vm_type
|
|
]
|
|
else:
|
|
machine_type_name = constants.DEFAULT_MACHINE_TYPE
|
|
|
|
# Setting the specs for the custom machine.
|
|
if custom_cpu or custom_memory or ext:
|
|
if not custom_cpu:
|
|
raise calliope_exceptions.RequiredArgumentException(
|
|
'--custom-cpu', 'Both [--custom-cpu] and [--custom-memory] must be '
|
|
'set to create a custom machine type instance.')
|
|
if not custom_memory:
|
|
raise calliope_exceptions.RequiredArgumentException(
|
|
'--custom-memory', 'Both [--custom-cpu] and [--custom-memory] must '
|
|
'be set to create a custom machine type instance.')
|
|
if machine_type:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--machine-type', 'Cannot set both [--machine-type] and '
|
|
'[--custom-cpu]/[--custom-memory] for the same instance.')
|
|
custom_type_string = GetNameForCustom(
|
|
custom_cpu,
|
|
# converting from B to MiB.
|
|
custom_memory // (2**20),
|
|
ext,
|
|
vm_type)
|
|
|
|
# Updating the machine type that is set for the URIs
|
|
machine_type_name = custom_type_string
|
|
return machine_type_name
|
|
|
|
|
|
def CheckCustomCpuRamRatio(compute_client, project, zone, machine_type_name):
|
|
"""Checks that the CPU and memory ratio is a supported custom instance type.
|
|
|
|
Args:
|
|
compute_client: GCE API client,
|
|
project: a project,
|
|
zone: the zone of the instance(s) being created,
|
|
machine_type_name: The machine type of the instance being created.
|
|
|
|
Returns:
|
|
Nothing. Function acts as a bound checker, and will raise an exception from
|
|
within the function if needed.
|
|
|
|
Raises:
|
|
utils.RaiseToolException if a custom machine type ratio is out of bounds.
|
|
"""
|
|
messages = compute_client.messages
|
|
compute = compute_client.apitools_client
|
|
if 'custom' in machine_type_name:
|
|
mt_get_pb = messages.ComputeMachineTypesGetRequest(
|
|
machineType=machine_type_name, project=project, zone=zone)
|
|
mt_get_reqs = [(compute.machineTypes, 'Get', mt_get_pb)]
|
|
errors = []
|
|
|
|
# Makes a 'machine-types describe' request to check the bounds
|
|
_ = list(
|
|
compute_client.MakeRequests(
|
|
requests=mt_get_reqs, errors_to_collect=errors))
|
|
|
|
if errors:
|
|
utils.RaiseToolException(
|
|
errors, error_message='Could not fetch machine type:')
|
|
|
|
|
|
def CreateServiceAccountMessages(messages, scopes, service_account):
|
|
"""Returns a list of ServiceAccount messages corresponding to scopes."""
|
|
if scopes is None:
|
|
scopes = constants.DEFAULT_SCOPES
|
|
if service_account is None:
|
|
service_account = 'default'
|
|
|
|
accounts_to_scopes = collections.defaultdict(list)
|
|
for scope in scopes:
|
|
parts = scope.split('=')
|
|
if len(parts) == 1:
|
|
account = service_account
|
|
scope_uri = scope
|
|
elif len(parts) == 2:
|
|
# TODO(b/33688878) Remove exception for this deprecated format
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--scopes',
|
|
'Flag format --scopes [ACCOUNT=]SCOPE,[[ACCOUNT=]SCOPE, ...] is '
|
|
'removed. Use --scopes [SCOPE,...] --service-account ACCOUNT '
|
|
'instead.')
|
|
else:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--scopes',
|
|
'[{0}] is an illegal value for [--scopes]. Values must be of the '
|
|
'form [SCOPE].'.format(scope))
|
|
|
|
if service_account != 'default' and not EMAIL_REGEX.match(service_account):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--service-account',
|
|
'Invalid format: expected default or user@domain.com, received ' +
|
|
service_account)
|
|
|
|
# Expands the scope if the user provided an alias like
|
|
# "compute-rw".
|
|
scope_uri = constants.SCOPES.get(scope_uri, [scope_uri])
|
|
accounts_to_scopes[account].extend(scope_uri)
|
|
|
|
if not scopes and service_account != 'default':
|
|
return [messages.ServiceAccount(email=service_account, scopes=[])]
|
|
res = []
|
|
for account, scopes in sorted(six.iteritems(accounts_to_scopes)):
|
|
res.append(messages.ServiceAccount(email=account, scopes=sorted(scopes)))
|
|
return res
|
|
|
|
|
|
def CreateWorkloadIdentityConfigMessage(
|
|
args, messages, support_workload_identity_config
|
|
):
|
|
"""Create workloadIdentityConfig message for VM."""
|
|
if not support_workload_identity_config:
|
|
return None
|
|
if not args.IsKnownAndSpecified(
|
|
'identity'
|
|
) and not args.IsKnownAndSpecified('identity_certificate'):
|
|
return None
|
|
return messages.WorkloadIdentityConfig(
|
|
identity=args.identity
|
|
if args.IsKnownAndSpecified('identity')
|
|
else None,
|
|
identityCertificateEnabled=args.identity_certificate
|
|
if args.IsKnownAndSpecified('identity_certificate')
|
|
else None,
|
|
)
|
|
|
|
|
|
def CreateOnHostMaintenanceMessage(messages, maintenance_policy):
|
|
"""Create on-host-maintenance message for VM."""
|
|
if maintenance_policy:
|
|
on_host_maintenance = messages.Scheduling.OnHostMaintenanceValueValuesEnum(
|
|
maintenance_policy)
|
|
else:
|
|
on_host_maintenance = None
|
|
return on_host_maintenance
|
|
|
|
|
|
def CreateSchedulingMessage(
|
|
messages,
|
|
maintenance_policy,
|
|
preemptible,
|
|
restart_on_failure,
|
|
node_affinities=None,
|
|
min_node_cpu=None,
|
|
location_hint=None,
|
|
maintenance_freeze_duration=None,
|
|
maintenance_interval=None,
|
|
provisioning_model=None,
|
|
instance_termination_action=None,
|
|
host_error_timeout_seconds=None,
|
|
max_run_duration=None,
|
|
termination_time=None,
|
|
local_ssd_recovery_timeout=None,
|
|
availability_domain=None,
|
|
graceful_shutdown=None,
|
|
discard_local_ssds_at_termination_timestamp=None,
|
|
skip_guest_os_shutdown=None,
|
|
preemption_notice_duration=None,
|
|
):
|
|
"""Create scheduling message for VM."""
|
|
# Note: We always specify automaticRestart=False for preemptible VMs. This
|
|
# makes sense, since no-restart-on-failure is defined as "store-true", and
|
|
# thus can't be given an explicit value. Hence it either has its default
|
|
# value (in which case we override it for convenience's sake to the only
|
|
# setting that makes sense for preemptible VMs), or the user actually
|
|
# specified no-restart-on-failure, the only usable setting.
|
|
on_host_maintenance = CreateOnHostMaintenanceMessage(messages,
|
|
maintenance_policy)
|
|
if preemptible or provisioning_model == 'SPOT':
|
|
scheduling = messages.Scheduling(
|
|
automaticRestart=False,
|
|
onHostMaintenance=on_host_maintenance,
|
|
preemptible=True)
|
|
else:
|
|
scheduling = messages.Scheduling(
|
|
automaticRestart=restart_on_failure,
|
|
onHostMaintenance=on_host_maintenance)
|
|
|
|
if provisioning_model:
|
|
scheduling.provisioningModel = (
|
|
messages.Scheduling.ProvisioningModelValueValuesEnum(provisioning_model)
|
|
)
|
|
|
|
if instance_termination_action:
|
|
scheduling.instanceTerminationAction = (
|
|
messages.Scheduling.InstanceTerminationActionValueValuesEnum(
|
|
instance_termination_action
|
|
)
|
|
)
|
|
|
|
if max_run_duration is not None:
|
|
scheduling.maxRunDuration = messages.Duration(seconds=max_run_duration)
|
|
|
|
if local_ssd_recovery_timeout is not None:
|
|
scheduling.localSsdRecoveryTimeout = messages.Duration(
|
|
seconds=local_ssd_recovery_timeout
|
|
)
|
|
|
|
if graceful_shutdown is not None:
|
|
scheduling.gracefulShutdown = messages.SchedulingGracefulShutdown()
|
|
if 'enabled' in graceful_shutdown:
|
|
scheduling.gracefulShutdown.enabled = graceful_shutdown['enabled']
|
|
if 'maxDuration' in graceful_shutdown:
|
|
scheduling.gracefulShutdown.maxDuration = messages.Duration(
|
|
seconds=graceful_shutdown['maxDuration']
|
|
)
|
|
|
|
if termination_time:
|
|
scheduling.terminationTime = times.FormatDateTime(termination_time)
|
|
|
|
if node_affinities:
|
|
scheduling.nodeAffinities = node_affinities
|
|
|
|
if min_node_cpu is not None:
|
|
scheduling.minNodeCpus = int(min_node_cpu)
|
|
|
|
if location_hint:
|
|
scheduling.locationHint = location_hint
|
|
|
|
if maintenance_freeze_duration:
|
|
scheduling.maintenanceFreezeDurationHours = (
|
|
maintenance_freeze_duration // 3600)
|
|
|
|
if maintenance_interval:
|
|
scheduling.maintenanceInterval = (
|
|
messages.Scheduling.MaintenanceIntervalValueValuesEnum(
|
|
maintenance_interval))
|
|
|
|
if host_error_timeout_seconds:
|
|
scheduling.hostErrorTimeoutSeconds = host_error_timeout_seconds
|
|
|
|
if availability_domain:
|
|
scheduling.availabilityDomain = availability_domain
|
|
|
|
if discard_local_ssds_at_termination_timestamp is not None:
|
|
scheduling.onInstanceStopAction = messages.SchedulingOnInstanceStopAction(
|
|
discardLocalSsd=discard_local_ssds_at_termination_timestamp
|
|
)
|
|
|
|
if skip_guest_os_shutdown is not None:
|
|
scheduling.skipGuestOsShutdown = skip_guest_os_shutdown
|
|
|
|
if preemption_notice_duration is not None:
|
|
scheduling.preemptionNoticeDuration = messages.Duration(
|
|
seconds=preemption_notice_duration
|
|
)
|
|
|
|
return scheduling
|
|
|
|
|
|
def CreateShieldedInstanceConfigMessage(messages, enable_secure_boot,
|
|
enable_vtpm,
|
|
enable_integrity_monitoring):
|
|
"""Create shieldedInstanceConfig message for VM."""
|
|
|
|
shielded_instance_config = messages.ShieldedInstanceConfig(
|
|
enableSecureBoot=enable_secure_boot,
|
|
enableVtpm=enable_vtpm,
|
|
enableIntegrityMonitoring=enable_integrity_monitoring)
|
|
|
|
return shielded_instance_config
|
|
|
|
|
|
def CreateShieldedInstanceIntegrityPolicyMessage(messages,
|
|
update_auto_learn_policy=True):
|
|
"""Creates shieldedInstanceIntegrityPolicy message for VM."""
|
|
|
|
shielded_instance_integrity_policy = messages.ShieldedInstanceIntegrityPolicy(
|
|
updateAutoLearnPolicy=update_auto_learn_policy)
|
|
|
|
return shielded_instance_integrity_policy
|
|
|
|
|
|
def ValidateSvsmConfig(svsm_args, support_snp_svsm):
|
|
"""Validates flags specifying SVSM config.
|
|
|
|
Args:
|
|
svsm_args: The flags specifying SVSM config.
|
|
support_snp_svsm: Whether SVSM is supported.
|
|
|
|
Returns:
|
|
Nothing. Function acts as a validator, and will raise an exception from
|
|
within the function if needed.
|
|
|
|
Raises:
|
|
calliope_exceptions.RequiredArgumentException when the flags are not
|
|
specified or are not valid.
|
|
"""
|
|
|
|
if not svsm_args or not support_snp_svsm:
|
|
return
|
|
|
|
tpm_choices = ['EPHEMERAL', 'NO_CC_TPM']
|
|
snp_irq_choices = ['RESTRICTED', 'UNRESTRICTED']
|
|
|
|
tpm = svsm_args.get('tpm', None)
|
|
snp_irq = svsm_args.get('snp-irq', None)
|
|
if tpm and tpm not in tpm_choices:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--svsm-config',
|
|
f'Unexpected confidential TPM type: [{tpm}]. Legal values are '
|
|
f'[{", ".join(tpm_choices)}].',
|
|
)
|
|
if snp_irq and snp_irq not in snp_irq_choices:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--svsm-config',
|
|
f'Unexpected SEV SNP IRQ mode: [{snp_irq}]. Legal values are '
|
|
f'[{", ".join(snp_irq_choices)}].',
|
|
)
|
|
|
|
|
|
def CreateConfidentialParavisorConfigMessage(args, messages, support_snp_svsm):
|
|
"""Create confidentialParavisorConfig message for VM."""
|
|
if (
|
|
not hasattr(args, 'svsm_config') or not args.IsSpecified('svsm_config')
|
|
or not support_snp_svsm
|
|
):
|
|
return None
|
|
ValidateSvsmConfig(args.svsm_config, support_snp_svsm)
|
|
tpm = (
|
|
args.svsm_config.get('tpm', 'CONFIDENTIAL_TPM_TYPE_UNSPECIFIED')
|
|
if args.svsm_config
|
|
else 'CONFIDENTIAL_TPM_TYPE_UNSPECIFIED'
|
|
)
|
|
snp_irq = (
|
|
args.svsm_config.get('snp-irq', 'SEV_SNP_IRQ_MODE_UNSPECIFIED')
|
|
if args.svsm_config
|
|
else 'SEV_SNP_IRQ_MODE_UNSPECIFIED'
|
|
)
|
|
confidential_tpm_type = (
|
|
messages.ConfidentialParavisorConfig.ConfidentialTpmTypeValueValuesEnum(
|
|
tpm
|
|
)
|
|
)
|
|
sev_snp_irq_mode = (
|
|
messages.ConfidentialParavisorConfig.SevSnpIrqModeValueValuesEnum(
|
|
snp_irq
|
|
)
|
|
)
|
|
return messages.ConfidentialParavisorConfig(
|
|
confidentialTpmType=confidential_tpm_type,
|
|
sevSnpIrqMode=sev_snp_irq_mode,
|
|
)
|
|
|
|
|
|
def CreateConfidentialInstanceMessage(messages, args,
|
|
support_confidential_compute_type,
|
|
support_confidential_compute_type_tdx,
|
|
support_snp_svsm):
|
|
"""Create confidentialInstanceConfig message for VM."""
|
|
confidential_instance_config_msg = None
|
|
enable_confidential_compute = None
|
|
confidential_instance_type = None
|
|
|
|
if (hasattr(args, 'confidential_compute') and
|
|
args.IsSpecified('confidential_compute') and
|
|
isinstance(args.confidential_compute, bool)):
|
|
enable_confidential_compute = args.confidential_compute
|
|
|
|
if (support_confidential_compute_type and
|
|
hasattr(args, 'confidential_compute_type') and
|
|
args.IsSpecified('confidential_compute_type') and
|
|
isinstance(args.confidential_compute_type, six.string_types)):
|
|
confidential_instance_type = (
|
|
messages.ConfidentialInstanceConfig
|
|
.ConfidentialInstanceTypeValueValuesEnum(
|
|
args.confidential_compute_type))
|
|
|
|
if (not support_confidential_compute_type_tdx and
|
|
'TDX' in (
|
|
messages.ConfidentialInstanceConfig
|
|
.ConfidentialInstanceTypeValueValuesEnum)):
|
|
enable_confidential_compute = None
|
|
confidential_instance_type = None
|
|
|
|
confidential_paravisor_config_msg = CreateConfidentialParavisorConfigMessage(
|
|
args, messages, support_snp_svsm
|
|
)
|
|
if (
|
|
confidential_instance_type is not None
|
|
and confidential_paravisor_config_msg is not None
|
|
):
|
|
confidential_instance_config_msg = messages.ConfidentialInstanceConfig(
|
|
confidentialInstanceType=confidential_instance_type,
|
|
confidentialParavisorConfig=confidential_paravisor_config_msg,
|
|
)
|
|
elif confidential_instance_type is not None:
|
|
confidential_instance_config_msg = messages.ConfidentialInstanceConfig(
|
|
confidentialInstanceType=confidential_instance_type
|
|
)
|
|
elif confidential_paravisor_config_msg is not None:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--confidential-compute-type',
|
|
'Confidential compute type must be specified when using '
|
|
'--svsm-config.',
|
|
)
|
|
elif enable_confidential_compute is not None:
|
|
confidential_instance_config_msg = messages.ConfidentialInstanceConfig(
|
|
enableConfidentialCompute=enable_confidential_compute
|
|
)
|
|
|
|
return confidential_instance_config_msg
|
|
|
|
|
|
def CreateAdvancedMachineFeaturesMessage(
|
|
messages,
|
|
enable_nested_virtualization=None,
|
|
threads_per_core=None,
|
|
numa_node_count=None,
|
|
visible_core_count=None,
|
|
enable_uefi_networking=None,
|
|
performance_monitoring_unit=None,
|
|
enable_watchdog_timer=None,
|
|
turbo_mode=None,
|
|
):
|
|
"""Create AdvancedMachineFeatures message for an Instance."""
|
|
# Start with an empty AdvancedMachineFeatures and optionally add on
|
|
# the features we have like CreateSchedulingMessage does. This lets us
|
|
# treat None as also "not supported in this version of the API (yet)".
|
|
features = messages.AdvancedMachineFeatures()
|
|
|
|
if enable_nested_virtualization is not None:
|
|
features.enableNestedVirtualization = enable_nested_virtualization
|
|
|
|
if threads_per_core is not None:
|
|
features.threadsPerCore = threads_per_core
|
|
|
|
if numa_node_count is not None:
|
|
features.numaNodeCount = numa_node_count
|
|
|
|
if visible_core_count is not None:
|
|
features.visibleCoreCount = visible_core_count
|
|
|
|
if enable_uefi_networking is not None:
|
|
features.enableUefiNetworking = enable_uefi_networking
|
|
|
|
if performance_monitoring_unit is not None:
|
|
features.performanceMonitoringUnit = messages.AdvancedMachineFeatures.PerformanceMonitoringUnitValueValuesEnum(
|
|
performance_monitoring_unit.upper()
|
|
)
|
|
|
|
if enable_watchdog_timer is not None:
|
|
features.enableWatchdogTimer = enable_watchdog_timer
|
|
|
|
if turbo_mode is not None:
|
|
features.turboMode = turbo_mode
|
|
|
|
return features
|
|
|
|
|
|
def ParseDiskResource(resources, name, project, zone, type_):
|
|
"""Parses disk resources.
|
|
|
|
Project and zone are ignored if a fully-qualified resource name is given, i.e.
|
|
- https://compute.googleapis.com/compute/v1/projects/my-project
|
|
/zones/us-central1-a/disks/disk-1
|
|
- projects/my-project/zones/us-central1-a/disks/disk-1
|
|
|
|
If project and zone cannot be parsed, we will use the given project and zone
|
|
as fallbacks.
|
|
|
|
Args:
|
|
resources: resources.Registry, The resource registry
|
|
name: str, name of the disk.
|
|
project: str, project of the disk.
|
|
zone: str, zone of the disk.
|
|
type_: ScopeEnum, type of the disk.
|
|
|
|
Returns:
|
|
A disk resource.
|
|
"""
|
|
if type_ == compute_scopes.ScopeEnum.REGION:
|
|
return resources.Parse(
|
|
name,
|
|
collection='compute.regionDisks',
|
|
params={
|
|
'project': project,
|
|
'region': utils.ZoneNameToRegionName(zone)
|
|
})
|
|
else:
|
|
return resources.Parse(
|
|
name,
|
|
collection='compute.disks',
|
|
params={
|
|
'project': project,
|
|
'zone': zone
|
|
})
|
|
|
|
|
|
def ParseDiskResourceFromAttachedDisk(resources, attached_disk):
|
|
"""Parses the source disk resource of an AttachedDisk.
|
|
|
|
The source of an AttachedDisk is either a partial or fully specified URL
|
|
referencing either a regional or zonal disk.
|
|
|
|
Args:
|
|
resources: resources.Registry, The resource registry
|
|
attached_disk: AttachedDisk
|
|
|
|
Returns:
|
|
A disk resource.
|
|
|
|
Raises:
|
|
InvalidResourceException: If the attached disk source cannot be parsed as a
|
|
regional or zonal disk.
|
|
"""
|
|
try:
|
|
disk = resources.Parse(
|
|
attached_disk.source, collection='compute.regionDisks'
|
|
)
|
|
if disk:
|
|
return disk
|
|
except (
|
|
cloud_resources.WrongResourceCollectionException,
|
|
cloud_resources.RequiredFieldOmittedException,
|
|
):
|
|
pass
|
|
|
|
try:
|
|
disk = resources.Parse(attached_disk.source, collection='compute.disks')
|
|
if disk:
|
|
return disk
|
|
except (
|
|
cloud_resources.WrongResourceCollectionException,
|
|
cloud_resources.RequiredFieldOmittedException,
|
|
):
|
|
pass
|
|
|
|
raise cloud_resources.InvalidResourceException(
|
|
"Unable to parse disk's source: [{0}] of device name: [{1}], try using"
|
|
' `--device-name` instead.'.format(
|
|
attached_disk.source,
|
|
attached_disk.deviceName,
|
|
)
|
|
)
|
|
|
|
|
|
def GetDiskDeviceName(disk, name, container_mount_disk):
|
|
"""Helper method to get device-name for a disk message."""
|
|
if container_mount_disk and filter(
|
|
bool, [d.get('name', name) == name for d in container_mount_disk]
|
|
):
|
|
# device-name must be the same as name if it is being mounted to a
|
|
# container.
|
|
if not disk.get('device-name'):
|
|
log.warning(_DEFAULT_DEVICE_NAME_CONTAINER_WARNING.format(name))
|
|
return name
|
|
# This is defensive only; should be validated before this method is called.
|
|
elif disk.get('device-name') != name:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--container-mount-disk',
|
|
'Attempting to mount disk named [{}] with device-name [{}]. If '
|
|
'being mounted to container, disk name must match device-name.'
|
|
.format(name, disk.get('device-name')),
|
|
)
|
|
return disk.get('device-name')
|
|
|
|
|
|
def ParseDiskType(
|
|
resources, disk_type, project, location, scope, replica_zone_cnt=0
|
|
):
|
|
"""Parses disk type reference based on location scope."""
|
|
if scope == compute_scopes.ScopeEnum.ZONE:
|
|
if replica_zone_cnt != 2:
|
|
collection = 'compute.diskTypes'
|
|
params = {'project': project, 'zone': location}
|
|
else:
|
|
collection = 'compute.regionDiskTypes'
|
|
location = GetRegionFromZone(location)
|
|
params = {'project': project, 'region': location}
|
|
elif scope == compute_scopes.ScopeEnum.REGION:
|
|
collection = 'compute.regionDiskTypes'
|
|
params = {'project': project, 'region': location}
|
|
disk_type_ref = resources.Parse(
|
|
disk_type, collection=collection, params=params)
|
|
return disk_type_ref
|
|
|
|
|
|
def GetRegionFromZone(zone):
|
|
"""Returns the GCP region that the input zone is in."""
|
|
return '-'.join(zone.split('-')[:-1]).lower()
|
|
|
|
|
|
def ParseStoragePool(resources, storage_pool, project, location):
|
|
"""Parses disk type reference based on location scope."""
|
|
collection = 'compute.storagePools'
|
|
params = {'project': project, 'zone': location}
|
|
storage_pool_ref = resources.Parse(
|
|
storage_pool, collection=collection, params=params
|
|
)
|
|
return storage_pool_ref
|
|
|
|
|
|
def UseExistingBootDisk(disks):
|
|
"""Returns True if the user has specified an existing boot disk."""
|
|
return any(disk.get('boot', False) for disk in disks)
|
|
|
|
|
|
def IsAnySpecified(args, *dests):
|
|
return any([args.IsSpecified(dest) for dest in dests])
|
|
|
|
|
|
def GetSourceInstanceTemplate(args, resources, source_instance_template_arg):
|
|
if not args.IsSpecified('source_instance_template'):
|
|
return None
|
|
ref = source_instance_template_arg.ResolveAsResource(
|
|
args, resources, default_scope=flags.compute_scope.ScopeEnum.GLOBAL
|
|
)
|
|
return ref.SelfLink()
|
|
|
|
|
|
def GetSkipDefaults(source_instance_template):
|
|
# gcloud creates default values for some fields in Instance resource
|
|
# when no value was specified on command line.
|
|
# When --source-instance-template was specified, defaults are taken from
|
|
# Instance Template and gcloud flags are used to override them - by default
|
|
# fields should not be initialized.
|
|
return source_instance_template is not None
|
|
|
|
|
|
def GetScheduling(
|
|
args,
|
|
client,
|
|
skip_defaults,
|
|
support_node_affinity=False,
|
|
support_min_node_cpu=True,
|
|
support_node_project=False,
|
|
support_host_error_timeout_seconds=True,
|
|
support_max_run_duration=False,
|
|
support_local_ssd_recovery_timeout=False,
|
|
support_graceful_shutdown=False,
|
|
support_skip_guest_os_shutdown=False,
|
|
support_preemption_notice_duration=False,
|
|
):
|
|
"""Generate a Scheduling Message or None based on specified args."""
|
|
node_affinities = None
|
|
if support_node_affinity:
|
|
node_affinities = sole_tenancy_util.GetSchedulingNodeAffinityListFromArgs(
|
|
args, client.messages, support_node_project)
|
|
min_node_cpu = None
|
|
if support_min_node_cpu:
|
|
min_node_cpu = args.min_node_cpu
|
|
location_hint = None
|
|
if hasattr(args, 'location_hint'):
|
|
location_hint = args.location_hint
|
|
freeze_duration = None
|
|
if hasattr(args, 'maintenance_freeze_duration') and args.IsSpecified(
|
|
'maintenance_freeze_duration'):
|
|
freeze_duration = args.maintenance_freeze_duration
|
|
maintenance_interval = None
|
|
if hasattr(
|
|
args,
|
|
'maintenance_interval') and args.IsSpecified('maintenance_interval'):
|
|
maintenance_interval = args.maintenance_interval
|
|
provisioning_model = None
|
|
if (hasattr(args, 'provisioning_model') and
|
|
args.IsSpecified('provisioning_model')):
|
|
provisioning_model = args.provisioning_model
|
|
|
|
instance_termination_action = None
|
|
if (hasattr(args, 'instance_termination_action') and
|
|
args.IsSpecified('instance_termination_action')):
|
|
instance_termination_action = args.instance_termination_action
|
|
|
|
host_error_timeout_seconds = None
|
|
if support_host_error_timeout_seconds and hasattr(
|
|
args, 'host_error_timeout_seconds'):
|
|
host_error_timeout_seconds = args.host_error_timeout_seconds
|
|
|
|
max_run_duration = None
|
|
if support_max_run_duration and hasattr(args, 'max_run_duration'):
|
|
max_run_duration = args.max_run_duration
|
|
|
|
local_ssd_recovery_timeout = None
|
|
if support_local_ssd_recovery_timeout and hasattr(
|
|
args, 'local_ssd_recovery_timeout') and args.IsSpecified(
|
|
'local_ssd_recovery_timeout'):
|
|
local_ssd_recovery_timeout = args.local_ssd_recovery_timeout
|
|
|
|
graceful_shutdown = ExtractGracefulShutdownFromArgs(
|
|
args, support_graceful_shutdown
|
|
)
|
|
|
|
termination_time = None
|
|
if support_max_run_duration and hasattr(args, 'termination_time'):
|
|
termination_time = args.termination_time
|
|
|
|
discard_local_ssds_at_termination_timestamp = None
|
|
if support_max_run_duration and hasattr(
|
|
args, 'discard_local_ssds_at_termination_timestamp'
|
|
):
|
|
discard_local_ssds_at_termination_timestamp = (
|
|
args.discard_local_ssds_at_termination_timestamp
|
|
)
|
|
|
|
# Make sure restart_on_failure always retain user-provided value,
|
|
# but also ignore its default value when asked to skip defaults.
|
|
restart_on_failure = None
|
|
if not skip_defaults or args.IsKnownAndSpecified('restart_on_failure'):
|
|
restart_on_failure = args.restart_on_failure
|
|
|
|
availability_domain = None
|
|
if args.IsKnownAndSpecified('availability_domain') and hasattr(
|
|
args, 'availability_domain'
|
|
):
|
|
availability_domain = args.availability_domain
|
|
|
|
skip_guest_os_shutdown = None
|
|
if support_skip_guest_os_shutdown and args.IsKnownAndSpecified(
|
|
'skip_guest_os_shutdown'
|
|
):
|
|
skip_guest_os_shutdown = args.skip_guest_os_shutdown
|
|
|
|
preemption_notice_duration = None
|
|
if support_preemption_notice_duration and hasattr(
|
|
args, 'preemption_notice_duration'
|
|
):
|
|
preemption_notice_duration = args.preemption_notice_duration
|
|
|
|
if (
|
|
skip_defaults
|
|
and not IsAnySpecified(
|
|
args,
|
|
'instance_termination_action',
|
|
'maintenance_policy',
|
|
'preemptible',
|
|
'provisioning_model',
|
|
)
|
|
and not restart_on_failure
|
|
and not node_affinities
|
|
and not max_run_duration
|
|
and not termination_time
|
|
and not freeze_duration
|
|
and not host_error_timeout_seconds
|
|
and not maintenance_interval
|
|
and not local_ssd_recovery_timeout
|
|
and not graceful_shutdown
|
|
and not preemption_notice_duration
|
|
):
|
|
return None
|
|
|
|
return CreateSchedulingMessage(
|
|
messages=client.messages,
|
|
maintenance_policy=args.maintenance_policy,
|
|
preemptible=args.preemptible,
|
|
restart_on_failure=restart_on_failure,
|
|
node_affinities=node_affinities,
|
|
min_node_cpu=min_node_cpu,
|
|
location_hint=location_hint,
|
|
maintenance_freeze_duration=freeze_duration,
|
|
maintenance_interval=maintenance_interval,
|
|
provisioning_model=provisioning_model,
|
|
instance_termination_action=instance_termination_action,
|
|
host_error_timeout_seconds=host_error_timeout_seconds,
|
|
max_run_duration=max_run_duration,
|
|
termination_time=termination_time,
|
|
local_ssd_recovery_timeout=local_ssd_recovery_timeout,
|
|
availability_domain=availability_domain,
|
|
graceful_shutdown=graceful_shutdown,
|
|
discard_local_ssds_at_termination_timestamp=discard_local_ssds_at_termination_timestamp,
|
|
skip_guest_os_shutdown=skip_guest_os_shutdown,
|
|
preemption_notice_duration=preemption_notice_duration,
|
|
)
|
|
|
|
|
|
def GetServiceAccounts(args, client, skip_defaults):
|
|
if args.no_service_account:
|
|
service_account = None
|
|
else:
|
|
service_account = args.service_account
|
|
if (skip_defaults and not IsAnySpecified(
|
|
args, 'scopes', 'no_scopes', 'service_account', 'no_service_account')):
|
|
return []
|
|
return CreateServiceAccountMessages(
|
|
messages=client.messages,
|
|
scopes=[] if args.no_scopes else args.scopes,
|
|
service_account=service_account)
|
|
|
|
|
|
def GetValidatedMetadata(args, client):
|
|
user_metadata = metadata_utils.ConstructMetadataMessage(
|
|
client.messages,
|
|
metadata=args.metadata,
|
|
metadata_from_file=args.metadata_from_file)
|
|
containers_utils.ValidateUserMetadata(user_metadata)
|
|
return user_metadata
|
|
|
|
|
|
def GetMetadata(args, client, skip_defaults):
|
|
if (skip_defaults and
|
|
not IsAnySpecified(args, 'metadata', 'metadata_from_file')):
|
|
return None
|
|
else:
|
|
return metadata_utils.ConstructMetadataMessage(
|
|
client.messages,
|
|
metadata=args.metadata,
|
|
metadata_from_file=args.metadata_from_file)
|
|
|
|
|
|
def GetBootDiskSizeGb(args):
|
|
boot_disk_size_gb = utils.BytesToGb(args.boot_disk_size)
|
|
utils.WarnIfDiskSizeIsTooSmall(boot_disk_size_gb, args.boot_disk_type)
|
|
return boot_disk_size_gb
|
|
|
|
|
|
def GetInstanceRefs(args, client, holder):
|
|
instance_refs = flags.INSTANCES_ARG.ResolveAsResource(
|
|
args,
|
|
holder.resources,
|
|
scope_lister=compute_flags.GetDefaultScopeLister(client))
|
|
# Check if the zone is deprecated or has maintenance coming.
|
|
zone_resource_fetcher = zone_utils.ZoneResourceFetcher(client)
|
|
zone_resource_fetcher.WarnForZonalCreation(instance_refs)
|
|
return instance_refs
|
|
|
|
|
|
def GetSourceMachineImageKey(args, source_image, compute_client, holder):
|
|
machine_image_ref = source_image.ResolveAsResource(args, holder.resources)
|
|
csek_keys = csek_utils.CsekKeyStore.FromFile(
|
|
args.source_machine_image_csek_key_file, allow_rsa_encrypted=False)
|
|
disk_key_or_none = csek_utils.MaybeLookupKeyMessage(
|
|
csek_keys, machine_image_ref, compute_client.apitools_client)
|
|
return disk_key_or_none
|
|
|
|
|
|
def CheckSpecifiedMachineTypeArgs(args, skip_defaults):
|
|
return (not skip_defaults or
|
|
IsAnySpecified(args, 'machine_type', 'custom_cpu', 'custom_memory'))
|
|
|
|
|
|
def CreateMachineTypeUri(args,
|
|
compute_client,
|
|
resource_parser,
|
|
project,
|
|
location,
|
|
scope,
|
|
confidential_vm_type=None):
|
|
"""Create a machine type URI for given args and instance reference."""
|
|
|
|
machine_type_name = CreateMachineTypeName(args, confidential_vm_type)
|
|
|
|
# Check to see if the custom machine type ratio is supported
|
|
CheckCustomCpuRamRatio(compute_client, project, location, machine_type_name)
|
|
|
|
machine_type_uri = ParseMachineType(resource_parser, machine_type_name,
|
|
project, location, scope)
|
|
return machine_type_uri
|
|
|
|
|
|
def CreateMachineTypeName(args, confidential_vm_type=None):
|
|
"""Create a machine type name for given args and instance reference."""
|
|
|
|
machine_type = args.machine_type
|
|
custom_cpu = args.custom_cpu
|
|
custom_memory = args.custom_memory
|
|
vm_type = getattr(args, 'custom_vm_type', None)
|
|
ext = getattr(args, 'custom_extensions', None)
|
|
|
|
# Setting the machine type
|
|
machine_type_name = InterpretMachineType(
|
|
machine_type=machine_type,
|
|
custom_cpu=custom_cpu,
|
|
custom_memory=custom_memory,
|
|
ext=ext,
|
|
vm_type=vm_type,
|
|
confidential_vm_type=confidential_vm_type)
|
|
|
|
return machine_type_name
|
|
|
|
|
|
def ParseMachineType(resource_parser, machine_type_name, project, location,
|
|
scope):
|
|
"""Returns the location-specific machine type uri."""
|
|
if scope == compute_scopes.ScopeEnum.ZONE:
|
|
collection = 'compute.machineTypes'
|
|
params = {'project': project, 'zone': location}
|
|
elif scope == compute_scopes.ScopeEnum.REGION:
|
|
collection = 'compute.regionMachineTypes'
|
|
params = {'project': project, 'region': location}
|
|
machine_type_uri = resource_parser.Parse(
|
|
machine_type_name, collection=collection, params=params).SelfLink()
|
|
return machine_type_uri
|
|
|
|
|
|
def GetConfidentialVmType(args, support_confidential_compute_type):
|
|
"""Returns the CONFIDENTIAL_VM_TYPES of the machine."""
|
|
confidential_vm_type = None
|
|
|
|
if args.IsSpecified('confidential_compute') and args.confidential_compute:
|
|
confidential_vm_type = constants.CONFIDENTIAL_VM_TYPES.SEV
|
|
|
|
if (support_confidential_compute_type
|
|
and args.IsSpecified('confidential_compute_type')
|
|
and args.confidential_compute_type is not None):
|
|
confidential_vm_type = getattr(constants.CONFIDENTIAL_VM_TYPES,
|
|
args.confidential_compute_type.upper())
|
|
|
|
return confidential_vm_type
|
|
|
|
|
|
def GetCanIpForward(args, skip_defaults):
|
|
if skip_defaults and not args.IsSpecified('can_ip_forward'):
|
|
return None
|
|
return args.can_ip_forward
|
|
|
|
|
|
def GetTags(args, client):
|
|
if args.tags:
|
|
return client.messages.Tags(items=args.tags)
|
|
return None
|
|
|
|
|
|
def GetLabels(args, client, instance_properties=False):
|
|
"""Gets labels for the instance message."""
|
|
labels_value = client.messages.Instance.LabelsValue
|
|
if instance_properties:
|
|
labels_value = client.messages.InstanceProperties.LabelsValue
|
|
if args.labels:
|
|
return labels_value(additionalProperties=[
|
|
labels_value.AdditionalProperty(key=key, value=value)
|
|
for key, value in sorted(six.iteritems(args.labels))
|
|
])
|
|
return None
|
|
|
|
|
|
def ParseAcceleratorType(accelerator_type_name, resource_parser, project,
|
|
location, scope):
|
|
"""Returns accelerator type ref based on location scope."""
|
|
if scope == compute_scopes.ScopeEnum.ZONE:
|
|
collection = 'compute.acceleratorTypes'
|
|
params = {'project': project, 'zone': location}
|
|
elif scope == compute_scopes.ScopeEnum.REGION:
|
|
collection = 'compute.regionAcceleratorTypes'
|
|
params = {'project': project, 'region': location}
|
|
accelerator_type = resource_parser.Parse(
|
|
accelerator_type_name, collection=collection, params=params).SelfLink()
|
|
return accelerator_type
|
|
|
|
|
|
def ResolveSnapshotURI(user_project, snapshot, resource_parser, region=None):
|
|
"""Returns snapshot URI based on location scope."""
|
|
if user_project and snapshot and resource_parser:
|
|
if region:
|
|
snapshot_ref = resource_parser.Parse(
|
|
snapshot,
|
|
collection='compute.regionSnapshots',
|
|
params={'project': user_project, 'region': region},
|
|
)
|
|
return snapshot_ref.SelfLink()
|
|
|
|
snapshot_ref = resource_parser.Parse(
|
|
snapshot,
|
|
collection='compute.snapshots',
|
|
params={'project': user_project})
|
|
return snapshot_ref.SelfLink()
|
|
return None
|
|
|
|
|
|
def ResolveInstantSnapshotURI(user_project, instant_snapshot, resource_parser):
|
|
if user_project and instant_snapshot and resource_parser:
|
|
instant_snapshot_ref = resource_parser.Parse(
|
|
instant_snapshot,
|
|
collection='compute.instantSnapshots',
|
|
params={'project': user_project},
|
|
)
|
|
return instant_snapshot_ref.SelfLink()
|
|
return None
|
|
|
|
|
|
def GetReservationAffinity(args, client, support_specific_then_x_affinity):
|
|
"""Returns the message of reservation affinity for the instance."""
|
|
if args.IsSpecified('reservation_affinity'):
|
|
type_msgs = (
|
|
client.messages.ReservationAffinity
|
|
.ConsumeReservationTypeValueValuesEnum)
|
|
|
|
reservation_key = None
|
|
reservation_values = []
|
|
|
|
specific_affinities_map = {
|
|
'specific': type_msgs.SPECIFIC_RESERVATION,
|
|
}
|
|
if support_specific_then_x_affinity:
|
|
specific_affinities_map.update({
|
|
'specific-then-any-reservation': (
|
|
type_msgs.SPECIFIC_THEN_ANY_RESERVATION
|
|
),
|
|
'specific-then-no-reservation': (
|
|
type_msgs.SPECIFIC_THEN_NO_RESERVATION
|
|
),
|
|
})
|
|
|
|
if args.reservation_affinity == 'none':
|
|
reservation_type = type_msgs.NO_RESERVATION
|
|
elif args.reservation_affinity in specific_affinities_map:
|
|
reservation_type = specific_affinities_map[args.reservation_affinity]
|
|
# Currently, the key is fixed and the value is the name of the
|
|
# reservation.
|
|
# The value being a repeated field is reserved for future use when user
|
|
# can specify more than one reservation names from which the VM can take
|
|
# capacity from.
|
|
reservation_key = _RESERVATION_AFFINITY_KEY
|
|
reservation_values = [args.reservation]
|
|
else:
|
|
reservation_type = type_msgs.ANY_RESERVATION
|
|
|
|
return client.messages.ReservationAffinity(
|
|
consumeReservationType=reservation_type,
|
|
key=reservation_key or None,
|
|
values=reservation_values)
|
|
|
|
return None
|
|
|
|
|
|
def GetNetworkPerformanceConfig(args, client):
|
|
"""Get NetworkPerformanceConfig message for the instance."""
|
|
|
|
network_perf_args = getattr(args, 'network_performance_configs', [])
|
|
network_perf_configs = client.messages.NetworkPerformanceConfig()
|
|
|
|
for config in network_perf_args:
|
|
total_tier = config.get('total-egress-bandwidth-tier', '').upper()
|
|
if total_tier:
|
|
network_perf_configs.totalEgressBandwidthTier = client.messages.NetworkPerformanceConfig.TotalEgressBandwidthTierValueValuesEnum(
|
|
total_tier)
|
|
|
|
return network_perf_configs
|
|
|
|
|
|
def ExtractGracefulShutdownFromArgs(args, support_graceful_shutdown=False):
|
|
"""Extracts graceful shutdown from args."""
|
|
|
|
graceful_shutdown = None
|
|
if support_graceful_shutdown:
|
|
if hasattr(args, 'graceful_shutdown') and args.IsSpecified(
|
|
'graceful_shutdown'
|
|
):
|
|
graceful_shutdown = {'enabled': args.graceful_shutdown}
|
|
|
|
if hasattr(args, 'graceful_shutdown_max_duration') and args.IsSpecified(
|
|
'graceful_shutdown_max_duration'
|
|
):
|
|
if graceful_shutdown is None:
|
|
graceful_shutdown = {'maxDuration': args.graceful_shutdown_max_duration}
|
|
else:
|
|
graceful_shutdown['maxDuration'] = args.graceful_shutdown_max_duration
|
|
|
|
return graceful_shutdown
|
|
|
|
|
|
def CreateParams(args, client):
|
|
"""Create a Params message for the instance."""
|
|
|
|
params = client.messages.InstanceParams()
|
|
|
|
if args.IsKnownAndSpecified('resource_manager_tags'):
|
|
ret_resource_manager_tags = (
|
|
resource_manager_tags_utils.GetResourceManagerTags(
|
|
args.resource_manager_tags
|
|
)
|
|
)
|
|
if ret_resource_manager_tags is not None:
|
|
resource_manager_tags_value = (
|
|
client.messages.InstanceParams.ResourceManagerTagsValue
|
|
)
|
|
params.resourceManagerTags = resource_manager_tags_value(
|
|
additionalProperties=[
|
|
resource_manager_tags_value.AdditionalProperty(
|
|
key=key, value=value
|
|
)
|
|
for key, value in sorted(six.iteritems(ret_resource_manager_tags))
|
|
]
|
|
)
|
|
|
|
if args.IsKnownAndSpecified('request_valid_for_duration'):
|
|
if (
|
|
not hasattr(args, 'provisioning_model')
|
|
or not args.IsSpecified('provisioning_model')
|
|
or args.provisioning_model != 'FLEX_START'
|
|
):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--request_valid_for_duration',
|
|
'[--request_valid_for_duration] is only supported for FLEX_START'
|
|
' provisioning model.',
|
|
)
|
|
|
|
params.requestValidForDuration = client.messages.Duration(
|
|
seconds=args.request_valid_for_duration
|
|
)
|
|
|
|
return params
|
|
|
|
|
|
_RESERVATION_AFFINITY_KEY = 'compute.googleapis.com/reservation-name'
|