From 03033b549d711b76682e2a72de9644820948336a Mon Sep 17 00:00:00 2001 From: Stavros Sachtouris Date: Wed, 29 May 2013 17:41:04 +0300 Subject: [PATCH] Impleent floating ip methods @compute/cyclades Refs: #3862 New methods introduced and unit-tested: ComputeRestClient: floating_ip_pools_get, floating_ips_get/post/delete CycladesRestClient: floating_ip_pools_get, floating_ips_get/post/delete ComputeClient: get_floating_ip_pools, get_floating_ips, alloc/get_delete_floating_ip CycladesClient: get_floating_ip_pools, get_floating_ips, alloc/get_delete_floating_ip, dis/assoc_floating_ip_to_server --- Changelog | 8 ++- kamaki/clients/compute/__init__.py | 58 +++++++++++++++++ kamaki/clients/compute/rest_api.py | 17 ++--- kamaki/clients/compute/test.py | 96 ++++++++++++++++++++------- kamaki/clients/cyclades/__init__.py | 103 +++++++++++++++++++++++++++++ kamaki/clients/cyclades/rest_api.py | 16 ++--- kamaki/clients/cyclades/test.py | 123 ++++++++++++++++++++++++++++------- 7 files changed, 354 insertions(+), 67 deletions(-) diff --git a/Changelog b/Changelog index 3dffa93..2456110 100644 --- a/Changelog +++ b/Changelog @@ -5,5 +5,11 @@ Bug Fixes: Changes: Features: - +- Implement floating ip methods for compute and cyclades clients [#3862] + ComputeRestClient: floating_ip_pools_get, floating_ips_get/post/delete + CycladesRestClient: floating_ip_pools_get, floating_ips_get/post/delete + ComputeClient: get_floating_ip_pools, get_floating_ips, + alloc/get_delete_floating_ip + CycladesClient: get_floating_ip_pools, get_floating_ips, + alloc/get_delete_floating_ip, dis/assoc_floating_ip_to_server diff --git a/kamaki/clients/compute/__init__.py b/kamaki/clients/compute/__init__.py index 3cfbeb8..2464e98 100644 --- a/kamaki/clients/compute/__init__.py +++ b/kamaki/clients/compute/__init__.py @@ -297,3 +297,61 @@ class ComputeClient(ComputeRestClient): command = path4url('metadata', key) r = self.images_delete(image_id, command) return r.headers + + def get_floating_ip_pools(self, tenant_id): + """ + :param tenant_id: (str) + + :returns: (dict) {floating_ip_pools:[{name: ...}, ...]} + """ + r = self.floating_ip_pools_get(tenant_id) + return r.json + + def get_floating_ips(self, tenant_id): + """ + :param tenant_id: (str) + + :returns: (dict) {floating_ips:[ + {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}, + ... ]} + """ + r = self.floating_ips_get(tenant_id) + return r.json + + def alloc_floating_ip(self, tenant_id, pool=None): + """ + :param tenant_id: (str) + + :param pool: (str) pool of ips to allocate from + + :returns: (dict) { + fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ... + } + """ + json_data = dict(pool=pool) if pool else dict() + r = self.floating_ips_post(tenant_id, json_data) + return r.json['floating_ip'] + + def get_floating_ip(self, tenant_id, fip_id=None): + """ + :param tenant_id: (str) + + :param fip_id: (str) floating ip id (if None, all ips are returned) + + :returns: (list) [ + {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}, + ... ] + """ + r = self.floating_ips_get(tenant_id, fip_id) + return r.json['floating_ips'] + + def delete_floating_ip(self, tenant_id, fip_id=None): + """ + :param tenant_id: (str) + + :param fip_id: (str) floating ip id (if None, all ips are deleted) + + :returns: (dict) request headers + """ + r = self.floating_ips_delete(tenant_id, fip_id) + return r.headers diff --git a/kamaki/clients/compute/rest_api.py b/kamaki/clients/compute/rest_api.py index 6172ce2..fb3b7a3 100644 --- a/kamaki/clients/compute/rest_api.py +++ b/kamaki/clients/compute/rest_api.py @@ -242,22 +242,19 @@ class ComputeRestClient(Client): path = path4url(tenant_id, 'os-floating-ip-pools') return self.get(path, success=success, **kwargs) - def floating_ips_get(self, tenant_id, success=200, **kwargs): - path = path4url(tenant_id, 'os-floating-ips') + def floating_ips_get(self, tenant_id, ip='', success=200, **kwargs): + path = path4url(tenant_id, 'os-floating-ips', ip or '') return self.get(path, success=success, **kwargs) - def floating_ips_post(self, tenant_id, json_data, success=201, **kwargs): - path = path4url(tenant_id, 'os-floating-ips') + def floating_ips_post( + self, tenant_id, json_data, ip='', success=201, **kwargs): + path = path4url(tenant_id, 'os-floating-ips', ip or '') if json_data is not None: json_data = json.dumps(json_data) self.set_header('Content-Type', 'application/json') self.set_header('Content-Length', len(json_data)) return self.post(path, data=json_data, success=success, **kwargs) - def floating_ip_get(self, tenant_id, success=200, **kwargs): - path = path4url(tenant_id, 'os-floating-ip') - return self.get(path, success=success, **kwargs) - - def floating_ip_delete(self, tenant_id, success=204, **kwargs): - path = path4url(tenant_id, 'os-floating-ip') + def floating_ips_delete(self, tenant_id, ip='', success=204, **kwargs): + path = path4url(tenant_id, 'os-floating-ips', ip or '') return self.delete(path, success=success, **kwargs) diff --git a/kamaki/clients/compute/test.py b/kamaki/clients/compute/test.py index 819aa01..c0d32ed 100644 --- a/kamaki/clients/compute/test.py +++ b/kamaki/clients/compute/test.py @@ -244,13 +244,15 @@ class ComputeRestClient(TestCase): def test_floating_ips_get(self, get): for args in product( ('tenant1', 'tenant2'), + ('', '192.193.194.195'), (200, 204), ({}, {'k': 'v'})): - tenant_id, success, kwargs = args - r = self.client.floating_ips_get(tenant_id, success, **kwargs) + tenant_id, ip, success, kwargs = args + r = self.client.floating_ips_get(*args[:3], **kwargs) self.assertTrue(isinstance(r, FR)) + expected = '' if not ip else '/%s' % ip self.assertEqual(get.mock_calls[-1], call( - '/%s/os-floating-ips' % tenant_id, + '/%s/os-floating-ips%s' % (tenant_id, expected), success=success, **kwargs)) @patch('%s.set_header' % rest_pkg) @@ -259,44 +261,35 @@ class ComputeRestClient(TestCase): for args in product( ('tenant1', 'tenant2'), (None, [dict(json="data"), dict(data="json")]), + ('', '192.193.194.195'), (202, 204), ({}, {'k': 'v'})): - (tenant_id, json_data, success, kwargs) = args - self.client.floating_ips_post(*args[:3], **kwargs) + (tenant_id, json_data, ip, success, kwargs) = args + self.client.floating_ips_post(*args[:4], **kwargs) if json_data: json_data = dumps(json_data) self.assertEqual(SH.mock_calls[-2:], [ call('Content-Type', 'application/json'), call('Content-Length', len(json_data))]) + expected = '' if not ip else '/%s' % ip self.assertEqual(post.mock_calls[-1], call( - '/%s/os-floating-ips' % tenant_id, + '/%s/os-floating-ips%s' % (tenant_id, expected), data=json_data, success=success, **kwargs)) - @patch('%s.get' % rest_pkg, return_value=FR()) - def test_floating_ip_get(self, get): - for args in product( - ('tenant1', 'tenant2'), - (200, 204), - ({}, {'k': 'v'})): - tenant_id, success, kwargs = args - r = self.client.floating_ip_get(tenant_id, success, **kwargs) - self.assertTrue(isinstance(r, FR)) - self.assertEqual(get.mock_calls[-1], call( - '/%s/os-floating-ip' % tenant_id, - success=success, **kwargs)) - @patch('%s.delete' % rest_pkg, return_value=FR()) - def test_floating_ip_delete(self, delete): + def test_floating_ips_delete(self, delete): for args in product( ('tenant1', 'tenant2'), + ('', '192.193.194.195'), (204,), ({}, {'k': 'v'})): - tenant_id, success, kwargs = args - r = self.client.floating_ip_delete(tenant_id, success, **kwargs) + tenant_id, ip, success, kwargs = args + r = self.client.floating_ips_delete(*args[:3], **kwargs) self.assertTrue(isinstance(r, FR)) + expected = '' if not ip else '/%s' % ip self.assertEqual(delete.mock_calls[-1], call( - '/%s/os-floating-ip' % tenant_id, + '/%s/os-floating-ips%s' % (tenant_id, expected), success=success, **kwargs)) @@ -508,6 +501,63 @@ class ComputeClient(TestCase): self.client.delete_image_metadata(img_ref, key) ID.assert_called_once_with(img_ref, '/metadata/%s' % key) + @patch('%s.floating_ip_pools_get' % compute_pkg, return_value=FR()) + def test_get_floating_ip_pools(self, get): + tid = 't3n@nt_1d' + r = self.client.get_floating_ip_pools(tid) + self.assert_dicts_are_equal(r, FR.json) + self.assertEqual(get.mock_calls[-1], call(tid)) + + @patch('%s.floating_ips_get' % compute_pkg, return_value=FR()) + def test_get_floating_ips(self, get): + tid = 't3n@nt_1d' + r = self.client.get_floating_ips(tid) + self.assert_dicts_are_equal(r, FR.json) + self.assertEqual(get.mock_calls[-1], call(tid)) + + @patch('%s.floating_ips_post' % compute_pkg, return_value=FR()) + def test_alloc_floating_ip(self, post): + FR.json = dict(floating_ip=dict( + fixed_ip='fip', + id=1, + instance_id='lala', + ip='102.0.0.1', + pool='pisine')) + for args in product( + ('t1', 't2'), + (None, 'pisine')): + r = self.client.alloc_floating_ip(*args) + tenant_id, pool = args + self.assert_dicts_are_equal(r, FR.json['floating_ip']) + expected = dict(pool=pool) if pool else dict() + self.assertEqual(post.mock_calls[-1], call(tenant_id, expected)) + + @patch('%s.floating_ips_get' % compute_pkg, return_value=FR()) + def test_get_floating_ip(self, get): + FR.json = dict(floating_ips=[dict( + fixed_ip='fip', + id=1, + instance_id='lala', + ip='102.0.0.1', + pool='pisine'), ]) + for args in product( + ('t1', 't2'), + (None, 'fip')): + r = self.client.get_floating_ip(*args) + tenant_id, fip = args + self.assertEqual(r, FR.json['floating_ips']) + self.assertEqual(get.mock_calls[-1], call(tenant_id, fip)) + + @patch('%s.floating_ips_delete' % compute_pkg, return_value=FR()) + def test_delete_floating_ip(self, delete): + for args in product( + ('t1', 't2'), + (None, 'fip')): + r = self.client.delete_floating_ip(*args) + tenant_id, fip = args + self.assertEqual(r, FR.headers) + self.assertEqual(delete.mock_calls[-1], call(tenant_id, fip)) + if __name__ == '__main__': from sys import argv diff --git a/kamaki/clients/cyclades/__init__.py b/kamaki/clients/cyclades/__init__.py index 28b56d6..de4cbeb 100644 --- a/kamaki/clients/cyclades/__init__.py +++ b/kamaki/clients/cyclades/__init__.py @@ -316,3 +316,106 @@ class CycladesClient(CycladesRestClient): pass return r['status'] return False + + def get_floating_ip_pools(self): + """ + :returns: (dict) {floating_ip_pools:[{name: ...}, ...]} + """ + r = self.floating_ip_pools_get() + return r.json + + def get_floating_ips(self): + """ + :returns: (dict) {floating_ips:[ + {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}, + ... ]} + """ + r = self.floating_ips_get() + return r.json + + def alloc_floating_ip(self, pool=None, address=None): + """ + :param pool: (str) pool of ips to allocate from + + :param address: (str) ip address to request + + :returns: (dict) { + fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ... + } + """ + json_data = dict() + if pool: + json_data['pool'] = pool + if address: + json_data['address'] = address + r = self.floating_ips_post(json_data) + return r.json['floating_ip'] + + def get_floating_ip(self, fip_id): + """ + :param fip_id: (str) floating ip id + + :returns: (dict) + {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}, + + :raises AssertionError: if fip_id is emtpy + """ + assert fip_id, 'floating ip id is needed for get_floating_ip' + r = self.floating_ips_get(fip_id) + return r.json['floating_ip'] + + def delete_floating_ip(self, fip_id=None): + """ + :param fip_id: (str) floating ip id (if None, all ips are deleted) + + :returns: (dict) request headers + + :raises AssertionError: if fip_id is emtpy + """ + assert fip_id, 'floating ip id is needed for delete_floating_ip' + r = self.floating_ips_delete(fip_id) + return r.headers + + def assoc_floating_ip_to_server(self, server_id, address): + """Associate the address ip to server with server_id + + :param server_id: (int) + + :param address: (str) the ip address to assign to server (vm) + + :returns: (dict) request headers + + :raises ValueError: if server_id cannot be converted to int + + :raises ValueError: if server_id is not of a int-convertable type + + :raises AssertionError: if address is emtpy + """ + server_id = int(server_id) + assert address, 'address is needed for assoc_floating_ip_to_server' + r = self.servers_post( + server_id, 'action', + json_data=dict(addFloatingIp=dict(address=address))) + return r.headers + + def disassoc_floating_ip_to_server(self, server_id, address): + """Disassociate an address ip from the server with server_id + + :param server_id: (int) + + :param address: (str) the ip address to assign to server (vm) + + :returns: (dict) request headers + + :raises ValueError: if server_id cannot be converted to int + + :raises ValueError: if server_id is not of a int-convertable type + + :raises AssertionError: if address is emtpy + """ + server_id = int(server_id) + assert address, 'address is needed for disassoc_floating_ip_to_server' + r = self.servers_post( + server_id, 'action', + json_data=dict(removeFloatingIp=dict(address=address))) + return r.headers diff --git a/kamaki/clients/cyclades/rest_api.py b/kamaki/clients/cyclades/rest_api.py index eb469b0..cbe8c41 100644 --- a/kamaki/clients/cyclades/rest_api.py +++ b/kamaki/clients/cyclades/rest_api.py @@ -170,22 +170,18 @@ class CycladesRestClient(ComputeClient): path = path4url('os-floating-ip-pools') return self.get(path, success=success, **kwargs) - def floating_ips_get(self, success=200, **kwargs): - path = path4url('os-floating-ips') + def floating_ips_get(self, fip_id='', success=200, **kwargs): + path = path4url('os-floating-ips', fip_id) return self.get(path, success=success, **kwargs) - def floating_ips_post(self, json_data, success=201, **kwargs): - path = path4url('os-floating-ips') + def floating_ips_post(self, json_data, fip_id='', success=201, **kwargs): + path = path4url('os-floating-ips', fip_id) if json_data is not None: json_data = json.dumps(json_data) self.set_header('Content-Type', 'application/json') self.set_header('Content-Length', len(json_data)) return self.post(path, data=json_data, success=success, **kwargs) - def floating_ip_get(self, floating_ip_id, success=200, **kwargs): - path = path4url('os-floating-ip', floating_ip_id) - return self.get(path, success=success, **kwargs) - - def floating_ip_delete(self, floating_ip_id, success=200, **kwargs): - path = path4url('os-floating-ip', floating_ip_id) + def floating_ips_delete(self, fip_id, success=200, **kwargs): + path = path4url('os-floating-ips', fip_id) return self.delete(path, success=success, **kwargs) diff --git a/kamaki/clients/cyclades/test.py b/kamaki/clients/cyclades/test.py index 03582ec..f1b8a97 100644 --- a/kamaki/clients/cyclades/test.py +++ b/kamaki/clients/cyclades/test.py @@ -160,7 +160,6 @@ class CycladesRestClient(TestCase): @patch('%s.set_header' % rest_pkg) @patch('%s.post' % rest_pkg, return_value=FR()) def test_networks_post(self, post, SH): - from json import dumps for args in product( ('', 'net_id'), ('', 'cmd'), @@ -184,7 +183,6 @@ class CycladesRestClient(TestCase): @patch('%s.set_header' % rest_pkg) @patch('%s.put' % rest_pkg, return_value=FR()) def test_networks_put(self, put, SH): - from json import dumps for args in product( ('', 'net_id'), ('', 'cmd'), @@ -219,56 +217,48 @@ class CycladesRestClient(TestCase): @patch('%s.get' % rest_pkg, return_value=FR()) def test_floating_ips_get(self, get): for args in product( + ('fip', ''), (200, 204), ({}, {'k': 'v'})): - success, kwargs = args - r = self.client.floating_ips_get(success, **kwargs) + fip, success, kwargs = args + r = self.client.floating_ips_get(fip, success, **kwargs) self.assertTrue(isinstance(r, FR)) + expected = '' if not fip else '/%s' % fip self.assertEqual(get.mock_calls[-1], call( - '/os-floating-ips', success=success, **kwargs)) + '/os-floating-ips%s' % expected, success=success, **kwargs)) @patch('%s.set_header' % rest_pkg) @patch('%s.post' % rest_pkg, return_value=FR()) def test_floating_ips_post(self, post, SH): for args in product( (None, [dict(json="data"), dict(data="json")]), + ('fip', ''), (202, 204), ({}, {'k': 'v'})): - (json_data, success, kwargs) = args - self.client.floating_ips_post(*args[:2], **kwargs) + json_data, fip, success, kwargs = args + self.client.floating_ips_post(*args[:3], **kwargs) if json_data: json_data = dumps(json_data) self.assertEqual(SH.mock_calls[-2:], [ call('Content-Type', 'application/json'), call('Content-Length', len(json_data))]) + expected = '' if not fip else '/%s' % fip self.assertEqual(post.mock_calls[-1], call( - '/os-floating-ips', + '/os-floating-ips%s' % expected, data=json_data, success=success, **kwargs)) - @patch('%s.get' % rest_pkg, return_value=FR()) - def test_floating_ip_get(self, get): - for args in product( - ('fip1', 'fip2'), - (200, 204), - ({}, {'k': 'v'})): - fip, success, kwargs = args - r = self.client.floating_ip_get(fip, success, **kwargs) - self.assertTrue(isinstance(r, FR)) - self.assertEqual(get.mock_calls[-1], call( - '/os-floating-ip/%s' % fip, success=success, **kwargs)) - @patch('%s.delete' % rest_pkg, return_value=FR()) - def test_floating_ip_delete(self, delete): + def test_floating_ips_delete(self, delete): for args in product( ('fip1', 'fip2'), (200, 204), ({}, {'k': 'v'})): fip, success, kwargs = args - r = self.client.floating_ip_delete(fip, success, **kwargs) + r = self.client.floating_ips_delete(fip, success, **kwargs) self.assertTrue(isinstance(r, FR)) self.assertEqual(delete.mock_calls[-1], call( - '/os-floating-ip/%s' % fip, success=success, **kwargs)) + '/os-floating-ips/%s' % fip, success=success, **kwargs)) class CycladesClient(TestCase): @@ -499,6 +489,93 @@ class CycladesClient(TestCase): self.assertEqual(err.details, [ 'Network may be still connected to at least one server']) + @patch('%s.floating_ip_pools_get' % cyclades_pkg, return_value=FR()) + def test_get_floating_ip_pools(self, get): + r = self.client.get_floating_ip_pools() + self.assert_dicts_are_equal(r, FR.json) + self.assertEqual(get.mock_calls[-1], call()) + + @patch('%s.floating_ips_get' % cyclades_pkg, return_value=FR()) + def test_get_floating_ips(self, get): + r = self.client.get_floating_ips() + self.assert_dicts_are_equal(r, FR.json) + self.assertEqual(get.mock_calls[-1], call()) + + @patch('%s.floating_ips_post' % cyclades_pkg, return_value=FR()) + def test_alloc_floating_ip(self, post): + FR.json = dict(floating_ip=dict( + fixed_ip='fip', + id=1, + instance_id='lala', + ip='102.0.0.1', + pool='pisine')) + for args in product( + (None, 'pisine'), + (None, 'Iwannanip')): + r = self.client.alloc_floating_ip(*args) + pool, address = args + self.assert_dicts_are_equal(r, FR.json['floating_ip']) + json_data = dict() + if pool: + json_data['pool'] = pool + if address: + json_data['address'] = address + self.assertEqual(post.mock_calls[-1], call(json_data)) + + @patch('%s.floating_ips_get' % cyclades_pkg, return_value=FR()) + def test_get_floating_ip(self, get): + FR.json = dict(floating_ip=dict( + fixed_ip='fip', + id=1, + instance_id='lala', + ip='102.0.0.1', + pool='pisine')) + self.assertRaises(AssertionError, self.client.get_floating_ip, None) + fip = 'fip' + r = self.client.get_floating_ip(fip) + self.assert_dicts_are_equal(r, FR.json['floating_ip']) + self.assertEqual(get.mock_calls[-1], call(fip)) + + @patch('%s.floating_ips_delete' % cyclades_pkg, return_value=FR()) + def test_delete_floating_ip(self, delete): + self.assertRaises(AssertionError, self.client.delete_floating_ip, None) + fip = 'fip' + r = self.client.delete_floating_ip(fip) + self.assert_dicts_are_equal(r, FR.headers) + self.assertEqual(delete.mock_calls[-1], call(fip)) + + @patch('%s.servers_post' % cyclades_pkg, return_value=FR()) + def test_assoc_floating_ip_to_server(self, spost): + vmid, addr = 42, 'anIpAddress' + for err, args in { + ValueError: ['not a server id', addr], + TypeError: [None, addr], + AssertionError: [vmid, None], + AssertionError: [vmid, '']}.items(): + self.assertRaises( + err, self.client.assoc_floating_ip_to_server, *args) + r = self.client.assoc_floating_ip_to_server(vmid, addr) + self.assert_dicts_are_equal(r, FR.headers) + expected = dict(addFloatingIp=dict(address=addr)) + self.assertEqual( + spost.mock_calls[-1], call(vmid, 'action', json_data=expected)) + + @patch('%s.servers_post' % cyclades_pkg, return_value=FR()) + def test_disassoc_floating_ip_to_server(self, spost): + vmid, addr = 42, 'anIpAddress' + for err, args in { + ValueError: ['not a server id', addr], + TypeError: [None, addr], + AssertionError: [vmid, None], + AssertionError: [vmid, '']}.items(): + self.assertRaises( + err, self.client.disassoc_floating_ip_to_server, *args) + r = self.client.disassoc_floating_ip_to_server(vmid, addr) + self.assert_dicts_are_equal(r, FR.headers) + expected = dict(removeFloatingIp=dict(address=addr)) + self.assertEqual( + spost.mock_calls[-1], call(vmid, 'action', json_data=expected)) + if __name__ == '__main__': from sys import argv -- 1.7.10.4