From 08b720364f30ee4e8ad37822b5eccd3a9b429d74 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Jan 2017 12:14:31 -0500 Subject: [PATCH] Add ZK API methods for node requests Adds ZooKeeper API methods to get the current list of outstanding node requests, and to get the data for an individual node request. A new NodeRequest object is introduced to the data model. The model will be expanded on in future reviews. Change-Id: I4af96e4e307cc5ce5d3208462e7335c24eece952 --- nodepool/tests/test_zk.py | 63 +++++++++++++++++++++++++++++--- nodepool/zk.py | 77 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 10 deletions(-) diff --git a/nodepool/tests/test_zk.py b/nodepool/tests/test_zk.py index 30a0ada63..426ebe81b 100644 --- a/nodepool/tests/test_zk.py +++ b/nodepool/tests/test_zk.py @@ -437,27 +437,58 @@ class TestZooKeeper(tests.DBTestCase): self.assertEqual(1, len(launchers)) self.assertEqual(name, launchers[0]) + def test_getNodeRequests_empty(self): + self.assertEqual([], self.zk.getNodeRequests()) + + def test_getNodeRequests(self): + r1 = self.zk._requestPath("500-123") + r2 = self.zk._requestPath("100-456") + r3 = self.zk._requestPath("100-123") + r4 = self.zk._requestPath("400-123") + self.zk.client.create(r1, makepath=True, ephemeral=True) + self.zk.client.create(r2, makepath=True, ephemeral=True) + self.zk.client.create(r3, makepath=True, ephemeral=True) + self.zk.client.create(r4, makepath=True, ephemeral=True) + + self.assertEqual( + ["100-123", "100-456", "400-123", "500-123"], + self.zk.getNodeRequests() + ) + + def test_getNodeRequest(self): + r = zk.NodeRequest("500-123") + r.state = zk.READY + path = self.zk._requestPath(r.id) + self.zk.client.create(path, value=self.zk._dictToStr(r.toDict()), + makepath=True, ephemeral=True) + o = self.zk.getNodeRequest(r.id) + self.assertIsInstance(o, zk.NodeRequest) + self.assertEqual(r.id, o.id) + + def test_getNodeRequest_not_found(self): + self.assertIsNone(self.zk.getNodeRequest("invalid")) + class TestZKModel(tests.BaseTestCase): def setUp(self): super(TestZKModel, self).setUp() - def test_BaseBuilderModel_bad_id(self): + def test_BaseModel_bad_id(self): with testtools.ExpectedException( TypeError, "'id' attribute must be a string type" ): - zk.BaseBuilderModel(123) + zk.BaseModel(123) - def test_BaseBuilderModel_bad_state(self): + def test_BaseModel_bad_state(self): with testtools.ExpectedException( TypeError, "'blah' is not a valid state" ): - o = zk.BaseBuilderModel('0001') + o = zk.BaseModel('0001') o.state = 'blah' - def test_BaseBuilderModel_toDict(self): - o = zk.BaseBuilderModel('0001') + def test_BaseModel_toDict(self): + o = zk.BaseModel('0001') o.state = zk.BUILDING d = o.toDict() self.assertNotIn('id', d) @@ -524,3 +555,23 @@ class TestZKModel(tests.BaseTestCase): self.assertEqual(o.state_time, d['state_time']) self.assertEqual(o.external_id, d['external_id']) self.assertEqual(o.external_name, d['external_name']) + + def test_NodeRequest_toDict(self): + o = zk.NodeRequest("500-123") + d = o.toDict() + self.assertNotIn('id', d) + self.assertIn('state', d) + self.assertIn('state_time', d) + + def test_NodeRequest_fromDict(self): + now = int(time.time()) + req_id = "500-123" + d = { + 'state': zk.READY, + 'state_time': now + } + + o = zk.NodeRequest.fromDict(d, req_id) + self.assertEqual(o.id, req_id) + self.assertEqual(o.state, d['state']) + self.assertEqual(o.state_time, d['state_time']) diff --git a/nodepool/zk.py b/nodepool/zk.py index e6c0e2430..749cdd3aa 100644 --- a/nodepool/zk.py +++ b/nodepool/zk.py @@ -106,7 +106,7 @@ class ZooKeeperWatchEvent(object): self.image = image -class BaseBuilderModel(object): +class BaseModel(object): def __init__(self, o_id): if o_id: self.id = o_id @@ -137,7 +137,7 @@ class BaseBuilderModel(object): def toDict(self): ''' - Convert a BaseBuilderModel object's attributes to a dictionary. + Convert a BaseModel object's attributes to a dictionary. ''' d = {} d['state'] = self.state @@ -157,7 +157,7 @@ class BaseBuilderModel(object): self.state_time = d['state_time'] -class ImageBuild(BaseBuilderModel): +class ImageBuild(BaseModel): ''' Class representing a DIB image build within the ZooKeeper cluster. ''' @@ -216,7 +216,7 @@ class ImageBuild(BaseBuilderModel): return o -class ImageUpload(BaseBuilderModel): +class ImageUpload(BaseModel): ''' Class representing a provider image upload within the ZooKeeper cluster. ''' @@ -277,6 +277,42 @@ class ImageUpload(BaseBuilderModel): return o +class NodeRequest(BaseModel): + ''' + Class representing a node request. + ''' + + def __init__(self, id=None): + super(NodeRequest, self).__init__(id) + + def __repr__(self): + d = self.toDict() + d['id'] = self.id + d['stat'] = self.stat + return '' % d + + def toDict(self): + ''' + Convert a NodeRequest object's attributes to a dictionary. + ''' + d = super(NodeRequest, self).toDict() + return d + + @staticmethod + def fromDict(d, o_id=None): + ''' + Create a NodeRequest object from a dictionary. + + :param dict d: The dictionary. + :param str o_id: The object ID. + + :returns: An initialized ImageBuild object. + ''' + o = NodeRequest(o_id) + super(NodeRequest, o).fromDict(d) + return o + + class ZooKeeper(object): ''' Class implementing the ZooKeeper interface. @@ -297,6 +333,7 @@ class ZooKeeper(object): IMAGE_ROOT = "/nodepool/images" LAUNCHER_ROOT = "/nodepool/launchers" + REQUEST_ROOT = "/nodepool/requests" def __init__(self): ''' @@ -341,6 +378,9 @@ class ZooKeeper(object): def _launcherPath(self, launcher): return "%s/%s" % (self.LAUNCHER_ROOT, launcher) + def _requestPath(self, request): + return "%s/%s" % (self.REQUEST_ROOT, request) + def _dictToStr(self, data): return json.dumps(data) @@ -1024,3 +1064,32 @@ class ZooKeeper(object): return [] return launchers + + def getNodeRequests(self): + ''' + Get the current list of all node requests in priority sorted order. + + :returns: A list of request nodes. + ''' + try: + requests = self.client.get_children(self.REQUEST_ROOT) + except kze.NoNodeError: + return [] + + return sorted(requests) + + def getNodeRequest(self, request): + ''' + Get the data for a specific node request. + + :returns: The request data, or None if the request was not found. + ''' + path = self._requestPath(request) + try: + data, stat = self.client.get(path) + except kze.NoNodeError: + return None + + d = NodeRequest.fromDict(self._strToDict(data), request) + d.stat = stat + return d