Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out shared 'Operation' class and helpers. #2165

Merged
merged 9 commits into from
Aug 24, 2016
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
gcloud-api
gcloud-config
gcloud-auth
operation-api

.. toctree::
:maxdepth: 0
Expand Down
7 changes: 7 additions & 0 deletions docs/operation-api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Long-Running Operations
~~~~~~~~~~~~~~~~~~~~~~~

.. automodule:: gcloud.operation
:members:
:show-inheritance:

143 changes: 22 additions & 121 deletions gcloud/bigtable/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,29 @@

import re

from google.longrunning import operations_pb2

from gcloud.bigtable._generated import (
instance_pb2 as data_v2_pb2)
from gcloud.bigtable._generated import (
bigtable_instance_admin_pb2 as messages_v2_pb2)
from gcloud.operation import Operation
from gcloud.operation import _compute_type_url
from gcloud.operation import _register_type_url


_CLUSTER_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
r'instances/(?P<instance>[^/]+)/clusters/'
r'(?P<cluster_id>[a-z][-a-z0-9]*)$')
_OPERATION_NAME_RE = re.compile(r'^operations/'
r'projects/([^/]+)/'
r'instances/([^/]+)/'
r'clusters/([a-z][-a-z0-9]*)/'
r'operations/(?P<operation_id>\d+)$')
_TYPE_URL_MAP = {
}

DEFAULT_SERVE_NODES = 3
"""Default number of nodes to use when creating a cluster."""


_UPDATE_CLUSTER_METADATA_URL = _compute_type_url(
messages_v2_pb2.UpdateClusterMetadata)
_register_type_url(
_UPDATE_CLUSTER_METADATA_URL, messages_v2_pb2.UpdateClusterMetadata)


def _prepare_create_request(cluster):
"""Creates a protobuf request for a CreateCluster request.

Expand All @@ -58,109 +58,6 @@ def _prepare_create_request(cluster):
)


def _parse_pb_any_to_native(any_val, expected_type=None):
"""Convert a serialized "google.protobuf.Any" value to actual type.

:type any_val: :class:`google.protobuf.any_pb2.Any`
:param any_val: A serialized protobuf value container.

:type expected_type: str
:param expected_type: (Optional) The type URL we expect ``any_val``
to have.

:rtype: object
:returns: The de-serialized object.
:raises: :class:`ValueError <exceptions.ValueError>` if the
``expected_type`` does not match the ``type_url`` on the input.
"""
if expected_type is not None and expected_type != any_val.type_url:
raise ValueError('Expected type: %s, Received: %s' % (
expected_type, any_val.type_url))
container_class = _TYPE_URL_MAP[any_val.type_url]
return container_class.FromString(any_val.value)


def _process_operation(operation_pb):
"""Processes a create protobuf response.

:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
:param operation_pb: The long-running operation response from a
Create/Update/Undelete cluster request.

:rtype: tuple
:returns: integer ID of the operation (``operation_id``).
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
doesn't match the :data:`_OPERATION_NAME_RE` regex.
"""
match = _OPERATION_NAME_RE.match(operation_pb.name)
if match is None:
raise ValueError('Operation name was not in the expected '
'format after a cluster modification.',
operation_pb.name)
operation_id = int(match.group('operation_id'))

return operation_id


class Operation(object):
"""Representation of a Google API Long-Running Operation.

In particular, these will be the result of operations on
clusters using the Cloud Bigtable API.

:type op_type: str
:param op_type: The type of operation being performed. Expect
``create``, ``update`` or ``undelete``.

:type op_id: int
:param op_id: The ID of the operation.

:type cluster: :class:`Cluster`
:param cluster: The cluster that created the operation.
"""

def __init__(self, op_type, op_id, cluster=None):
self.op_type = op_type
self.op_id = op_id
self._cluster = cluster
self._complete = False

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (other.op_type == self.op_type and
other.op_id == self.op_id and
other._cluster == self._cluster and
other._complete == self._complete)

def __ne__(self, other):
return not self.__eq__(other)

def finished(self):
"""Check if the operation has finished.

:rtype: bool
:returns: A boolean indicating if the current operation has completed.
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
has already completed.
"""
if self._complete:
raise ValueError('The operation has completed.')

operation_name = ('operations/' + self._cluster.name +
'/operations/%d' % (self.op_id,))
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
# We expect a `google.longrunning.operations_pb2.Operation`.
client = self._cluster._instance._client
operation_pb = client._operations_stub.GetOperation(request_pb)

if operation_pb.done:
self._complete = True
return True
else:
return False


class Cluster(object):
"""Representation of a Google Cloud Bigtable Cluster.

Expand Down Expand Up @@ -317,11 +214,13 @@ def create(self):
"""
request_pb = _prepare_create_request(self)
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._instance._client._instance_stub.CreateCluster(
request_pb)
client = self._instance._client
operation_pb = client._instance_stub.CreateCluster(request_pb)

op_id = _process_operation(operation_pb)
return Operation('create', op_id, cluster=self)
operation = Operation.from_pb(operation_pb, client)
operation.target = self
operation.metadata['request_type'] = 'CreateCluster'
return operation

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


def update(self):
"""Update this cluster.
Expand All @@ -345,12 +244,14 @@ def update(self):
name=self.name,
serve_nodes=self.serve_nodes,
)
# Ignore expected `._generated.instance_pb2.Cluster`.
operation_pb = self._instance._client._instance_stub.UpdateCluster(
request_pb)
# We expect a `google.longrunning.operations_pb2.Operation`.
client = self._instance._client
operation_pb = client._instance_stub.UpdateCluster(request_pb)

op_id = _process_operation(operation_pb)
return Operation('update', op_id, cluster=self)
operation = Operation.from_pb(operation_pb, client)
operation.target = self
operation.metadata['request_type'] = 'UpdateCluster'
return operation

def delete(self):
"""Delete this cluster.
Expand Down
147 changes: 13 additions & 134 deletions gcloud/bigtable/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@

import re

from google.longrunning import operations_pb2

from gcloud._helpers import _pb_timestamp_to_datetime
from gcloud.bigtable._generated import (
instance_pb2 as data_v2_pb2)
from gcloud.bigtable._generated import (
Expand All @@ -29,21 +26,20 @@
from gcloud.bigtable.cluster import Cluster
from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES
from gcloud.bigtable.table import Table
from gcloud.operation import Operation
from gcloud.operation import _compute_type_url
from gcloud.operation import _register_type_url


_EXISTING_INSTANCE_LOCATION_ID = 'see-existing-cluster'
_INSTANCE_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
r'instances/(?P<instance_id>[a-z][-a-z0-9]*)$')
_OPERATION_NAME_RE = re.compile(r'^operations/projects/([^/]+)/'
r'instances/([a-z][-a-z0-9]*)/'
r'locations/(?P<location_id>[a-z][-a-z0-9]*)/'
r'operations/(?P<operation_id>\d+)$')
_TYPE_URL_BASE = 'type.googleapis.com/google.bigtable.'
_ADMIN_TYPE_URL_BASE = _TYPE_URL_BASE + 'admin.v2.'
_INSTANCE_CREATE_METADATA = _ADMIN_TYPE_URL_BASE + 'CreateInstanceMetadata'
_TYPE_URL_MAP = {
_INSTANCE_CREATE_METADATA: messages_v2_pb2.CreateInstanceMetadata,
}


_CREATE_INSTANCE_METADATA_URL = _compute_type_url(
messages_v2_pb2.CreateInstanceMetadata)
_register_type_url(
_CREATE_INSTANCE_METADATA_URL, messages_v2_pb2.CreateInstanceMetadata)


def _prepare_create_request(instance):
Expand Down Expand Up @@ -71,125 +67,6 @@ def _prepare_create_request(instance):
return message


def _parse_pb_any_to_native(any_val, expected_type=None):
"""Convert a serialized "google.protobuf.Any" value to actual type.

:type any_val: :class:`google.protobuf.any_pb2.Any`
:param any_val: A serialized protobuf value container.

:type expected_type: str
:param expected_type: (Optional) The type URL we expect ``any_val``
to have.

:rtype: object
:returns: The de-serialized object.
:raises: :class:`ValueError <exceptions.ValueError>` if the
``expected_type`` does not match the ``type_url`` on the input.
"""
if expected_type is not None and expected_type != any_val.type_url:
raise ValueError('Expected type: %s, Received: %s' % (
expected_type, any_val.type_url))
container_class = _TYPE_URL_MAP[any_val.type_url]
return container_class.FromString(any_val.value)


def _process_operation(operation_pb):
"""Processes a create protobuf response.

:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
:param operation_pb: The long-running operation response from a
Create/Update/Undelete instance request.

:rtype: (int, str, datetime)
:returns: (operation_id, location_id, operation_begin).
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
doesn't match the :data:`_OPERATION_NAME_RE` regex.
"""
match = _OPERATION_NAME_RE.match(operation_pb.name)
if match is None:
raise ValueError('Operation name was not in the expected '
'format after instance creation.',
operation_pb.name)
location_id = match.group('location_id')
operation_id = int(match.group('operation_id'))

request_metadata = _parse_pb_any_to_native(operation_pb.metadata)
operation_begin = _pb_timestamp_to_datetime(
request_metadata.request_time)

return operation_id, location_id, operation_begin


class Operation(object):
"""Representation of a Google API Long-Running Operation.

In particular, these will be the result of operations on
instances using the Cloud Bigtable API.

:type op_type: str
:param op_type: The type of operation being performed. Expect
``create``, ``update`` or ``undelete``.

:type op_id: int
:param op_id: The ID of the operation.

:type begin: :class:`datetime.datetime`
:param begin: The time when the operation was started.

:type location_id: str
:param location_id: ID of the location in which the operation is running

:type instance: :class:`Instance`
:param instance: The instance that created the operation.
"""

def __init__(self, op_type, op_id, begin, location_id, instance=None):
self.op_type = op_type
self.op_id = op_id
self.begin = begin
self.location_id = location_id
self._instance = instance
self._complete = False

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (other.op_type == self.op_type and
other.op_id == self.op_id and
other.begin == self.begin and
other.location_id == self.location_id and
other._instance == self._instance and
other._complete == self._complete)

def __ne__(self, other):
return not self.__eq__(other)

def finished(self):
"""Check if the operation has finished.

:rtype: bool
:returns: A boolean indicating if the current operation has completed.
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
has already completed.
"""
if self._complete:
raise ValueError('The operation has completed.')

operation_name = (
'operations/%s/locations/%s/operations/%d' %
(self._instance.name, self.location_id, self.op_id))
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._instance._client._operations_stub.GetOperation(
request_pb)

if operation_pb.done:
self._complete = True
return True
else:
return False


class Instance(object):
"""Representation of a Google Cloud Bigtable Instance.

Expand Down Expand Up @@ -359,8 +236,10 @@ def create(self):
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._client._instance_stub.CreateInstance(request_pb)

op_id, loc_id, op_begin = _process_operation(operation_pb)
return Operation('create', op_id, op_begin, loc_id, instance=self)
operation = Operation.from_pb(operation_pb, self._client)
operation.target = self
operation.metadata['request_type'] = 'CreateInstance'
return operation

def update(self):
"""Update this instance.
Expand Down
Loading