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 and classes for dealing with instances groups."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import enum
22from apitools.base.py import encoding
23
24from googlecloudsdk.api_lib.compute import exceptions
25from googlecloudsdk.api_lib.compute import lister
26from googlecloudsdk.api_lib.compute import path_simplifier
27from googlecloudsdk.api_lib.compute import utils
28from googlecloudsdk.calliope import exceptions as calliope_exceptions
29from googlecloudsdk.core import properties
30import six
31from six.moves import range  # pylint: disable=redefined-builtin
32
33INSTANCE_GROUP_GET_NAMED_PORT_DETAILED_HELP = {
34    'brief': 'Lists the named ports for an instance group resource',
35    'DESCRIPTION': """
36Named ports are key:value pairs metadata representing
37the service name and the port that it's running on. Named ports
38can be assigned to an instance group, which indicates that the service
39is available on all instances in the group. This information is used
40by the HTTP Load Balancing service.
41
42*{command}* lists the named ports (name and port tuples)
43for an instance group.
44""",
45    'EXAMPLES': """
46For example, to list named ports for an instance group:
47
48  $ {command} example-instance-group --zone=us-central1-a
49
50The above example lists named ports assigned to an instance
51group named 'example-instance-group' in the ``us-central1-a'' zone.
52""",
53}
54
55# max_langth limit of instances field in InstanceGroupManagers*InstancesRequest
56INSTANCES_MAX_LENGTH = 1000
57
58
59def IsZonalGroup(group_ref):
60  """Checks if group reference is zonal."""
61  return group_ref.Collection() == 'compute.instanceGroups'
62
63
64def ValidateInstanceInZone(instances, zone):
65  """Validate if provided list in zone given as parameter.
66
67  Args:
68    instances: list of instances resources to be validated
69    zone: a zone all instances must be in order to pass validation
70
71  Raises:
72    InvalidArgumentException: If any instance is in different zone
73                              than given as parameter.
74  """
75  invalid_instances = [inst.SelfLink()
76                       for inst in instances if inst.zone != zone]
77  if any(invalid_instances):
78    raise calliope_exceptions.InvalidArgumentException(
79        'instances', 'The zone of instance must match the instance group zone. '
80        'Following instances has invalid zone: %s'
81        % ', '.join(invalid_instances))
82
83
84def UnwrapResponse(responses, attr_name):
85  """Extracts items stored in given attribute of instance group response."""
86  for response in responses:
87    for item in getattr(response, attr_name):
88      yield item
89
90
91def UriFuncForListInstanceRelatedObjects(resource):
92  """UriFunc for listing instance-group related subresources.
93
94  Function returns field with URI for objects being subresources of
95  instance-groups, with instance fields. Works for list-instances and
96  instance-configs list commands.
97
98  Args:
99    resource: instance-group subresource with instance field
100
101  Returns:
102    URI of instance
103  """
104  return resource.instance
105
106
107def OutputNamedPortsForGroup(group_ref, compute_client):
108  """Gets the request to fetch instance group."""
109  compute = compute_client.apitools_client
110  if group_ref.Collection() == 'compute.instanceGroups':
111    service = compute.instanceGroups
112    request = service.GetRequestType('Get')(
113        instanceGroup=group_ref.Name(),
114        zone=group_ref.zone,
115        project=group_ref.project)
116  else:
117    service = compute.regionInstanceGroups
118    request = service.GetRequestType('Get')(
119        instanceGroup=group_ref.Name(),
120        region=group_ref.region,
121        project=group_ref.project)
122  results = compute_client.MakeRequests(requests=[(service, 'Get', request)])
123  return list(UnwrapResponse(results, 'namedPorts'))
124
125
126class FingerprintFetchException(exceptions.Error):
127  """Exception thrown when there is a problem with getting fingerprint."""
128
129
130def _GetGroupFingerprint(compute_client, group_ref):
131  """Gets fingerprint of given instance group."""
132  compute = compute_client.apitools_client
133  if IsZonalGroup(group_ref):
134    service = compute.instanceGroups
135    request = compute.MESSAGES_MODULE.ComputeInstanceGroupsGetRequest(
136        project=group_ref.project,
137        zone=group_ref.zone,
138        instanceGroup=group_ref.instanceGroup)
139  else:
140    service = compute.regionInstanceGroups
141    request = compute.MESSAGES_MODULE.ComputeRegionInstanceGroupsGetRequest(
142        project=group_ref.project,
143        region=group_ref.region,
144        instanceGroup=group_ref.instanceGroup)
145
146  errors = []
147  resources = compute_client.MakeRequests(
148      requests=[(service, 'Get', request)],
149      errors_to_collect=errors)
150
151  if errors:
152    utils.RaiseException(
153        errors,
154        FingerprintFetchException,
155        error_message='Could not set named ports for resource:')
156  return resources[0].fingerprint
157
158
159def GetSetNamedPortsRequestForGroup(compute_client, group_ref, ports):
160  """Returns a request to get named ports and service to send request.
161
162  Args:
163    compute_client: GCE API client,
164    group_ref: reference to instance group (zonal or regional),
165    ports: list of named ports to set
166
167  Returns:
168    request, message to send in order to set named ports on instance group,
169    service, service where request should be sent
170      - regionInstanceGroups for regional groups
171      - instanceGroups for zonal groups
172  """
173  compute = compute_client.apitools_client
174  messages = compute_client.messages
175  # Instance group fingerprint will be used for optimistic locking. Each
176  # modification of instance group changes the fingerprint. This request will
177  # fail if instance group fingerprint does not match fingerprint sent in
178  # request.
179  fingerprint = _GetGroupFingerprint(compute_client, group_ref)
180  if IsZonalGroup(group_ref):
181    request_body = messages.InstanceGroupsSetNamedPortsRequest(
182        fingerprint=fingerprint,
183        namedPorts=ports)
184    return messages.ComputeInstanceGroupsSetNamedPortsRequest(
185        instanceGroup=group_ref.Name(),
186        instanceGroupsSetNamedPortsRequest=request_body,
187        zone=group_ref.zone,
188        project=group_ref.project), compute.instanceGroups
189  else:
190    request_body = messages.RegionInstanceGroupsSetNamedPortsRequest(
191        fingerprint=fingerprint,
192        namedPorts=ports)
193    return messages.ComputeRegionInstanceGroupsSetNamedPortsRequest(
194        instanceGroup=group_ref.Name(),
195        regionInstanceGroupsSetNamedPortsRequest=request_body,
196        region=group_ref.region,
197        project=group_ref.project), compute.regionInstanceGroups
198
199
200def ValidateAndParseNamedPortsArgs(messages, named_ports):
201  """Validates named ports flags."""
202  ports = []
203  for named_port in named_ports:
204    if named_port.count(':') != 1:
205      raise calliope_exceptions.InvalidArgumentException(
206          named_port, 'Named ports should follow NAME:PORT format.')
207    host, port = named_port.split(':')
208    if not port.isdigit():
209      raise calliope_exceptions.InvalidArgumentException(
210          named_port, 'Named ports should follow NAME:PORT format.')
211    ports.append(messages.NamedPort(name=host, port=int(port)))
212  return ports
213
214
215SET_NAMED_PORTS_HELP = {
216    'brief': 'Sets the list of named ports for an instance group',
217    'DESCRIPTION': """
218Named ports are key:value pairs metadata representing
219the service name and the port that it's running on. Named ports
220can be assigned to an instance group, which
221indicates that the service is available on all instances in the
222group. This information is used by the HTTP Load Balancing
223service.
224
225*{command}* sets the list of named ports for all instances
226in an instance group.
227
228Note: Running this command will clear all existing named ports.
229""",
230    'EXAMPLES': """
231For example, to apply the named ports to an entire instance group:
232
233  $ {command} example-instance-group --named-ports=example-service:1111 --zone=us-central1-a
234
235The above example will assign a name 'example-service' for port 1111
236to the instance group called 'example-instance-group' in the
237``us-central1-a'' zone. The command removes any named ports that are
238already set for this instance group.
239
240To clear named ports from instance group provide empty named ports
241list as parameter:
242
243  $ {command} example-instance-group --named-ports="" --zone=us-central1-a
244""",
245}
246
247
248def CreateInstanceReferences(
249    resources, compute_client, igm_ref, instance_names):
250  """Creates reference to instances in instance group (zonal or regional)."""
251  if igm_ref.Collection() == 'compute.instanceGroupManagers':
252    instance_refs = []
253    for instance in instance_names:
254      instance_refs.append(resources.Parse(
255          instance,
256          params={
257              'project': igm_ref.project,
258              'zone': igm_ref.zone,
259          },
260          collection='compute.instances'))
261    return [instance_ref.SelfLink() for instance_ref in instance_refs]
262  elif igm_ref.Collection() == 'compute.regionInstanceGroupManagers':
263    service = compute_client.apitools_client.regionInstanceGroupManagers
264    request = service.GetRequestType('ListManagedInstances')(
265        instanceGroupManager=igm_ref.Name(),
266        region=igm_ref.region,
267        project=igm_ref.project)
268    results = compute_client.MakeRequests(requests=[
269        (service, 'ListManagedInstances', request)])
270    # here we assume that instances are uniquely named within RMIG
271    return [instance_ref.instance for instance_ref in results
272            if path_simplifier.Name(instance_ref.instance) in instance_names
273            or instance_ref.instance in instance_names]
274  else:
275    raise ValueError('Unknown reference type {0}'.format(igm_ref.Collection()))
276
277
278def SplitInstancesInRequest(request,
279                            request_field,
280                            max_length=INSTANCES_MAX_LENGTH):
281  """Split request into parts according to max_length limit.
282
283  Example:
284    requests = SplitInstancesInRequest(
285          self.messages.
286          ComputeInstanceGroupManagersAbandonInstancesRequest(
287              instanceGroupManager=igm_ref.Name(),
288              instanceGroupManagersAbandonInstancesRequest=(
289                  self.messages.InstanceGroupManagersAbandonInstancesRequest(
290                      instances=instances,
291                  )
292              ),
293              project=igm_ref.project,
294              zone=igm_ref.zone,
295          ), 'instanceGroupManagersAbandonInstancesRequest')
296
297  Then:
298    return client.MakeRequests(LiftRequestsList(service, method, requests))
299
300  Args:
301    request: _messages.Message, request to split
302    request_field: str, name of property inside request holding instances field
303    max_length: int, max_length of instances property
304
305  Returns:
306    List of requests with instances field length limited by max_length.
307  """
308  result = []
309  all_instances = getattr(request, request_field).instances or []
310  n = len(all_instances)
311  for i in range(0, n, max_length):
312    request_part = encoding.CopyProtoMessage(request)
313    field = getattr(request_part, request_field)
314    field.instances = all_instances[i:i+max_length]
315    result.append(request_part)
316  return result
317
318
319def GenerateRequestTuples(service, method, requests):
320  """(a, b, [c]) -> [(a, b, c)]."""
321  for request in requests:
322    yield (service, method, request)
323
324
325# TODO(b/36799480) Parallelize instance_groups_utils.MakeRequestsList
326def MakeRequestsList(client, requests, field_name):
327  """Make list of *-instances requests returning actionable feedback.
328
329  Args:
330    client: Compute client.
331    requests: [(service, method, request)].
332    field_name: name of field inside request holding list of instances.
333
334  Yields:
335    Dictionaries with link to instance keyed with 'selfLink' and string
336    indicating if operation succeeded keyed with 'status'.
337  """
338  errors_to_propagate = []
339  result = []
340
341  for service, method, request in requests:
342    # Using batching here because otherwise all tests would have to be
343    # rewritten.
344    errors = []
345    # Result is synthesized, API response is unused.
346    client.MakeRequests([(service, method, request)], errors)
347    if errors:
348      # Operation failed for all instances.
349      errors_to_propagate.extend(errors)
350      status = 'FAIL'
351    else:
352      status = 'SUCCESS'
353    for instance in getattr(request, field_name).instances:
354      # Cannot yield here - only last dict will be printed
355      result.append({'selfLink': instance, 'status': status})
356
357  for record in result:
358    yield record
359
360  if errors_to_propagate:
361    raise utils.RaiseToolException(errors_to_propagate)
362
363
364class InstanceGroupFilteringMode(enum.Enum):
365  """Filtering mode for Instance Groups based on dynamic properties."""
366  ALL_GROUPS = 1
367  ONLY_MANAGED_GROUPS = 2
368  ONLY_UNMANAGED_GROUPS = 3
369
370
371def ComputeInstanceGroupManagerMembership(
372    compute_holder, items, filter_mode=InstanceGroupFilteringMode.ALL_GROUPS):
373  """Add information if instance group is managed.
374
375  Args:
376    compute_holder: ComputeApiHolder, The compute API holder
377    items: list of instance group messages,
378    filter_mode: InstanceGroupFilteringMode, managed/unmanaged filtering options
379  Returns:
380    list of instance groups with computed dynamic properties
381  """
382  client = compute_holder.client
383  resources = compute_holder.resources
384
385  errors = []
386  items = list(items)
387  zone_links = set([ig['zone'] for ig in items if 'zone' in ig])
388
389  project_to_zones = {}
390  for zone in zone_links:
391    zone_ref = resources.Parse(
392        zone,
393        params={
394            'project': properties.VALUES.core.project.GetOrFail,
395        },
396        collection='compute.zones')
397    if zone_ref.project not in project_to_zones:
398      project_to_zones[zone_ref.project] = set()
399    project_to_zones[zone_ref.project].add(zone_ref.zone)
400
401  zonal_instance_group_managers = []
402  for project, zones in six.iteritems(project_to_zones):
403    zonal_instance_group_managers.extend(lister.GetZonalResources(
404        service=client.apitools_client.instanceGroupManagers,
405        project=project,
406        requested_zones=zones,
407        filter_expr=None,
408        http=client.apitools_client.http,
409        batch_url=client.batch_url,
410        errors=errors))
411
412  regional_instance_group_managers = []
413  if hasattr(client.apitools_client, 'regionInstanceGroups'):
414    # regional instance groups are just in 'alpha' API
415    region_links = set([ig['region'] for ig in items if 'region' in ig])
416    project_to_regions = {}
417    for region in region_links:
418      region_ref = resources.Parse(region, collection='compute.regions')
419      if region_ref.project not in project_to_regions:
420        project_to_regions[region_ref.project] = set()
421      project_to_regions[region_ref.project].add(region_ref.region)
422    for project, regions in six.iteritems(project_to_regions):
423      regional_instance_group_managers.extend(lister.GetRegionalResources(
424          service=client.apitools_client.regionInstanceGroupManagers,
425          project=project,
426          requested_regions=regions,
427          filter_expr=None,
428          http=client.apitools_client.http,
429          batch_url=client.batch_url,
430          errors=errors))
431
432  instance_group_managers = (
433      list(zonal_instance_group_managers)
434      + list(regional_instance_group_managers))
435  instance_group_managers_refs = set([
436      path_simplifier.ScopedSuffix(igm.selfLink)
437      for igm in instance_group_managers])
438
439  if errors:
440    utils.RaiseToolException(errors)
441
442  results = []
443  for item in items:
444    self_link = item['selfLink']
445    igm_self_link = self_link.replace(
446        '/instanceGroups/', '/instanceGroupManagers/')
447    scoped_suffix = path_simplifier.ScopedSuffix(igm_self_link)
448    is_managed = scoped_suffix in instance_group_managers_refs
449
450    if (is_managed and
451        filter_mode == InstanceGroupFilteringMode.ONLY_UNMANAGED_GROUPS):
452      continue
453    elif (not is_managed and
454          filter_mode == InstanceGroupFilteringMode.ONLY_MANAGED_GROUPS):
455      continue
456
457    item['isManaged'] = ('Yes' if is_managed else 'No')
458    if is_managed:
459      item['instanceGroupManagerUri'] = igm_self_link
460    results.append(item)
461
462  return results
463