# -*- coding: utf-8 -*- # # Copyright 2015 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. """Utilities for manipulating GCE instances running an App Engine project.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import re from googlecloudsdk.core import exceptions from googlecloudsdk.core import log from googlecloudsdk.core import properties from googlecloudsdk.core.console import console_io from six.moves import filter # pylint: disable=redefined-builtin from six.moves import map # pylint: disable=redefined-builtin class InvalidInstanceSpecificationError(exceptions.Error): pass class SelectInstanceError(exceptions.Error): pass class Instance(object): """Value class for instances running the current App Engine project.""" # TODO(b/27900246): Once API supports "Get" verb, convert to use resource # parser. _INSTANCE_NAME_PATTERN = ('apps/(?P.*)/' 'services/(?P.*)/' 'versions/(?P.*)/' 'instances/(?P.*)') def __init__(self, service, version, id_, instance=None): self.service = service self.version = version self.id = id_ self.instance = instance # The Client API instance object @classmethod def FromInstanceResource(cls, instance): match = re.match(cls._INSTANCE_NAME_PATTERN, instance.name) service = match.group('service') version = match.group('version') return cls(service, version, instance.id, instance) @classmethod def FromResourcePath(cls, path, service=None, version=None): """Convert a resource path into an AppEngineInstance. A resource path is of the form '//'. '' and '' can be omitted, in which case they are None in the resulting instance. >>> (AppEngineInstance.FromResourcePath('a/b/c') == ... AppEngineInstance('a', 'b', 'c')) True >>> (AppEngineInstance.FromResourcePath('b/c', service='a') == ... AppEngineInstance('a', 'b', 'c')) True >>> (AppEngineInstance.FromResourcePath('c', service='a', version='b') == ... AppEngineInstance('a', 'b', 'c')) True Args: path: str, the resource path service: the service of the instance (replaces the service from the resource path) version: the version of the instance (replaces the version from the resource path) Returns: AppEngineInstance, an AppEngineInstance representing the path Raises: InvalidInstanceSpecificationError: if the instance is over- or under-specified """ parts = path.split('/') if len(parts) == 1: path_service, path_version, instance = None, None, parts[0] elif len(parts) == 2: path_service, path_version, instance = None, parts[0], parts[1] elif len(parts) == 3: path_service, path_version, instance = parts else: raise InvalidInstanceSpecificationError( 'Instance resource path is incorrectly specified. ' 'Please provide at most one service, version, and instance id, ' '.\n\n' 'You provided:\n' + path) if path_service and service and path_service != service: raise InvalidInstanceSpecificationError( 'Service [{0}] is inconsistent with specified instance [{1}].'.format( service, path)) service = service or path_service if path_version and version and path_version != version: raise InvalidInstanceSpecificationError( 'Version [{0}] is inconsistent with specified instance [{1}].'.format( version, path)) version = version or path_version return cls(service, version, instance) def __eq__(self, other): return (type(self) is type(other) and self.service == other.service and self.version == other.version and self.id == other.id) def __ne__(self, other): return not self == other # needed for set comparisons in tests def __hash__(self): return hash((self.service, self.version, self.id)) def __str__(self): return '/'.join(filter(bool, [self.service, self.version, self.id])) def __cmp__(self, other): return cmp((self.service, self.version, self.id), (other.service, other.version, other.id)) def FilterInstances(instances, service=None, version=None, instance=None): """Filter a list of App Engine instances. Args: instances: list of AppEngineInstance, all App Engine instances service: str, the name of the service to filter by or None to match all services version: str, the name of the version to filter by or None to match all versions instance: str, the instance id to filter by or None to match all versions. Returns: list of instances matching the given filters """ matching_instances = [] for provided_instance in instances: if ((not service or provided_instance.service == service) and (not version or provided_instance.version == version) and (not instance or provided_instance.id == instance)): matching_instances.append(provided_instance) return matching_instances def GetMatchingInstance(instances, service=None, version=None, instance=None): """Return exactly one matching instance. If instance is given, filter down based on the given criteria (service, version, instance) and return the matching instance (it is an error unless exactly one instance matches). Otherwise, prompt the user to select the instance interactively. Args: instances: list of AppEngineInstance, all instances to select from service: str, a service to filter by or None to include all services version: str, a version to filter by or None to include all versions instance: str, an instance ID to filter by. If not given, the instance will be selected interactively. Returns: AppEngineInstance, an instance from the given list. Raises: InvalidInstanceSpecificationError: if no matching instances or more than one matching instance were found. """ if not instance: return SelectInstanceInteractive(instances, service=service, version=version) matching = FilterInstances(instances, service, version, instance) if len(matching) > 1: raise InvalidInstanceSpecificationError( 'More than one instance matches the given specification.\n\n' 'Matching instances: {0}'.format(list(sorted(map(str, matching))))) elif not matching: raise InvalidInstanceSpecificationError( 'No instances match the given specification.\n\n' 'All instances: {0}'.format(list(sorted(map(str, instances))))) return matching[0] def SelectInstanceInteractive(all_instances, service=None, version=None): """Interactively choose an instance from a provided list. Example interaction: Which service? [1] default [2] service1 Please enter your numeric choice: 1 Which version? [1] v1 [2] v2 Please enter your numeric choice: 1 Which instance? [1] i1 [2] i2 Please enter your numeric choice: 1 Skips any prompts with only one option. Args: all_instances: list of AppEngineInstance, the list of instances to drill down on. service: str. If provided, skip the service prompt. version: str. If provided, skip the version prompt. Returns: AppEngineInstance, the selected instance from the list. Raises: SelectInstanceError: if no versions matching the criteria can be found or prompts are disabled. """ if properties.VALUES.core.disable_prompts.GetBool(): raise SelectInstanceError( 'Cannot interactively select instances with prompts disabled.') # Defined here to close over all_instances for the error message def _PromptOptions(options, type_): """Given an iterable options of type type_, prompt and return one.""" options = sorted(set(options), key=str) if len(options) > 1: idx = console_io.PromptChoice(options, message='Which {0}?'.format(type_)) elif len(options) == 1: idx = 0 log.status.Print('Choosing [{0}] for {1}.\n'.format(options[0], type_)) else: if all_instances: msg = ('No instances could be found matching the given criteria.\n\n' 'All instances:\n' + '\n'.join( map('* [{0}]'.format, sorted(all_instances, key=str)))) else: msg = 'No instances were found for the current project [{0}].'.format( properties.VALUES.core.project.Get(required=True)) raise SelectInstanceError(msg) return options[idx] matching_instances = FilterInstances(all_instances, service, version) service = _PromptOptions((i.service for i in matching_instances), 'service') matching_instances = FilterInstances(matching_instances, service=service) version = _PromptOptions((i.version for i in matching_instances), 'version') matching_instances = FilterInstances(matching_instances, version=version) return _PromptOptions(matching_instances, 'instance')