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