diff --git a/freezer/apiclient/backups.py b/freezer/apiclient/backups.py index a13802c2..5f4628dd 100644 --- a/freezer/apiclient/backups.py +++ b/freezer/apiclient/backups.py @@ -29,10 +29,13 @@ class BackupsManager(object): def __init__(self, client): self.client = client - self.endpoint = self.client.endpoint + 'backups/' - self.headers = {'X-Auth-Token': client.token} + self.endpoint = self.client.api_endpoint + '/v1/backups/' - def create(self, backup_metadata, username=None, tenant_name=None): + @property + def headers(self): + return {'X-Auth-Token': self.client.auth_token} + + def create(self, backup_metadata): r = requests.post(self.endpoint, data=json.dumps(backup_metadata), headers=self.headers) @@ -42,14 +45,14 @@ class BackupsManager(object): backup_id = r.json()['backup_id'] return backup_id - def delete(self, backup_id, username=None, tenant_name=None): + def delete(self, backup_id): endpoint = self.endpoint + backup_id r = requests.delete(endpoint, headers=self.headers) if r.status_code != 204: raise exceptions.MetadataDeleteFailure( "[*] Error {0}".format(r.status_code)) - def list(self, username=None, tenant_name=None): + def list(self): r = requests.get(self.endpoint, headers=self.headers) if r.status_code != 200: raise exceptions.MetadataGetFailure( @@ -57,7 +60,7 @@ class BackupsManager(object): return r.json()['backups'] - def get(self, backup_id, username=None, tenant_name=None): + def get(self, backup_id): endpoint = self.endpoint + backup_id r = requests.get(endpoint, headers=self.headers) if r.status_code == 200: diff --git a/freezer/apiclient/client.py b/freezer/apiclient/client.py index b587b96e..13e5ae5c 100644 --- a/freezer/apiclient/client.py +++ b/freezer/apiclient/client.py @@ -22,14 +22,15 @@ Hudson (tjh@cryptsoft.com). import os import sys +from openstackclient.identity import client as os_client + possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, os.pardir, os.pardir)) if os.path.exists(os.path.join(possible_topdir, 'freezer', '__init__.py')): sys.path.insert(0, possible_topdir) -import keystoneclient - from freezer.apiclient.backups import BackupsManager +import exceptions class Client(object): @@ -40,26 +41,66 @@ class Client(object): password=None, tenant_name=None, auth_url=None, - endpoint=None, - session=None): - if endpoint is None: - raise Exception('Missing endpoint information') - self.endpoint = endpoint - - if token is not None: - # validate the token ? - self.token = token - elif session is not None: - pass - # TODO: handle session auth - # assert isinstance(session, keystoneclient.session.Session) - else: - self.username = username - self.tenant_name = tenant_name - kc = keystoneclient.v2_0.client.Client( - username=username, - password=password, - tenant_name=tenant_name, - auth_url=auth_url) - self.token = kc.auth_token + session=None, + api_endpoint=None): + self.version = version + self.token = token + self.username = username + self.tenant_name = tenant_name + self.password = password + self.auth_url = auth_url + self._api_endpoint = api_endpoint + self.session = session + self._auth = None self.backups = BackupsManager(self) + + def _update_api_endpoint(self): + services = self.auth.services.list() + try: + freezer_service = next(x for x in services if x.name == 'freezer') + except: + raise exceptions.AuthFailure( + 'freezer service not found in services list') + endpoints = self.auth.endpoints.list() + try: + freezer_endpoint =\ + next(x for x in endpoints + if x.service_id == freezer_service.id) + except: + raise exceptions.AuthFailure( + 'freezer endpoint not found in endpoint list') + self._api_endpoint = freezer_endpoint.publicurl + + @property + def auth(self): + if self._auth is None: + if self.username and self.password: + self._auth = os_client.IdentityClientv2( + auth_url=self.auth_url, + username=self.username, + password=self.password, + tenant_name=self.tenant_name) + elif self.token: + self._auth = os_client.IdentityClientv2( + endpoint=self.auth_url, + token=self.token) + else: + raise exceptions.AuthFailure("Missing auth credentials") + return self._auth + + @property + def auth_token(self): + return self.auth.auth_token + + @property + def api_endpoint(self): + if self._api_endpoint is None: + self._update_api_endpoint() + return self._api_endpoint + + def api_exists(self): + try: + if self.api_endpoint is not None: + return True + except: + return False diff --git a/freezer/apiclient/exceptions.py b/freezer/apiclient/exceptions.py index 4b7b4fe9..c91381f4 100644 --- a/freezer/apiclient/exceptions.py +++ b/freezer/apiclient/exceptions.py @@ -20,27 +20,17 @@ Hudson (tjh@cryptsoft.com). """ -class FreezerClientException(Exception): - """ - Base Freezer API Exception - """ - message = ("Unknown exception occurred") - - def __init__(self, message=None, *args, **kwargs): - if not message: - message = self.message - message = message % kwargs - - Exception.__init__(self, message) - - -class MetadataCreationFailure(FreezerClientException): +class MetadataCreationFailure(Exception): message = "Metadata creation failed: %reason" -class MetadataGetFailure(FreezerClientException): +class MetadataGetFailure(Exception): message = "Metadata read failed: %reason" -class MetadataDeleteFailure(FreezerClientException): +class MetadataDeleteFailure(Exception): message = "Metadata deletion failed: %reason" + + +class AuthFailure(Exception): + message = "Authentication Error: %reason" diff --git a/freezer_api/README.rst b/freezer_api/README.rst index f712a178..18a963bc 100644 --- a/freezer_api/README.rst +++ b/freezer_api/README.rst @@ -61,6 +61,22 @@ utilizes the timestamp of the first (level 0) backup in the session It is identified by (container, hostname, backupname, timestamp-of-level-0) +API registration +================ +keystone user-create --name freezer --pass FREEZER_PWD +keystone user-role-add --user freezer --tenant service --role admin + +keystone service-create --name freezer --type backup \ + --description "Freezer Backup Service" + +keystone endpoint-create \ + --service-id $(keystone service-list | awk '/ backup / {print $2}') \ + --publicurl http://freezer_api_publicurl:port \ + --internalurl http://freezer_api_internalurl:port \ + --adminurl http://freezer_api_adminurl:port \ + --region regionOne + + API routes ========== diff --git a/tests/test_apiclient_backup.py b/tests/test_apiclient_backup.py new file mode 100644 index 00000000..64004ca9 --- /dev/null +++ b/tests/test_apiclient_backup.py @@ -0,0 +1,112 @@ +"""Freezer swift.py related tests + +Copyright 2014 Hewlett-Packard + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). +======================================================================== + +""" + +import unittest +from mock import Mock, patch + +from freezer.apiclient import exceptions +from freezer.apiclient import backups + + +class TestBackupManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_client.api_endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.b = backups.BackupsManager(self.mock_client) + + @patch('freezer.apiclient.backups.requests') + def test_create(self, mock_requests): + self.assertEqual(self.b.endpoint, 'http://testendpoint:9999/v1/backups/') + self.assertEqual(self.b.headers, {'X-Auth-Token': 'testtoken'}) + + @patch('freezer.apiclient.backups.requests') + def test_create_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 201 + mock_response.json.return_value = {'backup_id': 'qwerqwer'} + mock_requests.post.return_value = mock_response + retval = self.b.create(backup_metadata={'backup': 'metadata'}) + self.assertEqual(retval, 'qwerqwer') + + @patch('freezer.apiclient.backups.requests') + def test_create_fail(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + #mock_response.json.return_value = {'backup_id': 'qwerqwer'} + mock_requests.post.return_value = mock_response + self.assertRaises(exceptions.MetadataCreationFailure, self.b.create, {'backup': 'metadata'}) + + @patch('freezer.apiclient.backups.requests') + def test_delete_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 204 + mock_requests.delete.return_value = mock_response + retval = self.b.delete('test_backup_id') + self.assertIsNone(retval) + + @patch('freezer.apiclient.backups.requests') + def test_delete_fail(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.delete.return_value = mock_response + self.assertRaises(exceptions.MetadataDeleteFailure, self.b.delete, 'test_backup_id') + + @patch('freezer.apiclient.backups.requests') + def test_get_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'backup_id': 'qwerqwer'} + mock_requests.get.return_value = mock_response + retval = self.b.get('test_backup_id') + self.assertEqual(retval, {'backup_id': 'qwerqwer'}) + + @patch('freezer.apiclient.backups.requests') + def test_get_none(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + mock_requests.get.return_value = mock_response + retval = self.b.get('test_backup_id') + self.assertIsNone(retval) + + # get_error + + @patch('freezer.apiclient.backups.requests') + def test_list_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + backup_list = [{'backup_id_0': 'qwerqwer'}, {'backup_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'backups': backup_list} + mock_requests.get.return_value = mock_response + retval = self.b.list() + self.assertEqual(retval, backup_list) + + @patch('freezer.apiclient.backups.requests') + def test_list_error(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + backup_list = [{'backup_id_0': 'qwerqwer'}, {'backup_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'backups': backup_list} + mock_requests.get.return_value = mock_response + self.assertRaises(exceptions.MetadataGetFailure, self.b.list) diff --git a/tests/test_apiclient_client.py b/tests/test_apiclient_client.py new file mode 100644 index 00000000..e66775b3 --- /dev/null +++ b/tests/test_apiclient_client.py @@ -0,0 +1,108 @@ +"""Freezer swift.py related tests + +Copyright 2014 Hewlett-Packard + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). +======================================================================== + +""" + +import unittest +from mock import Mock, patch + +from freezer.apiclient import client +from freezer.apiclient import exceptions + + +class TestClientMock(unittest.TestCase): + + def create_mock_endpoint(self, service_id): + m = Mock() + m.service_id = service_id + m.publicurl = 'http://frezerapiurl:9090' + return m + + def create_mock_service(self, name, id): + m = Mock() + m.name = name + m.id = id + return m + + def setUp(self): + mock_enpointlist_ok = [self.create_mock_endpoint('idqwerty'), + self.create_mock_endpoint('idfreak'), + self.create_mock_endpoint('blabla')] + mock_servicelist_ok = [self.create_mock_service(name='glance', id='idqwerty'), + self.create_mock_service(name='freezer', id='idfreak')] + self.mock_IdentityClientv2 = Mock() + self.mock_IdentityClientv2.endpoints.list.return_value = mock_enpointlist_ok + self.mock_IdentityClientv2.services.list.return_value = mock_servicelist_ok + + @patch('freezer.apiclient.client.os_client') + def test_client_create_username(self, mock_os_client): + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + c = client.Client(username='myname', + password='mypasswd', + tenant_name='mytenant', + auth_url='http://whatever:35357/v2.0/') + self.assertIsInstance(c, client.Client) + self.assertEqual(c.api_endpoint, 'http://frezerapiurl:9090') + + @patch('freezer.apiclient.client.os_client') + def test_client_create_token(self, mock_os_client): + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + c = client.Client(token='mytoken', + auth_url='http://whatever:35357/v2.0/') + self.assertIsInstance(c, client.Client) + self.assertEqual(c.api_endpoint, 'http://frezerapiurl:9090') + + @patch('freezer.apiclient.client.os_client') + def test_client_error_no_credentials(self, mock_os_client): + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + self.assertRaises(exceptions.AuthFailure, client.Client, auth_url='http://whatever:35357/v2.0/') + + @patch('freezer.apiclient.client.os_client') + def test_client_service_not_found(self, mock_os_client): + mock_servicelist_bad = [self.create_mock_service(name='glance', id='idqwerty'), + self.create_mock_service(name='spanishinquisition', id='idfreak')] + self.mock_IdentityClientv2.services.list.return_value = mock_servicelist_bad + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + self.assertRaises(exceptions.AuthFailure, client.Client, token='mytoken', auth_url='http://whatever:35357/v2.0/') + + @patch('freezer.apiclient.client.os_client') + def test_client_endpoint_not_found(self, mock_os_client): + mock_enpointlist_bad = [self.create_mock_endpoint('idqwerty'), + self.create_mock_endpoint('idfiasco'), + self.create_mock_endpoint('blabla')] + self.mock_IdentityClientv2.endpoints.list.return_value = mock_enpointlist_bad + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + self.assertRaises(exceptions.AuthFailure, client.Client, token='mytoken', auth_url='http://whatever:35357/v2.0/') + + @patch('freezer.apiclient.client.os_client') + def test_client_api_exists(self, mock_os_client): + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + c = client.Client(token='mytoken', + auth_url='http://whatever:35357/v2.0/') + self.assertTrue(c.api_exists()) + + @patch('freezer.apiclient.client.os_client') + def test_client_auth_token(self, mock_os_client): + self.mock_IdentityClientv2.auth_token = 'stotoken' + mock_os_client.IdentityClientv2.return_value = self.mock_IdentityClientv2 + c = client.Client(token='mytoken', + auth_url='http://whatever:35357/v2.0/') + self.assertEqual(c.auth_token, 'stotoken') diff --git a/tox.ini b/tox.ini index c7fe7fea..ab55742f 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,8 @@ deps = pytest-cov pytest-xdist pymysql + python-openstackclient + mock install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir}