# -*- coding: utf-8 -*- # # Copyright 2017 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. """Flags and helpers for the compute commitments commands.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import datetime from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.command_lib.compute import completers as compute_completers from googlecloudsdk.command_lib.compute import flags as compute_flags from googlecloudsdk.command_lib.compute import scope as compute_scope from googlecloudsdk.command_lib.compute.instances import flags as instance_flags from googlecloudsdk.command_lib.compute.reservations import flags as reservation_flags from googlecloudsdk.command_lib.compute.reservations import resource_args from googlecloudsdk.command_lib.util.apis import arg_utils import pytz VALID_PLANS = ['12-month', '36-month'] EXTENDED_VALID_PLANS = ['12-month', '24-month', '36-month', '60-month'] VALID_UPDATE_PLANS = ['36-month'] EXTENDED_VALID_UPDATE_PLANS = ['24-month', '36-month', '60-month'] RFC3339_FORMAT = '%Y-%m-%dT%H:%M:%SZ' DATE_ONLY_FORMAT = '%Y-%m-%d' _REQUIRED_RESOURCES = sorted(['vcpu', 'memory']) class RegionCommitmentsCompleter(compute_completers.ListCommandCompleter): def __init__(self, **kwargs): super(RegionCommitmentsCompleter, self).__init__( collection='compute.regionCommitments', list_command='alpha compute commitments list --uri', **kwargs ) def _GetFlagToPlanMap(messages): result = { '12-month': messages.Commitment.PlanValueValuesEnum.TWELVE_MONTH, '36-month': messages.Commitment.PlanValueValuesEnum.THIRTY_SIX_MONTH, } if hasattr(messages.Commitment.PlanValueValuesEnum, 'TWENTY_FOUR_MONTH'): result['24-month'] = ( messages.Commitment.PlanValueValuesEnum.TWENTY_FOUR_MONTH ) if hasattr(messages.Commitment.PlanValueValuesEnum, 'SIXTY_MONTH'): result['60-month'] = messages.Commitment.PlanValueValuesEnum.SIXTY_MONTH return result def TranslatePlanArg(messages, plan_arg): return _GetFlagToPlanMap(messages)[plan_arg] def TranslateAutoRenewArgForCreate(args): if args.IsSpecified('auto_renew'): return args.auto_renew return False def TranslateAutoRenewArgForUpdate(args): if args.IsSpecified('auto_renew'): return args.auto_renew return None def TranslateCustomEndTimeArg(args): """Translates the custom end time arg to a RFC3339 format.""" if args.IsSpecified('custom_end_time'): # make sure if along with the RFC3339 format. try_date_only_parse = False final_date_time = None try: datetime.datetime.strptime(args.custom_end_time, RFC3339_FORMAT) final_date_time = args.custom_end_time except ValueError: # Swallow the exception and try to parse the date string in the format of # YYYY-MM-DD try_date_only_parse = True if try_date_only_parse: try: # try to parse the date string in the format of YYYY-MM-DD tz = pytz.timezone('America/Los_Angeles') midnight_date_time_mtv = tz.localize( datetime.datetime.strptime(args.custom_end_time, DATE_ONLY_FORMAT) ) final_date_time = midnight_date_time_mtv.astimezone(pytz.utc).strftime( RFC3339_FORMAT ) except ValueError: # Swallow the exception and throw canonical one. pass if not final_date_time: raise ValueError( 'Invalid custom end time. Expected format: YYYY-MM-DD or' ' YYYY-MM-DDTHH:MM:SSZ' ) return final_date_time return None def TranslateResourcesArg(messages, resources_arg): return [ messages.ResourceCommitment( amount=resources_arg['vcpu'], type=messages.ResourceCommitment.TypeValueValuesEnum.VCPU, ), # Arg is in B API accepts values in MB. messages.ResourceCommitment( amount=resources_arg['memory'] // (1024 * 1024), type=messages.ResourceCommitment.TypeValueValuesEnum.MEMORY, ), ] def TranslateResourcesArgGroup(messages, args): """Util functions to parse ResourceCommitments.""" resources_arg = args.resources resources = TranslateResourcesArg(messages, resources_arg) if 'local-ssd' in resources_arg.keys(): resources.append( messages.ResourceCommitment( amount=resources_arg['local-ssd'], type=messages.ResourceCommitment.TypeValueValuesEnum.LOCAL_SSD, ) ) if args.IsSpecified('resources_accelerator'): accelerator_arg = args.resources_accelerator resources.append( messages.ResourceCommitment( amount=accelerator_arg['count'], acceleratorType=accelerator_arg['type'], type=messages.ResourceCommitment.TypeValueValuesEnum.ACCELERATOR, ) ) return resources def TranslateMergeArg(arg): """List arguments are delineated by a comma.""" return arg.split(',') if arg else [] def MakeCommitmentArg(plural): return compute_flags.ResourceArgument( resource_name='commitment', completer=RegionCommitmentsCompleter, plural=plural, name='commitment', regional_collection='compute.regionCommitments', region_explanation=compute_flags.REGION_PROPERTY_EXPLANATION, ) def AddCreateFlags( parser, support_share_setting=False, support_stable_fleet=False, support_existing_reservation=False, support_reservation_sharing_policy=False, support_60_month_plan=False, support_24_month_plan=False, ): """Add general arguments for `commitments create` flag.""" AddPlanForCreate(parser, support_60_month_plan, support_24_month_plan) AddReservationArgGroup( parser, support_share_setting, support_stable_fleet, support_existing_reservation, support_reservation_sharing_policy, ) AddResourcesArgGroup(parser) AddSplitSourceCommitment(parser) AddMergeSourceCommitments(parser) AddCustomEndTime(parser) def AddUpdateFlags( parser, support_60_month_plan=False, support_24_month_plan=False ): """Add general arguments for `commitments update` flag.""" AddAutoRenew(parser) AddPlanForUpdate(parser, support_60_month_plan, support_24_month_plan) def AddPlanForCreate(parser, support_60_month_plan, support_24_month_plan): return parser.add_argument( '--plan', required=True, choices=EXTENDED_VALID_PLANS if support_60_month_plan or support_24_month_plan else VALID_PLANS, help='Duration of the commitment.', ) def AddPlanForUpdate(parser, support_60_month_plan, support_24_month_plan): return parser.add_argument( '--plan', required=False, choices=EXTENDED_VALID_UPDATE_PLANS if support_60_month_plan or support_24_month_plan else VALID_UPDATE_PLANS, help='Duration of the commitment.', ) def AddAutoRenew(parser): return parser.add_argument( '--auto-renew', action='store_true', default=False, help='Enable auto renewal for the commitment.', ) def AddLicenceBasedFlags(parser): """Add license based flags for `commitments create` flag.""" parser.add_argument( '--license', required=True, help=( 'Applicable license URI. For example: ' '`https://www.googleapis.com/compute/v1/projects/suse-sap-cloud/global/licenses/sles-sap-12`' ), ) # pylint:disable=line-too-long parser.add_argument( '--cores-per-license', required=False, type=str, help=( 'Core range of the instance. Must be one of: `1-2`,' ' `3-4`, `5+`. Required for SAP licenses.' ), ) parser.add_argument( '--amount', required=True, type=int, help='Number of licenses purchased.' ) AddPlanForCreate( parser, support_60_month_plan=False, support_24_month_plan=False ) def AddSplitSourceCommitment(parser): return parser.add_argument( '--split-source-commitment', required=False, help=( 'Creates the new commitment by splitting the specified ' 'source commitment and redistributing the specified resources.' ), ) def AddMergeSourceCommitments(parser): return parser.add_argument( '--merge-source-commitments', required=False, help=( 'Creates the new commitment by merging the specified ' 'source commitments and combining their resources.' ), ) def AddCustomEndTime(parser): return parser.add_argument( '--custom-end-time', required=False, type=str, help=( "Specifies a custom future end date and extends the commitment's" ' ongoing term.' ), ) def AddResourcesArgGroup(parser): """Add the argument group for ResourceCommitment support in commitment.""" resources_group = parser.add_group( 'Manage the commitment for particular resources.', required=True ) resources_help = """\ Resources to be included in the commitment. For details and examples of valid specifications, refer to the [custom machine type guide](https://cloud.google.com/compute/docs/instances/creating-instance-with-custom-machine-type#specifications). *memory*::: The size of the memory, should include units (e.g. 3072MB or 9GB). If no units are specified, GB is assumed. *vcpu*::: The number of the vCPU cores. *local-ssd*::: The size of local SSD. """ resources_group.add_argument( '--resources', help=resources_help, type=arg_parsers.ArgDict( spec={ 'vcpu': int, 'local-ssd': int, 'memory': arg_parsers.BinarySize(), } ), ) accelerator_help = """\ Manage the configuration of the type and number of accelerator cards to include in the commitment. *count*::: The number of accelerators to include. *type*::: The specific type (e.g. nvidia-tesla-k80 for NVIDIA Tesla K80) of the accelerator. Use `gcloud compute accelerator-types list` to learn about all available accelerator types. """ resources_group.add_argument( '--resources-accelerator', help=accelerator_help, type=arg_parsers.ArgDict(spec={'count': int, 'type': str}), ) def GetTypeMapperFlag(messages): """Helper to get a choice flag from the commitment type enum.""" return arg_utils.ChoiceEnumMapper( '--type', messages.Commitment.TypeValueValuesEnum, help_str=( 'Type of commitment. `memory-optimized` indicates that the ' 'commitment is for memory-optimized VMs.' ), default='general-purpose', include_filter=lambda x: x != 'TYPE_UNSPECIFIED', ) def AddUpdateReservationGroup(parser): """Add reservation arguments to the update-reservations command.""" parent_reservations_group = parser.add_group( 'Manage reservations that are attached to the commitment.', mutex=True ) AddReservationsFromFileFlag( parent_reservations_group, custom_text="Path to a YAML file of two reservations' configuration.", ) reservations_group = parent_reservations_group.add_group( 'Specify source and destination reservations configuration.' ) AddReservationArguments(reservations_group) reservation_flags.GetAcceleratorFlag('--source-accelerator').AddToParser( reservations_group ) reservation_flags.GetAcceleratorFlag('--dest-accelerator').AddToParser( reservations_group ) reservation_flags.GetLocalSsdFlag('--source-local-ssd').AddToParser( reservations_group ) reservation_flags.GetLocalSsdFlag('--dest-local-ssd').AddToParser( reservations_group ) # Add share-setting and share-with flags. reservation_flags.GetSharedSettingFlag('--source-share-setting').AddToParser( reservations_group ) reservation_flags.GetShareWithFlag('--source-share-with').AddToParser( reservations_group ) reservation_flags.GetSharedSettingFlag('--dest-share-setting').AddToParser( reservations_group ) reservation_flags.GetShareWithFlag('--dest-share-with').AddToParser( reservations_group ) return parser def AddReservationArguments(parser): """Add --source-reservation and --dest-reservation arguments to parser.""" help_text = """ {0} reservation configuration. *reservation*::: Name of the {0} reservation to operate on. *reservation-zone*::: Zone of the {0} reservation to operate on. *vm-count*::: The number of VM instances that are allocated to this reservation. The value of this field must be an int in the range [1, 1000]. *machine-type*::: The type of machine (name only) which has a fixed number of vCPUs and a fixed amount of memory. This also includes specifying custom machine type following `custom-number_of_CPUs-amount_of_memory` pattern, e.g. `custom-32-29440`. *min-cpu-platform*::: Optional minimum CPU platform of the reservation to create. *require-specific-reservation*::: Indicates whether the reservation can be consumed by VMs with "any reservation" defined. If enabled, then only VMs that target this reservation by name using `--reservation-affinity=specific` can consume from this reservation. """ reservation_spec = { 'reservation': str, 'reservation-zone': str, 'vm-count': int, 'machine-type': str, 'min-cpu-platform': str, 'require-specific-reservation': bool, } parser.add_argument( '--source-reservation', type=arg_parsers.ArgDict(spec=reservation_spec), help=help_text.format('source'), required=True, ) parser.add_argument( '--dest-reservation', type=arg_parsers.ArgDict(spec=reservation_spec), help=help_text.format('destination'), required=True, ) return parser def AddReservationsFromFileFlag(parser, custom_text=None): help_text = ( custom_text if custom_text else "Path to a YAML file of multiple reservations' configuration." ) return parser.add_argument( '--reservations-from-file', type=arg_parsers.FileContents(), help=help_text, ) def AddExistingReservationFlag(parser): """Add --existing-reservation argument to parser.""" help_text = """ Details of the existing on-demand reservation or auto-created future reservation that you want to attach to your commitment. Specify a new instance of this flag for every existing reservation that you want to attach. The reservations must be in the same region as the commitment. *name*::: The name of the reservation. *zone*::: The zone of the reservation. For example, to attach an existing reservation named reservation-name in the zone reservation-zone, use the following text: --existing-reservation=name=reservation-name,zone=reservation-zone """ return parser.add_argument( '--existing-reservation', type=arg_parsers.ArgDict( spec={'name': str, 'zone': str}, required_keys=['name', 'zone'] ), action='append', help=help_text, ) def ResolveExistingReservationArgs(args, resources): """Method to translate existing-reservations args into URLs.""" resolver = compute_flags.ResourceResolver.FromMap( 'reservation', {compute_scope.ScopeEnum.ZONE: 'compute.reservations'} ) existing_reservations = getattr(args, 'existing_reservation', None) if existing_reservations is None: return [] reservation_urls = [] for reservation in existing_reservations: reservation_ref = resolver.ResolveResources( [reservation['name']], compute_scope.ScopeEnum.ZONE, reservation['zone'], resources, )[0] reservation_urls.append(reservation_ref.SelfLink()) return reservation_urls def AddReservationArgGroup( parser, support_share_setting=False, support_stable_fleet=False, support_existing_reservations=False, support_reservation_sharing_policy=False, ): """Adds all flags needed for reservations creation.""" reservations_manage_group = parser.add_group( 'Manage the reservations to be created with the commitment.', mutex=True ) AddReservationsFromFileFlag(reservations_manage_group) if support_existing_reservations: AddExistingReservationFlag(reservations_manage_group) single_reservation_group = reservations_manage_group.add_argument_group( help='Manage the reservation to be created with the commitment.' ) resource_args.GetReservationResourceArg(positional=False).AddArgument( single_reservation_group ) single_reservation_group.add_argument( '--reservation-type', hidden=True, choices=['specific'], default='specific', help='The type of the reservation to be created.', ) specific_sku_reservation_group = single_reservation_group.add_argument_group( help='Manage the specific SKU reservation properties to create.' ) AddFlagsToSpecificSkuGroup( specific_sku_reservation_group, support_stable_fleet ) if support_share_setting: share_setting_reservation_group = ( single_reservation_group.add_argument_group( help='Manage the properties of a shared reservation to create', required=False, ) ) AddFlagsToShareSettingGroup(share_setting_reservation_group) if support_reservation_sharing_policy: reservation_sharing_policy_group = single_reservation_group.add_argument_group( help='Manage the properties of a reservation sharing policy to create', required=False, ) AddFlagsToReservationSharingPolicyGroup(reservation_sharing_policy_group) def AddFlagsToReservationSharingPolicyGroup(group): """Adds flags needed for a reservation sharing policy.""" args = [ reservation_flags.GetReservationSharingPolicyFlag(), ] for arg in args: arg.AddToParser(group) def AddFlagsToSpecificSkuGroup(group, support_stable_fleet=False): """Adds flags needed for a specific sku zonal allocation.""" args = [ reservation_flags.GetRequireSpecificAllocation(), reservation_flags.GetVmCountFlag(required=False), reservation_flags.GetMinCpuPlatform(), reservation_flags.GetMachineType(required=False), reservation_flags.GetLocalSsdFlag(), reservation_flags.GetAcceleratorFlag(), reservation_flags.GetResourcePolicyFlag(), ] if support_stable_fleet: args.append(instance_flags.AddMaintenanceInterval()) for arg in args: arg.AddToParser(group) def AddFlagsToShareSettingGroup(group): """Adds flags needed for an allocation with share setting.""" args = [ reservation_flags.GetSharedSettingFlag(), reservation_flags.GetShareWithFlag(), ] for arg in args: arg.AddToParser(group)