1# -*- coding: utf-8 -*- #
2# Copyright 2015 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"""Common utility functions for sql operations."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import time
22
23from apitools.base.py import exceptions as base_exceptions
24
25from googlecloudsdk.api_lib.sql import exceptions
26from googlecloudsdk.core.console import progress_tracker as console_progress_tracker
27from googlecloudsdk.core.util import retry
28
29_MS_PER_SECOND = 1000
30# Ten mins, based off of the max time it usually takes to create a SQL instance.
31_INSTANCE_CREATION_TIMEOUT_SECONDS = 600
32
33
34class _BaseOperations(object):
35  """Common utility functions for sql operations."""
36
37  _PRE_START_SLEEP_SEC = 1
38  _INITIAL_SLEEP_MS = 2000
39  _WAIT_CEILING_MS = 20000
40  _HTTP_MAX_RETRY_MS = 2000
41
42  @classmethod
43  def WaitForOperation(cls,
44                       sql_client,
45                       operation_ref,
46                       message,
47                       max_wait_seconds=_INSTANCE_CREATION_TIMEOUT_SECONDS):
48    """Wait for a Cloud SQL operation to complete.
49
50    No operation is done instantly. Wait for it to finish following this logic:
51    First wait 1s, then query, then retry waiting exponentially more from 2s.
52    We want to limit to 20s between retries to maintain some responsiveness.
53    Finally, we want to limit the whole process to a conservative 300s. If we
54    get to that point it means something is wrong and we can throw an exception.
55
56    Args:
57      sql_client: apitools.BaseApiClient, The client used to make requests.
58      operation_ref: resources.Resource, A reference for the operation to poll.
59      message: str, The string to print while polling.
60      max_wait_seconds: integer or None, the number of seconds before the
61          poller times out.
62
63    Returns:
64      Operation: The polled operation.
65
66    Raises:
67      OperationError: If the operation has an error code, is in UNKNOWN state,
68          or if the operation takes more than max_wait_seconds when a value is
69          specified.
70    """
71
72    def ShouldRetryFunc(result, state):
73      # In case of HttpError, retry for up to _HTTP_MAX_RETRY_MS at most.
74      if isinstance(result, base_exceptions.HttpError):
75        if state.time_passed_ms > _BaseOperations._HTTP_MAX_RETRY_MS:
76          raise result
77        return True
78      # In case of other Exceptions, raise them immediately.
79      if isinstance(result, Exception):
80        raise result
81      # Otherwise let the retryer do it's job until the Operation is done.
82      is_operation_done = result.status == sql_client.MESSAGES_MODULE.Operation.StatusValueValuesEnum.DONE
83      return not is_operation_done
84
85    # Set the max wait time.
86    max_wait_ms = None
87    if max_wait_seconds:
88      max_wait_ms = max_wait_seconds * _MS_PER_SECOND
89    with console_progress_tracker.ProgressTracker(
90        message, autotick=False) as pt:
91      time.sleep(_BaseOperations._PRE_START_SLEEP_SEC)
92      retryer = retry.Retryer(
93          exponential_sleep_multiplier=2,
94          max_wait_ms=max_wait_ms,
95          wait_ceiling_ms=_BaseOperations._WAIT_CEILING_MS)
96      try:
97        return retryer.RetryOnResult(
98            cls.GetOperation, [sql_client, operation_ref],
99            {'progress_tracker': pt},
100            should_retry_if=ShouldRetryFunc,
101            sleep_ms=_BaseOperations._INITIAL_SLEEP_MS)
102      except retry.WaitException:
103        raise exceptions.OperationError(
104            ('Operation {0} is taking longer than expected. You can continue '
105             'waiting for the operation by running `{1}`').format(
106                 operation_ref, cls.GetOperationWaitCommand(operation_ref)))
107
108
109class OperationsV1Beta4(_BaseOperations):
110  """Common utility functions for sql operations V1Beta4."""
111
112  @staticmethod
113  def GetOperation(sql_client, operation_ref, progress_tracker=None):
114    """Helper function for getting the status of an operation for V1Beta4 API.
115
116    Args:
117      sql_client: apitools.BaseApiClient, The client used to make requests.
118      operation_ref: resources.Resource, A reference for the operation to poll.
119      progress_tracker: progress_tracker.ProgressTracker, A reference for the
120          progress tracker to tick, in case this function is used in a Retryer.
121
122    Returns:
123      Operation: if the operation succeeded without error or  is not yet done.
124      OperationError: If the operation has an error code or is in UNKNOWN state.
125      Exception: Any other exception that can occur when calling Get
126    """
127
128    if progress_tracker:
129      progress_tracker.Tick()
130    try:
131      op = sql_client.operations.Get(
132          sql_client.MESSAGES_MODULE.SqlOperationsGetRequest(
133              project=operation_ref.project, operation=operation_ref.operation))
134    except Exception as e:  # pylint:disable=broad-except
135      # Since we use this function in a retryer.RetryOnResult block, where we
136      # retry for different exceptions up to different amounts of time, we
137      # have to catch all exceptions here and return them.
138      return e
139    if op.error and op.error.errors:
140      error_object = op.error.errors[0]
141      # If there's an error message to show, show it in addition to the code.
142      error = '[{}]'.format(error_object.code)
143      if error_object.message:
144        error += ' ' + error_object.message
145      return exceptions.OperationError(error)
146    if op.status == sql_client.MESSAGES_MODULE.Operation.StatusValueValuesEnum.SQL_OPERATION_STATUS_UNSPECIFIED:
147      return exceptions.OperationError(op.status)
148    return op
149
150  @staticmethod
151  def GetOperationWaitCommand(operation_ref):
152    return 'gcloud beta sql operations wait --project {0} {1}'.format(
153        operation_ref.project, operation_ref.operation)
154