def _PromptForScopeList(self, ambiguous_refs, attributes, resource_type, choice_resources, raise_on_prompt_failure): """Prompt to resolve abiguous resources. Either returns str or throws.""" # targetInstances -> target instances resource_name = utils.CamelCaseToOutputFriendly(resource_type) # Resource names should be surrounded by brackets while choices should not names = ['[{0}]'.format(name) for name, _, _ in ambiguous_refs] # Print deprecation state for choices. choice_names = [] choice_mapping = [] for attribute in attributes: for choice_resource in choice_resources[attribute]: deprecated = choice_resource.deprecated if deprecated: choice_name = '{0} ({1})'.format(choice_resource.name, deprecated.state) else: choice_name = choice_resource.name if len(attributes) > 1: choice_name = '{0}: {1}'.format(attribute, choice_name) choice_mapping.append((attribute, choice_resource.name)) choice_names.append(choice_name) title = utils.ConstructList( 'For the following {0}:'.format(resource_name), names) idx = console_io.PromptChoice(options=choice_names, message='{0}choose a {1}:'.format( title, ' or '.join(attributes))) if idx is None: raise_on_prompt_failure() else: return choice_mapping[idx]
def PromptIfDisksWithoutAutoDeleteWillBeDeleted( self, args, disks_to_warn_for): """Prompts if disks with False autoDelete will be deleted.""" if not disks_to_warn_for: return prompt_list = [] disk_refs = self.CreateZonalReferences( disks_to_warn_for, args.zone, resource_type='disks') for ref in disk_refs: prompt_list.append('[{0}] in [{1}]'.format(ref.Name(), ref.zone)) prompt_message = utils.ConstructList( 'The following disks are not configured to be automatically deleted ' 'with instance deletion, but they will be deleted as a result of ' 'this operation if they are not attached to any other instances:', prompt_list) if not console_io.PromptContinue(message=prompt_message): raise exceptions.ToolException('Deletion aborted by user.')
def WarnForZonalCreation(self, resource_refs): """Warns the user if a zone has upcoming deprecation.""" zones = self.GetZones(resource_refs) if not zones: return prompts = [] zones_with_deprecated = [] for zone in zones: if zone.deprecated: zones_with_deprecated.append(zone) if not zones_with_deprecated: return if zones_with_deprecated: phrases = [] if len(zones_with_deprecated) == 1: phrases = ('zone is', 'this zone', 'the') else: phrases = ('zones are', 'these zones', 'their') title = ('\n' 'WARNING: The following selected {0} deprecated.' ' All resources in {1} will be deleted after' ' {2} turndown date.'.format(phrases[0], phrases[1], phrases[2])) printable_deprecated_zones = [] for zone in zones_with_deprecated: if zone.deprecated.deleted: printable_deprecated_zones.append( ('[{0}] {1}').format(zone.name, zone.deprecated.deleted)) else: printable_deprecated_zones.append('[{0}]'.format( zone.name)) prompts.append( utils.ConstructList(title, printable_deprecated_zones)) final_message = ' '.join(prompts) if not console_io.PromptContinue(message=final_message): raise calliope_exceptions.ToolException( 'Creation aborted by user.')
def PromptIfDisksWithoutAutoDeleteWillBeDeleted(self, disks_to_warn_for): """Prompts if disks with False autoDelete will be deleted. Args: disks_to_warn_for: list of references to disk resources. """ if not disks_to_warn_for: return prompt_list = [] for ref in disks_to_warn_for: prompt_list.append('[{0}] in [{1}]'.format(ref.Name(), ref.zone)) prompt_message = utils.ConstructList( 'The following disks are not configured to be automatically deleted ' 'with instance deletion, but they will be deleted as a result of ' 'this operation if they are not attached to any other instances:', prompt_list) if not console_io.PromptContinue(message=prompt_message): raise compute_exceptions.AbortedError('Deletion aborted by user.')
def WarnForRegionalCreation(self, resource_refs): """Warns the user if a region has upcoming deprecation.""" regions = self.GetRegions(resource_refs) if not regions: return prompts = [] regions_with_deprecated = [] for region in regions: if region.deprecated: regions_with_deprecated.append(region) if not regions_with_deprecated: return if regions_with_deprecated: phrases = [] if len(regions_with_deprecated) == 1: phrases = ('region is', 'this region', 'the') else: phrases = ('regions are', 'these regions', 'their') title = ('\n' 'WARNING: The following selected {0} deprecated.' ' All resources in {1} will be deleted after' ' {2} turndown date.'.format(phrases[0], phrases[1], phrases[2])) printable_deprecated_regions = [] for region in regions_with_deprecated: if region.deprecated.deleted: printable_deprecated_regions.append( ('[{0}] {1}').format(region.name, region.deprecated.deleted)) else: printable_deprecated_regions.append('[{0}]'.format( region.name)) prompts.append( utils.ConstructList(title, printable_deprecated_regions)) final_message = ' '.join(prompts) if not console_io.PromptContinue(message=final_message): raise exceptions.AbortedError('Creation aborted by user.')
def MakeRequests(requests, http, batch_url, errors, progress_tracker=None): """Makes one or more requests to the API. Each request can be either a synchronous API call or an asynchronous one. For synchronous calls (e.g., get and list), the result from the server is yielded immediately. For asynchronous calls (e.g., calls that return operations like insert), this function waits until the operation reaches the DONE state and fetches the corresponding object and yields that object (nothing is yielded for deletions). Currently, a heterogenous set of synchronous calls can be made (e.g., get request to fetch a disk and instance), however, the asynchronous requests must be homogenous (e.g., they must all be the same verb on the same collection). In the future, heterogenous asynchronous requests will be supported. For now, it is up to the client to ensure that the asynchronous requests are homogenous. Synchronous and asynchronous requests can be mixed. Args: requests: A list of requests to make. Each element must be a 3-element tuple where the first element is the service, the second element is the string name of the method on the service, and the last element is a protocol buffer representing the request. http: An httplib2.Http-like object. batch_url: The handler for making batch requests. errors: A list for capturing errors. If any response contains an error, it is added to this list. progress_tracker: progress tracker to be ticked while waiting for operations to finish. Yields: A response for each request. For deletion requests, no corresponding responses are returned. """ if _RequestsAreListRequests(requests): for item in _List(requests=requests, http=http, batch_url=batch_url, errors=errors): yield item return responses, new_errors = batch_helper.MakeRequests(requests=requests, http=http, batch_url=batch_url) errors.extend(new_errors) operation_service = None resource_service = None project = None # Collects all operation objects in a list so they can be waited on # and yields all non-operation objects since non-operation responses # cannot be waited on. operations_data = [] for request, response in zip(requests, responses): if response is None: continue service, _, request_body = request if (isinstance(response, service.client.MESSAGES_MODULE.Operation) and service.__class__.__name__ not in ('GlobalOperationsService', 'RegionOperationsService', 'ZoneOperationsService', 'GlobalAccountsOperationsService')): resource_service = service project = request_body.project if response.zone: operation_service = service.client.zoneOperations elif response.region: operation_service = service.client.regionOperations else: operation_service = service.client.globalOperations operations_data.append( waiters.OperationData(response, project, operation_service, resource_service)) else: yield response if operations_data: warnings = [] for response in waiters.WaitForOperations( operations_data=operations_data, http=http, batch_url=batch_url, warnings=warnings, progress_tracker=progress_tracker, errors=errors): yield response if warnings: log.warning( utils.ConstructList('Some requests generated warnings:', warnings))
def MakeRequests(requests, http, batch_url, errors): """Makes one or more requests to the API. Each request can be either a synchronous API call or an asynchronous one. For synchronous calls (e.g., get and list), the result from the server is yielded immediately. For asynchronous calls (e.g., calls that return operations like insert), this function waits until the operation reaches the DONE state and fetches the corresponding object and yields that object (nothing is yielded for deletions). Currently, a heterogenous set of synchronous calls can be made (e.g., get request to fetch a disk and instance), however, the asynchronous requests must be homogenous (e.g., they must all be the same verb on the same collection). In the future, heterogenous asynchronous requests will be supported. For now, it is up to the client to ensure that the asynchronous requests are homogenous. Synchronous and asynchronous requests can be mixed. Args: requests: A list of requests to make. Each element must be a 3-element tuple where the first element is the service, the second element is the string name of the method on the service, and the last element is a protocol buffer representing the request. http: An httplib2.Http-like object. batch_url: The handler for making batch requests. errors: A list for capturing errors. If any response contains an error, it is added to this list. Yields: A response for each request. For deletion requests, no corresponding responses are returned. """ if _RequestsAreListRequests(requests): for item in _List(requests=requests, http=http, batch_url=batch_url, errors=errors): yield item return # TODO(b/36057059): Delete the batch_helper module and move its logic # here. To do this, we also have to edit the lister module to depend # on this module instead of batch_helper. responses, new_errors = batch_helper.MakeRequests(requests=requests, http=http, batch_url=batch_url) errors.extend(new_errors) operation_service = None resource_service = None project = None # Collects all operation objects in a list so they can be waited on # and yields all non-operation objects since non-operation responses # cannot be waited on. operations = [] for request, response in zip(requests, responses): if response is None: continue service, _, request_body = request if (isinstance(response, service.client.MESSAGES_MODULE.Operation) and service.__class__.__name__ not in ('GlobalOperationsService', 'RegionOperationsService', 'ZoneOperationsService', 'GlobalAccountsOperationsService')): operations.append(response) # This logic assumes that all requests are homogenous, i.e., # they all make calls to the same service and verb. This is # temporary. In the future, we will push this logic into the # function that does the actual waiting. For now, we have to # deal with existing interfaces to keep the scopes of the # refactoring CLs small. # TODO(b/32276307) if not operation_service: resource_service = service project = request_body.project if response.kind == 'clouduseraccounts#operation': operation_service = service.client.globalAccountsOperations elif response.zone: operation_service = service.client.zoneOperations elif response.region: operation_service = service.client.regionOperations else: operation_service = service.client.globalOperations else: yield response if operations: warnings = [] for response in waiters.WaitForOperations( operations=operations, project=project, operation_service=operation_service, resource_service=resource_service, http=http, batch_url=batch_url, warnings=warnings, errors=errors): yield response if warnings: log.warn( utils.ConstructList('Some requests generated warnings:', warnings))
def SendInstancesRequestsAndPostProcessOutputs( api_holder, method_name, request_template, instances_holder_field, igm_ref, instances, per_instance_status_enabled=False): """Make *-instances requests and format output. Method resolves instance references, splits them to make batch of requests, adds to results statuses for unresolved instances, and yields all statuses raising errors, if any, in the end. Args: api_holder: Compute API holder. method_name: Name of the (region) instance groups managers service method to call. request_template: Partially filled *-instances request (no instances). instances_holder_field: Name of the field inside request holding instances field. igm_ref: URL to the target IGM. instances: A list of names of the instances to apply method to. per_instance_status_enabled: Enable functionality parsing resulting operation for graceful validation related warnings to allow per-instance status output. The plan is to gradually enable this for all per-instance commands in GA (even where graceful validation is not available / not used). Yields: A list of request statuses per instance. Requests status is a dictionary object with link to an instance keyed with 'selfLink', instance name keyed with 'instanceName', and status indicating if operation succeeded for instance keyed with 'status'. Status might be 'FAIL', 'SUCCESS', 'SKIPPED' in case of graceful validation, or 'MEMBER_NOT_FOUND' (in case of regional MIGs, when instance name cannot be resolved). """ client = api_holder.client if igm_ref.Collection() == 'compute.instanceGroupManagers': service = client.apitools_client.instanceGroupManagers elif igm_ref.Collection() == 'compute.regionInstanceGroupManagers': service = client.apitools_client.regionInstanceGroupManagers else: raise ValueError('Unknown reference type {0}'.format( igm_ref.Collection())) instances_with_references = CreateInstanceReferences( api_holder.resources, client, igm_ref, instances) resolved_references = [ instance.instance_reference for instance in instances_with_references if instance.instance_reference ] getattr(request_template, instances_holder_field).instances = resolved_references requests = SplitInstancesInRequest(request_template, instances_holder_field) request_tuples = GenerateRequestTuples(service, method_name, requests) errors_to_collect = [] warnings_to_collect = [] request_status_per_instance = [] if per_instance_status_enabled: request_status_per_instance.extend( MakeRequestsAndGetStatusPerInstanceFromOperation( client, request_tuples, instances_holder_field, warnings_to_collect, errors_to_collect)) else: request_status_per_instance.extend( MakeRequestsAndGetStatusPerInstance(client, request_tuples, instances_holder_field, errors_to_collect)) unresolved_instance_names = [ instance.instance_name for instance in instances_with_references if not instance.instance_reference ] request_status_per_instance.extend([ dict(instanceName=name, status='MEMBER_NOT_FOUND') for name in unresolved_instance_names ]) for status in request_status_per_instance: yield status if warnings_to_collect: log.warning( utils.ConstructList('Some requests generated warnings:', warnings_to_collect)) if errors_to_collect: raise utils.RaiseToolException(errors_to_collect)
def MakeRequests(requests, http, batch_url, errors, progress_tracker=None, no_followup=False, always_return_operation=False, followup_overrides=None, log_result=True, log_warnings=True, timeout=None): """Makes one or more requests to the API. Each request can be either a synchronous API call or an asynchronous one. For synchronous calls (e.g., get and list), the result from the server is yielded immediately. For asynchronous calls (e.g., calls that return operations like insert), this function waits until the operation reaches the DONE state and fetches the corresponding object and yields that object (nothing is yielded for deletions). Currently, a heterogenous set of synchronous calls can be made (e.g., get request to fetch a disk and instance), however, the asynchronous requests must be homogenous (e.g., they must all be the same verb on the same collection). In the future, heterogenous asynchronous requests will be supported. For now, it is up to the client to ensure that the asynchronous requests are homogenous. Synchronous and asynchronous requests can be mixed. Args: requests: A list of requests to make. Each element must be a 3-element tuple where the first element is the service, the second element is the string name of the method on the service, and the last element is a protocol buffer representing the request. http: An httplib2.Http-like object. batch_url: The handler for making batch requests. errors: A list for capturing errors. If any response contains an error, it is added to this list. progress_tracker: progress tracker to be ticked while waiting for operations to finish. no_followup: If True, do not followup operation with a GET request. always_return_operation: If True, return operation object even if operation fails. followup_overrides: A list of new resource names to GET once the operation finishes. Generally used in renaming calls. log_result: Whether the Operation Waiter should print the result in past tense of each request. log_warnings: Whether warnings for completed operation should be printed. timeout: The maximum amount of time, in seconds, to wait for the operations to reach the DONE state. Yields: A response for each request. For deletion requests, no corresponding responses are returned. """ if _RequestsAreListRequests(requests): for item in _List(requests=requests, http=http, batch_url=batch_url, errors=errors): yield item return responses, new_errors = batch_helper.MakeRequests(requests=requests, http=http, batch_url=batch_url) errors.extend(new_errors) operation_service = None resource_service = None # Collects all operation objects in a list so they can be waited on # and yields all non-operation objects since non-operation responses # cannot be waited on. operations_data = [] if not followup_overrides: followup_overrides = [None for _ in requests] for request, response, followup_override in zip(requests, responses, followup_overrides): if response is None: continue service, _, request_body = request if (isinstance(response, service.client.MESSAGES_MODULE.Operation) and not _IsEmptyOperation(response, service) and service.__class__.__name__ not in ('GlobalOperationsService', 'RegionOperationsService', 'ZoneOperationsService', 'GlobalOrganizationOperationsService', 'GlobalAccountsOperationsService')): resource_service = service project = None if hasattr(request_body, 'project'): project = request_body.project if response.zone: operation_service = service.client.zoneOperations elif response.region: operation_service = service.client.regionOperations else: operation_service = service.client.globalOperations else: operation_service = service.client.globalOrganizationOperations operations_data.append( waiters.OperationData( response, operation_service, resource_service, project=project, no_followup=no_followup, followup_override=followup_override, always_return_operation=always_return_operation)) else: yield response if operations_data: warnings = [] for response in waiters.WaitForOperations( operations_data=operations_data, http=http, batch_url=batch_url, warnings=warnings, progress_tracker=progress_tracker, errors=errors, log_result=log_result, timeout=timeout): yield response if warnings and log_warnings: log.warning( utils.ConstructList('Some requests generated warnings:', warnings))
def WarnForZonalCreation(self, resource_refs): """Warns the user if a zone has upcoming maintanence or deprecation.""" zones = self.GetZones(resource_refs) if not zones: return prompts = [] zones_with_upcoming_maintenance = [] zones_with_deprecated = [] for zone in zones: if zone.maintenanceWindows: zones_with_upcoming_maintenance.append(zone) if zone.deprecated: zones_with_deprecated.append(zone) if not zones_with_upcoming_maintenance and not zones_with_deprecated: return if zones_with_upcoming_maintenance: phrases = [] if len(zones_with_upcoming_maintenance) == 1: phrases = ('a zone', 'window is') else: phrases = ('zones', 'windows are') title = ('You have selected {0} with upcoming ' 'maintenance. During maintenance, resources are ' 'temporarily unavailible. The next scheduled ' '{1} as follows:'.format(phrases[0], phrases[1])) printable_maintenance_zones = [] for zone in zones_with_upcoming_maintenance: next_event = min(zone.maintenanceWindows, key=lambda x: x.beginTime) window = '[{0}]: {1} -- {2}'.format(zone.name, next_event.beginTime, next_event.endTime) printable_maintenance_zones.append(window) prompts.append( utils.ConstructList(title, printable_maintenance_zones)) if zones_with_deprecated: phrases = [] if len(zones_with_deprecated) == 1: phrases = ('zone is', 'this zone', 'the') else: phrases = ('zones are', 'these zones', 'their') title = ('\n' 'WARNING: The following selected {0} deprecated.' ' All resources in {1} will be deleted after' ' {2} turndown date.'.format(phrases[0], phrases[1], phrases[2])) printable_deprecated_zones = [] for zone in zones_with_deprecated: if zone.deprecated.deleted: printable_deprecated_zones.append( ('[{0}] {1}').format(zone.name, zone.deprecated.deleted)) else: printable_deprecated_zones.append('[{0}]'.format( zone.name)) prompts.append( utils.ConstructList(title, printable_deprecated_zones)) final_message = ' '.join(prompts) if not console_io.PromptContinue(message=final_message): raise calliope_exceptions.ToolException( 'Creation aborted by user.')