From 48dce464da161f7b1b71046c112538a4c7f16f32 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Thu, 19 Nov 2015 15:27:54 -0800 Subject: [PATCH] Adding Bigtable Cluster.from_pb factory. For creating a Cluster object from a protobuf cluster returned from the API. Also adding a helper for making sure properties are set on a protobuf object. --- gcloud/bigtable/cluster.py | 76 +++++++++++++++++++++++ gcloud/bigtable/test_cluster.py | 103 ++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/gcloud/bigtable/cluster.py b/gcloud/bigtable/cluster.py index e2d8420b28e71..aa57e324406f3 100644 --- a/gcloud/bigtable/cluster.py +++ b/gcloud/bigtable/cluster.py @@ -15,9 +15,51 @@ """User friendly container for Google Cloud Bigtable Cluster.""" +import re + from gcloud.bigtable.table import Table +_CLUSTER_NAME_RE = re.compile(r'^projects/(?P[^/]+)/' + r'zones/(?P[^/]+)/clusters/' + r'(?P[a-z][-a-z0-9]*)$') + + +def _require_pb_property(message_pb, property_name, value): + """Check that a property agrees with the value on the message. + + :type message_pb: :class:`google.protobuf.message.Message` + :param message_pb: The message to check for ``property_name``. + + :type property_name: str + :param property_name: The property value to check against. + + :type value: object or :data:`NoneType ` + :param value: The value to check against the cluster. If :data:`None`, + will not be checked. + + :rtype: object + :returns: The value of ``property_name`` set on ``message_pb``. + :raises: :class:`ValueError ` if the result returned + from the ``message_pb`` does not contain the ``property_name`` + value or if the value returned disagrees with the ``value`` + passed with the request (if that value is not null). + """ + # Make sure `property_name` is set on the response. + # NOTE: HasField() doesn't work in protobuf>=3.0.0a3 + all_fields = set([field.name for field in message_pb._fields]) + if property_name not in all_fields: + raise ValueError('Message does not contain %s.' % (property_name,)) + property_val = getattr(message_pb, property_name) + if value is None: + value = property_val + elif value != property_val: + raise ValueError('Message returned %s value disagreeing ' + 'with value passed in.' % (property_name,)) + + return value + + class Cluster(object): """Representation of a Google Cloud Bigtable Cluster. @@ -60,3 +102,37 @@ def table(self, table_id): :returns: The table owned by this cluster. """ return Table(table_id, self) + + def _update_from_pb(self, cluster_pb): + self.display_name = _require_pb_property( + cluster_pb, 'display_name', None) + self.serve_nodes = _require_pb_property( + cluster_pb, 'serve_nodes', None) + + @classmethod + def from_pb(cls, cluster_pb, client): + """Creates a cluster instance from a protobuf. + + :type cluster_pb: :class:`bigtable_cluster_data_pb2.Cluster` + :param cluster_pb: A cluster protobuf object. + + :type client: :class:`.client.Client` + :param client: The client that owns the cluster. + + :rtype: :class:`Cluster` + :returns: The cluster parsed from the protobuf response. + :raises: :class:`ValueError ` if the cluster + name does not match :data:`_CLUSTER_NAME_RE` or if the parsed + project ID does not match the project ID on the client. + """ + match = _CLUSTER_NAME_RE.match(cluster_pb.name) + if match is None: + raise ValueError('Cluster protobuf name was not in the ' + 'expected format.', cluster_pb.name) + if match.group('project') != client.project: + raise ValueError('Project ID on cluster does not match the ' + 'project ID on the client') + + result = cls(match.group('zone'), match.group('cluster_id'), client) + result._update_from_pb(cluster_pb) + return result diff --git a/gcloud/bigtable/test_cluster.py b/gcloud/bigtable/test_cluster.py index 8e3855070e3e7..2f96f62aad261 100644 --- a/gcloud/bigtable/test_cluster.py +++ b/gcloud/bigtable/test_cluster.py @@ -65,3 +65,106 @@ def test_table_factory(self): self.assertTrue(isinstance(table, Table)) self.assertEqual(table.table_id, table_id) self.assertEqual(table._cluster, cluster) + + def test_from_pb_success(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + + project = 'PROJECT' + zone = 'zone' + cluster_id = 'cluster-id' + client = _Client(project=project) + + cluster_name = ('projects/' + project + '/zones/' + zone + + '/clusters/' + cluster_id) + cluster_pb = data_pb2.Cluster( + name=cluster_name, + display_name=cluster_id, + serve_nodes=3, + ) + + klass = self._getTargetClass() + cluster = klass.from_pb(cluster_pb, client) + self.assertTrue(isinstance(cluster, klass)) + self.assertEqual(cluster._client, client) + self.assertEqual(cluster.zone, zone) + self.assertEqual(cluster.cluster_id, cluster_id) + + def test_from_pb_bad_cluster_name(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + + cluster_name = 'INCORRECT_FORMAT' + cluster_pb = data_pb2.Cluster(name=cluster_name) + + klass = self._getTargetClass() + with self.assertRaises(ValueError): + klass.from_pb(cluster_pb, None) + + def test_from_pb_project_mistmatch(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + + project = 'PROJECT' + zone = 'zone' + cluster_id = 'cluster-id' + alt_project = 'ALT_PROJECT' + client = _Client(project=alt_project) + + self.assertNotEqual(project, alt_project) + + cluster_name = ('projects/' + project + '/zones/' + zone + + '/clusters/' + cluster_id) + cluster_pb = data_pb2.Cluster(name=cluster_name) + + klass = self._getTargetClass() + with self.assertRaises(ValueError): + klass.from_pb(cluster_pb, client) + + +class Test__require_pb_property(unittest2.TestCase): + + def _callFUT(self, message_pb, property_name, value): + from gcloud.bigtable.cluster import _require_pb_property + return _require_pb_property(message_pb, property_name, value) + + def test_it(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + serve_nodes = 119 + cluster_pb = data_pb2.Cluster(serve_nodes=serve_nodes) + result = self._callFUT(cluster_pb, 'serve_nodes', serve_nodes) + self.assertEqual(result, serve_nodes) + + def test_with_null_value_passed_in(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + serve_nodes = None + actual_serve_nodes = 119 + cluster_pb = data_pb2.Cluster(serve_nodes=actual_serve_nodes) + result = self._callFUT(cluster_pb, 'serve_nodes', serve_nodes) + self.assertEqual(result, actual_serve_nodes) + + def test_with_value_unset_on_pb(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + serve_nodes = 119 + cluster_pb = data_pb2.Cluster() + with self.assertRaises(ValueError): + self._callFUT(cluster_pb, 'serve_nodes', serve_nodes) + + def test_with_values_disagreeing(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + serve_nodes = 119 + other_serve_nodes = 1000 + self.assertNotEqual(serve_nodes, other_serve_nodes) + cluster_pb = data_pb2.Cluster(serve_nodes=other_serve_nodes) + with self.assertRaises(ValueError): + self._callFUT(cluster_pb, 'serve_nodes', serve_nodes) + + +class _Client(object): + + def __init__(self, project): + self.project = project