1# -*- coding: utf-8 -*- #
2# Copyright 2014 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Convenience functions for dealing with instances and instance templates."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import collections
22import re
23
24from googlecloudsdk.api_lib.compute import constants
25from googlecloudsdk.api_lib.compute import containers_utils
26from googlecloudsdk.api_lib.compute import csek_utils
27from googlecloudsdk.api_lib.compute import metadata_utils
28from googlecloudsdk.api_lib.compute import utils
29from googlecloudsdk.api_lib.compute import zone_utils
30from googlecloudsdk.calliope import exceptions as calliope_exceptions
31from googlecloudsdk.command_lib.compute import flags as compute_flags
32from googlecloudsdk.command_lib.compute import scope as compute_scopes
33from googlecloudsdk.command_lib.compute.instances import flags
34from googlecloudsdk.command_lib.compute.sole_tenancy import util as sole_tenancy_util
35from googlecloudsdk.core import log
36from googlecloudsdk.core import resources as cloud_resources
37import six
38
39EMAIL_REGEX = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)')
40
41_DEFAULT_DEVICE_NAME_CONTAINER_WARNING = (
42    'Default device-name for disk name [{0}] will be [{0}] because it is being '
43    'mounted to a container with [`--container-mount-disk`]')
44
45
46def GetCpuRamVmFamilyFromCustomName(name):
47  """Gets the CPU and memory specs from the custom machine type name.
48
49  Args:
50    name: the custom machine type name for the 'instance create' call
51
52  Returns:
53    A three-tuple with the vm family, number of cpu and amount of memory for the
54    custom machine type.
55    custom_family, the name of the VM family
56    custom_cpu, the number of cpu desired for the custom machine type instance
57    custom_memory_mib, the amount of ram desired in MiB for the custom machine
58      type instance
59    None for both variables otherwise
60  """
61  check_custom = re.search('([a-zA-Z0-9]+)-custom-([0-9]+)-([0-9]+)', name)
62  if check_custom:
63    custom_family = check_custom.group(1)
64    custom_cpu = check_custom.group(2)
65    custom_memory_mib = check_custom.group(3)
66    return custom_family, custom_cpu, custom_memory_mib
67  return None, None, None
68
69
70def GetNameForCustom(custom_cpu, custom_memory_mib, ext=False, vm_type=False):
71  """Creates a custom machine type name from the desired CPU and memory specs.
72
73  Args:
74    custom_cpu: the number of cpu desired for the custom machine type
75    custom_memory_mib: the amount of ram desired in MiB for the custom machine
76      type instance
77    ext: extended custom machine type should be used if true
78    vm_type: VM instance generation
79
80  Returns:
81    The custom machine type name for the 'instance create' call
82  """
83  if vm_type:
84    machine_type = '{0}-custom-{1}-{2}'.format(vm_type, custom_cpu,
85                                               custom_memory_mib)
86  else:
87    machine_type = 'custom-{0}-{1}'.format(custom_cpu, custom_memory_mib)
88  if ext:
89    machine_type += '-ext'
90  return machine_type
91
92
93def InterpretMachineType(machine_type,
94                         custom_cpu,
95                         custom_memory,
96                         ext=True,
97                         vm_type=False,
98                         confidential_vm=False):
99  """Interprets the machine type for the instance.
100
101  Args:
102    machine_type: name of existing machine type, eg. n1-standard
103    custom_cpu: number of CPU cores for custom machine type,
104    custom_memory: amount of RAM memory in bytes for custom machine type,
105    ext: extended custom machine type should be used if true,
106    vm_type:  VM instance generation
107    confidential_vm: If True, default machine type is different for confidential
108      VMs.
109
110  Returns:
111    A string representing the URL naming a machine-type.
112
113  Raises:
114    calliope_exceptions.RequiredArgumentException when only one of the two
115      custom machine type flags are used.
116    calliope_exceptions.InvalidArgumentException when both the machine type and
117      custom machine type flags are used to generate a new instance.
118  """
119  # Setting the machine type
120  if machine_type:
121    machine_type_name = machine_type
122  elif confidential_vm:
123    machine_type_name = constants.DEFAULT_MACHINE_TYPE_FOR_CONFIDENTIAL_VMS
124  else:
125    machine_type_name = constants.DEFAULT_MACHINE_TYPE
126
127  # Setting the specs for the custom machine.
128  if custom_cpu or custom_memory or ext:
129    if not custom_cpu:
130      raise calliope_exceptions.RequiredArgumentException(
131          '--custom-cpu', 'Both [--custom-cpu] and [--custom-memory] must be '
132          'set to create a custom machine type instance.')
133    if not custom_memory:
134      raise calliope_exceptions.RequiredArgumentException(
135          '--custom-memory', 'Both [--custom-cpu] and [--custom-memory] must '
136          'be set to create a custom machine type instance.')
137    if machine_type:
138      raise calliope_exceptions.InvalidArgumentException(
139          '--machine-type', 'Cannot set both [--machine-type] and '
140          '[--custom-cpu]/[--custom-memory] for the same instance.')
141    custom_type_string = GetNameForCustom(
142        custom_cpu,
143        # converting from B to MiB.
144        custom_memory // (2**20),
145        ext,
146        vm_type)
147
148    # Updating the machine type that is set for the URIs
149    machine_type_name = custom_type_string
150  return machine_type_name
151
152
153def CheckCustomCpuRamRatio(compute_client, project, zone, machine_type_name):
154  """Checks that the CPU and memory ratio is a supported custom instance type.
155
156  Args:
157    compute_client: GCE API client,
158    project: a project,
159    zone: the zone of the instance(s) being created,
160    machine_type_name: The machine type of the instance being created.
161
162  Returns:
163    Nothing. Function acts as a bound checker, and will raise an exception from
164      within the function if needed.
165
166  Raises:
167    utils.RaiseToolException if a custom machine type ratio is out of bounds.
168  """
169  messages = compute_client.messages
170  compute = compute_client.apitools_client
171  if 'custom' in machine_type_name:
172    mt_get_pb = messages.ComputeMachineTypesGetRequest(
173        machineType=machine_type_name, project=project, zone=zone)
174    mt_get_reqs = [(compute.machineTypes, 'Get', mt_get_pb)]
175    errors = []
176
177    # Makes a 'machine-types describe' request to check the bounds
178    _ = list(
179        compute_client.MakeRequests(
180            requests=mt_get_reqs, errors_to_collect=errors))
181
182    if errors:
183      utils.RaiseToolException(
184          errors, error_message='Could not fetch machine type:')
185
186
187def CreateServiceAccountMessages(messages, scopes, service_account):
188  """Returns a list of ServiceAccount messages corresponding to scopes."""
189  if scopes is None:
190    scopes = constants.DEFAULT_SCOPES
191  if service_account is None:
192    service_account = 'default'
193
194  accounts_to_scopes = collections.defaultdict(list)
195  for scope in scopes:
196    parts = scope.split('=')
197    if len(parts) == 1:
198      account = service_account
199      scope_uri = scope
200    elif len(parts) == 2:
201      # TODO(b/33688878) Remove exception for this deprecated format
202      raise calliope_exceptions.InvalidArgumentException(
203          '--scopes',
204          'Flag format --scopes [ACCOUNT=]SCOPE,[[ACCOUNT=]SCOPE, ...] is '
205          'removed. Use --scopes [SCOPE,...] --service-account ACCOUNT '
206          'instead.')
207    else:
208      raise calliope_exceptions.ToolException(
209          '[{0}] is an illegal value for [--scopes]. Values must be of the '
210          'form [SCOPE].'.format(scope))
211
212    if service_account != 'default' and not EMAIL_REGEX.match(service_account):
213      raise calliope_exceptions.InvalidArgumentException(
214          '--service-account',
215          'Invalid format: expected default or user@domain.com, received ' +
216          service_account)
217
218    # Expands the scope if the user provided an alias like
219    # "compute-rw".
220    scope_uri = constants.SCOPES.get(scope_uri, [scope_uri])
221    accounts_to_scopes[account].extend(scope_uri)
222
223  if not scopes and service_account != 'default':
224    return [messages.ServiceAccount(email=service_account, scopes=[])]
225  res = []
226  for account, scopes in sorted(six.iteritems(accounts_to_scopes)):
227    res.append(messages.ServiceAccount(email=account, scopes=sorted(scopes)))
228  return res
229
230
231def CreateOnHostMaintenanceMessage(messages, maintenance_policy):
232  """Create on-host-maintenance message for VM."""
233  if maintenance_policy:
234    on_host_maintenance = messages.Scheduling.OnHostMaintenanceValueValuesEnum(
235        maintenance_policy)
236  else:
237    on_host_maintenance = None
238  return on_host_maintenance
239
240
241def CreateSchedulingMessage(messages,
242                            maintenance_policy,
243                            preemptible,
244                            restart_on_failure,
245                            node_affinities=None,
246                            min_node_cpu=None,
247                            location_hint=None,
248                            maintenance_freeze_duration=None,
249                            maintenance_interval=None):
250  """Create scheduling message for VM."""
251  # Note: We always specify automaticRestart=False for preemptible VMs. This
252  # makes sense, since no-restart-on-failure is defined as "store-true", and
253  # thus can't be given an explicit value. Hence it either has its default
254  # value (in which case we override it for convenience's sake to the only
255  # setting that makes sense for preemptible VMs), or the user actually
256  # specified no-restart-on-failure, the only usable setting.
257  on_host_maintenance = CreateOnHostMaintenanceMessage(messages,
258                                                       maintenance_policy)
259  if preemptible:
260    scheduling = messages.Scheduling(
261        automaticRestart=False,
262        onHostMaintenance=on_host_maintenance,
263        preemptible=True)
264  else:
265    scheduling = messages.Scheduling(
266        automaticRestart=restart_on_failure,
267        onHostMaintenance=on_host_maintenance)
268  if node_affinities:
269    scheduling.nodeAffinities = node_affinities
270
271  if min_node_cpu is not None:
272    scheduling.minNodeCpus = int(min_node_cpu)
273
274  if location_hint:
275    scheduling.locationHint = location_hint
276
277  if maintenance_freeze_duration:
278    scheduling.maintenanceFreezeDurationHours = \
279      maintenance_freeze_duration // 3600  # sec to hour
280
281  if maintenance_interval:
282    scheduling.maintenanceInterval = messages.\
283      Scheduling.MaintenanceIntervalValueValuesEnum(maintenance_interval)
284  return scheduling
285
286
287def CreateShieldedInstanceConfigMessage(messages, enable_secure_boot,
288                                        enable_vtpm,
289                                        enable_integrity_monitoring):
290  """Create shieldedInstanceConfig message for VM."""
291
292  shielded_instance_config = messages.ShieldedInstanceConfig(
293      enableSecureBoot=enable_secure_boot,
294      enableVtpm=enable_vtpm,
295      enableIntegrityMonitoring=enable_integrity_monitoring)
296
297  return shielded_instance_config
298
299
300def CreateShieldedInstanceIntegrityPolicyMessage(messages,
301                                                 update_auto_learn_policy=True):
302  """Creates shieldedInstanceIntegrityPolicy message for VM."""
303
304  shielded_instance_integrity_policy = messages.ShieldedInstanceIntegrityPolicy(
305      updateAutoLearnPolicy=update_auto_learn_policy)
306
307  return shielded_instance_integrity_policy
308
309
310def CreateConfidentialInstanceMessage(messages, enable_confidential_compute):
311  """Create confidentialInstanceConfig message for VM."""
312  confidential_instance_config = messages.ConfidentialInstanceConfig(
313      enableConfidentialCompute=enable_confidential_compute)
314
315  return confidential_instance_config
316
317
318def CreateAdvancedMachineFeaturesMessage(messages,
319                                         enable_nested_virtualization=None,
320                                         threads_per_core=None):
321  """Create AdvancedMachineFeatures message for an Instance."""
322  # Start with an empty AdvancedMachineFeatures and optionally add on
323  # the features we have like CreateSchedulingMessage does. This lets us
324  # treat None as also "not supported in this version of the API (yet)".
325  features = messages.AdvancedMachineFeatures()
326
327  if enable_nested_virtualization is not None:
328    features.enableNestedVirtualization = enable_nested_virtualization
329
330  if threads_per_core is not None:
331    features.threadsPerCore = threads_per_core
332
333  return features
334
335
336def ParseDiskResource(resources, name, project, zone, type_):
337  """Parses disk resources.
338
339  Project and zone are ignored if a fully-qualified resource name is given, i.e.
340    - https://compute.googleapis.com/compute/v1/projects/my-project
341          /zones/us-central1-a/disks/disk-1
342    - projects/my-project/zones/us-central1-a/disks/disk-1
343
344  If project and zone cannot be parsed, we will use the given project and zone
345  as fallbacks.
346
347  Args:
348    resources: resources.Registry, The resource registry
349    name: str, name of the disk.
350    project: str, project of the disk.
351    zone: str, zone of the disk.
352    type_: ScopeEnum, type of the disk.
353
354  Returns:
355    A disk resource.
356  """
357  if type_ == compute_scopes.ScopeEnum.REGION:
358    return resources.Parse(
359        name,
360        collection='compute.regionDisks',
361        params={
362            'project': project,
363            'region': utils.ZoneNameToRegionName(zone)
364        })
365  else:
366    return resources.Parse(
367        name,
368        collection='compute.disks',
369        params={
370            'project': project,
371            'zone': zone
372        })
373
374
375def ParseDiskResourceFromAttachedDisk(resources, attached_disk):
376  """Parses the source disk resource of an AttachedDisk.
377
378  The source of an AttachedDisk is either a partial or fully specified URL
379  referencing either a regional or zonal disk.
380
381  Args:
382    resources: resources.Registry, The resource registry
383    attached_disk: AttachedDisk
384
385  Returns:
386    A disk resource.
387
388  Raises:
389    InvalidResourceException: If the attached disk source cannot be parsed as a
390        regional or zonal disk.
391  """
392  try:
393    disk = resources.Parse(attached_disk.source,
394                           collection='compute.regionDisks')
395    if disk:
396      return disk
397  except (cloud_resources.WrongResourceCollectionException,
398          cloud_resources.RequiredFieldOmittedException):
399    pass
400
401  try:
402    disk = resources.Parse(attached_disk.source,
403                           collection='compute.disks')
404    if disk:
405      return disk
406  except (cloud_resources.WrongResourceCollectionException,
407          cloud_resources.RequiredFieldOmittedException):
408    pass
409
410  raise cloud_resources.InvalidResourceException('Unable to parse [{}]'.format(
411      attached_disk.source))
412
413
414def GetDiskDeviceName(disk, name, container_mount_disk):
415  """Helper method to get device-name for a disk message."""
416  if (container_mount_disk and filter(
417      bool, [d.get('name', name) == name for d in container_mount_disk])):
418    # device-name must be the same as name if it is being mounted to a
419    # container.
420    if not disk.get('device-name'):
421      log.warning(_DEFAULT_DEVICE_NAME_CONTAINER_WARNING.format(name))
422      return name
423    # This is defensive only; should be validated before this method is called.
424    elif disk.get('device-name') != name:
425      raise calliope_exceptions.InvalidArgumentException(
426          '--container-mount-disk',
427          'Attempting to mount disk named [{}] with device-name [{}]. If '
428          'being mounted to container, disk name must match device-name.'
429          .format(name, disk.get('device-name')))
430  return disk.get('device-name')
431
432
433def ParseDiskType(resources, disk_type, project, location, scope):
434  """Parses disk type reference based on location scope."""
435  if scope == compute_scopes.ScopeEnum.ZONE:
436    collection = 'compute.diskTypes'
437    params = {'project': project, 'zone': location}
438  elif scope == compute_scopes.ScopeEnum.REGION:
439    collection = 'compute.regionDiskTypes'
440    params = {'project': project, 'region': location}
441  disk_type_ref = resources.Parse(
442      disk_type,
443      collection=collection,
444      params=params)
445  return disk_type_ref
446
447
448def UseExistingBootDisk(disks):
449  """Returns True if the user has specified an existing boot disk."""
450  return any(disk.get('boot') == 'yes' for disk in disks)
451
452
453def IsAnySpecified(args, *dests):
454  return any([args.IsSpecified(dest) for dest in dests])
455
456
457def GetSourceInstanceTemplate(args, resources, source_instance_template_arg):
458  if not args.IsSpecified('source_instance_template'):
459    return None
460  ref = source_instance_template_arg.ResolveAsResource(args, resources)
461  return ref.SelfLink()
462
463
464def GetSkipDefaults(source_instance_template):
465  # gcloud creates default values for some fields in Instance resource
466  # when no value was specified on command line.
467  # When --source-instance-template was specified, defaults are taken from
468  # Instance Template and gcloud flags are used to override them - by default
469  # fields should not be initialized.
470  return source_instance_template is not None
471
472
473def GetScheduling(args,
474                  client,
475                  skip_defaults,
476                  support_node_affinity=False,
477                  support_min_node_cpu=True,
478                  support_location_hint=False):
479  """Generate a Scheduling Message or None based on specified args."""
480  node_affinities = None
481  if support_node_affinity:
482    node_affinities = sole_tenancy_util.GetSchedulingNodeAffinityListFromArgs(
483        args, client.messages)
484  min_node_cpu = None
485  if support_min_node_cpu:
486    min_node_cpu = args.min_node_cpu
487  location_hint = None
488  if support_location_hint:
489    location_hint = args.location_hint
490  if (skip_defaults and not IsAnySpecified(
491      args, 'maintenance_policy', 'preemptible', 'restart_on_failure') and
492      not node_affinities):
493    return None
494  freeze_duration = None
495  if hasattr(args, 'maintenance_freeze_duration') and args.IsSpecified(
496      'maintenance_freeze_duration'):
497    freeze_duration = args.maintenance_freeze_duration
498  maintenance_interval = None
499  if hasattr(args, 'maintenance_interval') and args.IsSpecified(
500      'maintenance_interval'):
501    maintenance_interval = args.maintenance_interval
502  return CreateSchedulingMessage(
503      messages=client.messages,
504      maintenance_policy=args.maintenance_policy,
505      preemptible=args.preemptible,
506      restart_on_failure=args.restart_on_failure,
507      node_affinities=node_affinities,
508      min_node_cpu=min_node_cpu,
509      location_hint=location_hint,
510      maintenance_freeze_duration=freeze_duration,
511      maintenance_interval=maintenance_interval)
512
513
514def GetServiceAccounts(args, client, skip_defaults):
515  if args.no_service_account:
516    service_account = None
517  else:
518    service_account = args.service_account
519  if (skip_defaults and not IsAnySpecified(
520      args, 'scopes', 'no_scopes', 'service_account', 'no_service_account')):
521    return []
522  return CreateServiceAccountMessages(
523      messages=client.messages,
524      scopes=[] if args.no_scopes else args.scopes,
525      service_account=service_account)
526
527
528def GetValidatedMetadata(args, client):
529  user_metadata = metadata_utils.ConstructMetadataMessage(
530      client.messages,
531      metadata=args.metadata,
532      metadata_from_file=args.metadata_from_file)
533  containers_utils.ValidateUserMetadata(user_metadata)
534  return user_metadata
535
536
537def GetMetadata(args, client, skip_defaults):
538  if (skip_defaults and
539      not IsAnySpecified(args, 'metadata', 'metadata_from_file')):
540    return None
541  else:
542    return metadata_utils.ConstructMetadataMessage(
543        client.messages,
544        metadata=args.metadata,
545        metadata_from_file=args.metadata_from_file)
546
547
548def GetBootDiskSizeGb(args):
549  boot_disk_size_gb = utils.BytesToGb(args.boot_disk_size)
550  utils.WarnIfDiskSizeIsTooSmall(boot_disk_size_gb, args.boot_disk_type)
551  return boot_disk_size_gb
552
553
554def GetInstanceRefs(args, client, holder):
555  instance_refs = flags.INSTANCES_ARG.ResolveAsResource(
556      args,
557      holder.resources,
558      scope_lister=compute_flags.GetDefaultScopeLister(client))
559  # Check if the zone is deprecated or has maintenance coming.
560  zone_resource_fetcher = zone_utils.ZoneResourceFetcher(client)
561  zone_resource_fetcher.WarnForZonalCreation(instance_refs)
562  return instance_refs
563
564
565def GetSourceMachineImageKey(args, source_image, compute_client, holder):
566  machine_image_ref = source_image.ResolveAsResource(args, holder.resources)
567  csek_keys = csek_utils.CsekKeyStore.FromFile(
568      args.source_machine_image_csek_key_file, allow_rsa_encrypted=False)
569  disk_key_or_none = csek_utils.MaybeLookupKeyMessage(
570      csek_keys, machine_image_ref, compute_client.apitools_client)
571  return disk_key_or_none
572
573
574def CheckSpecifiedMachineTypeArgs(args, skip_defaults):
575  return (not skip_defaults or
576          IsAnySpecified(args, 'machine_type', 'custom_cpu', 'custom_memory'))
577
578
579def CreateMachineTypeUri(args, compute_client, resource_parser, project,
580                         location, scope, confidential_vm=False):
581  """Create a machine type URI for given args and instance reference."""
582
583  machine_type = args.machine_type
584  custom_cpu = args.custom_cpu
585  custom_memory = args.custom_memory
586  vm_type = getattr(args, 'custom_vm_type', None)
587  ext = getattr(args, 'custom_extensions', None)
588
589  # Setting the machine type
590  machine_type_name = InterpretMachineType(
591      machine_type=machine_type,
592      custom_cpu=custom_cpu,
593      custom_memory=custom_memory,
594      ext=ext,
595      vm_type=vm_type,
596      confidential_vm=confidential_vm)
597
598  # Check to see if the custom machine type ratio is supported
599  CheckCustomCpuRamRatio(compute_client, project, location, machine_type_name)
600
601  machine_type_uri = ParseMachineType(resource_parser, machine_type_name,
602                                      project, location, scope)
603  return machine_type_uri
604
605
606def ParseMachineType(resource_parser, machine_type_name, project, location,
607                     scope):
608  """Returns the location-specific machine type uri."""
609  if scope == compute_scopes.ScopeEnum.ZONE:
610    collection = 'compute.machineTypes'
611    params = {'project': project, 'zone': location}
612  elif scope == compute_scopes.ScopeEnum.REGION:
613    collection = 'compute.regionMachineTypes'
614    params = {'project': project, 'region': location}
615  machine_type_uri = resource_parser.Parse(
616      machine_type_name, collection=collection, params=params).SelfLink()
617  return machine_type_uri
618
619
620def GetCanIpForward(args, skip_defaults):
621  if skip_defaults and not args.IsSpecified('can_ip_forward'):
622    return None
623  return args.can_ip_forward
624
625
626def GetTags(args, client):
627  if args.tags:
628    return client.messages.Tags(items=args.tags)
629  return None
630
631
632def GetLabels(args, client, instance_properties=False):
633  """Gets labels for the instance message."""
634  labels_value = client.messages.Instance.LabelsValue
635  if instance_properties:
636    labels_value = client.messages.InstanceProperties.LabelsValue
637  if args.labels:
638    return labels_value(additionalProperties=[
639        labels_value.AdditionalProperty(
640            key=key, value=value)
641        for key, value in sorted(six.iteritems(args.labels))
642    ])
643  return None
644
645
646def ParseAcceleratorType(accelerator_type_name, resource_parser, project,
647                         location, scope):
648  """Returns accelerator type ref based on location scope."""
649  if scope == compute_scopes.ScopeEnum.ZONE:
650    collection = 'compute.acceleratorTypes'
651    params = {'project': project, 'zone': location}
652  elif scope == compute_scopes.ScopeEnum.REGION:
653    collection = 'compute.regionAcceleratorTypes'
654    params = {'project': project, 'region': location}
655  accelerator_type = resource_parser.Parse(
656      accelerator_type_name, collection=collection, params=params).SelfLink()
657  return accelerator_type
658
659
660def ResolveSnapshotURI(user_project, snapshot, resource_parser):
661  if user_project and snapshot and resource_parser:
662    snapshot_ref = resource_parser.Parse(
663        snapshot,
664        collection='compute.snapshots',
665        params={'project': user_project})
666    return snapshot_ref.SelfLink()
667  return None
668
669
670def GetReservationAffinity(args, client):
671  """Returns the message of reservation affinity for the instance."""
672  if args.IsSpecified('reservation_affinity'):
673    type_msgs = (
674        client.messages.ReservationAffinity
675        .ConsumeReservationTypeValueValuesEnum)
676
677    reservation_key = None
678    reservation_values = []
679
680    if args.reservation_affinity == 'none':
681      reservation_type = type_msgs.NO_RESERVATION
682    elif args.reservation_affinity == 'specific':
683      reservation_type = type_msgs.SPECIFIC_RESERVATION
684      # Currently, the key is fixed and the value is the name of the
685      # reservation.
686      # The value being a repeated field is reserved for future use when user
687      # can specify more than one reservation names from which the VM can take
688      # capacity from.
689      reservation_key = _RESERVATION_AFFINITY_KEY
690      reservation_values = [args.reservation]
691    else:
692      reservation_type = type_msgs.ANY_RESERVATION
693
694    return client.messages.ReservationAffinity(
695        consumeReservationType=reservation_type,
696        key=reservation_key or None,
697        values=reservation_values)
698
699  return None
700
701
702def GetNetworkPerformanceConfig(args, client):
703  """Get NetworkPerformanceConfig message for the instance."""
704
705  network_perf_args = getattr(args, 'network_performance_configs', [])
706  network_perf_configs = client.messages.NetworkPerformanceConfig()
707
708  for config in network_perf_args:
709    total_tier = config.get('total-egress-bandwidth-tier', '').upper()
710    if total_tier:
711      network_perf_configs.totalEgressBandwidthTier = \
712        client.messages.NetworkPerformanceConfig.\
713          TotalEgressBandwidthTierValueValuesEnum(total_tier)
714
715  return network_perf_configs
716
717
718_RESERVATION_AFFINITY_KEY = 'compute.googleapis.com/reservation-name'
719