From 44e07a5af6e810300ff787c8765c47da4cd19bd5 Mon Sep 17 00:00:00 2001 From: hubian Date: Tue, 13 Dec 2016 14:09:33 +0800 Subject: [PATCH] implement pod_manager controller for BP:multi-podmanager Change-Id: I1c50e336f8dc85253c8bdd3611cccc11ef1c1300 Closes-Bug: #1646390 --- valence/api/route.py | 9 +- valence/api/v1/podmanagers.py | 54 ++++++ valence/common/constants.py | 19 ++ valence/common/exception.py | 20 ++- valence/controller/podmanagers.py | 118 +++++++++++++ valence/redfish/redfish.py | 16 +- .../tests/unit/controller/test_podmanagers.py | 165 ++++++++++++++++++ valence/tests/unit/redfish/test_redfish.py | 46 ++++- 8 files changed, 440 insertions(+), 7 deletions(-) create mode 100644 valence/api/v1/podmanagers.py create mode 100644 valence/common/constants.py create mode 100644 valence/controller/podmanagers.py create mode 100644 valence/tests/unit/controller/test_podmanagers.py diff --git a/valence/api/route.py b/valence/api/route.py index be0cecc..eaa7673 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -23,10 +23,10 @@ from valence.api import app as flaskapp import valence.api.root as api_root import valence.api.v1.flavors as v1_flavors import valence.api.v1.nodes as v1_nodes +import valence.api.v1.podmanagers as v1_podmanagers import valence.api.v1.storages as v1_storages import valence.api.v1.systems as v1_systems import valence.api.v1.version as v1_version - from valence.common import exception from valence.common import utils @@ -59,7 +59,6 @@ api = ValenceService(app) """API V1.0 Operations""" - # API Root operation api.add_resource(api_root.Root, '/', endpoint='root') @@ -88,5 +87,11 @@ api.add_resource(v1_storages.StoragesList, '/v1/storages', endpoint='storages') api.add_resource(v1_storages.Storages, '/v1/storages/', endpoint='storage') +# PodManager(s) operations +api.add_resource(v1_podmanagers.PodManager, + '/v1/pod_managers/', endpoint='podmanager') +api.add_resource(v1_podmanagers.PodManagersList, + '/v1/pod_managers', endpoint='podmanagers') + # Proxy to PODM api.add_resource(api_root.PODMProxy, '/', endpoint='podmproxy') diff --git a/valence/api/v1/podmanagers.py b/valence/api/v1/podmanagers.py new file mode 100644 index 0000000..c969fbb --- /dev/null +++ b/valence/api/v1/podmanagers.py @@ -0,0 +1,54 @@ +# Copyright (c) 2016 Intel, Inc. +# +# 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. + +import logging + +import flask +import flask_restful +from six.moves import http_client + +from valence.common import exception +from valence.common import utils +from valence.controller import podmanagers + + +LOG = logging.getLogger(__name__) + + +class PodManagersList(flask_restful.Resource): + + def get(self): + return utils.make_response(http_client.OK, podmanagers.get_podm_list()) + + def post(self): + values = flask.request.get_json() + return utils.make_response(http_client.OK, + podmanagers.create_podm(values)) + + +class PodManager(flask_restful.Resource): + + def get(self, podm_uuid): + return utils.make_response(http_client.OK, + podmanagers.get_podm_by_uuid(podm_uuid)) + + def patch(self, podm_uuid): + values = flask.request.form.to_dict() + return utils.make_response(http_client.OK, + podmanagers.update_podm(podm_uuid, values)) + + def delete(self, podm_uuid): + podmanagers.delete_podm_by_uuid(podm_uuid) + resp_dict = exception.confirmation(confirm_detail="DELETED") + return utils.make_response(http_client.OK, resp_dict) diff --git a/valence/common/constants.py b/valence/common/constants.py new file mode 100644 index 0000000..1ad3d67 --- /dev/null +++ b/valence/common/constants.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016 Intel, Inc. +# +# 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. + +PODM_AUTH_BASIC_TYPE = 'basic' + +PODM_STATUS_ONLINE = 'Online' +PODM_STATUS_OFFLINE = 'Offline' +PODM_STATUS_UNKNOWN = "Unknown" diff --git a/valence/common/exception.py b/valence/common/exception.py index 2f586dc..0de3a6e 100644 --- a/valence/common/exception.py +++ b/valence/common/exception.py @@ -86,8 +86,24 @@ class RedfishException(ValenceError): self.detail = message_detail -class NotFound(Exception): - status = http_client.NOT_FOUND +class NotFound(ValenceError): + + def __init__(self, detail='resource not found', request_id=None): + self.request_id = request_id + self.status_code = http_client.NOT_FOUND + self.code = http_client.NOT_FOUND + self.title = "resource not found" + self.detail = detail + + +class BadRequest(ValenceError): + + def __init__(self, detail='bad request', request_id=None): + self.request_id = request_id + self.status_code = http_client.BAD_REQUEST + self.code = http_client.BAD_REQUEST + self.title = "bad request" + self.detail = detail def _error(error_code, http_status, error_title, error_detail, diff --git a/valence/controller/podmanagers.py b/valence/controller/podmanagers.py new file mode 100644 index 0000000..5cd9697 --- /dev/null +++ b/valence/controller/podmanagers.py @@ -0,0 +1,118 @@ +# Copyright (c) 2016 Intel, Inc. +# +# 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. + +import logging + +from valence.common import constants +from valence.common import exception +from valence.db import api as db_api +from valence.redfish import redfish + +LOG = logging.getLogger(__name__) + + +def _check_creation(values): + """Checking args when creating a new pod manager + + authentication: should follow the format + name: can not be duplicated + url: can not be duplicated + + :values: The properties for this new pod manager. + :returns: improved values that could be inserted to db + """ + + if not ('name' in values and + 'url' in values and + 'authentication' in values): + raise exception.BadRequest(detail="Incomplete parameters") + # check authentication's format and content + try: + if not (values['authentication'][0]['type'] and + values['authentication'][0]['auth_items']): + LOG.error("invalid authentication when creating podmanager") + raise exception.BadRequest(detail="invalid " + "authentication properties") + except KeyError: + LOG.error("Incomplete parameters when creating podmanager") + raise exception.BadRequest(detail="invalid " + "authentication properties") + + pod_manager_list = get_podm_list() + names = [podm['name'] for podm in pod_manager_list] + urls = [podm['url'] for podm in pod_manager_list] + if values['name'] in names or values['url'] in urls: + raise exception.BadRequest('duplicated name or url !') + + # input status + values['status'] = get_podm_status(values['url'], values['authentication']) + + return values + + +def _check_updation(values): + """Checking args when updating a exist pod manager + + :values: The properties of pod manager to be updated + :returns: improved values that could be updated + """ + + # uuid, url can not be modified + if 'uuid' in values: + values.pop('uuid') + if 'url' in values: + values.pop('url') + return values + + +def get_podm_list(): + return map(lambda x: x.as_dict(), db_api.Connection.list_podmanager()) + + +def get_podm_by_uuid(uuid): + return db_api.Connection.get_podmanager_by_uuid(uuid).as_dict() + + +def create_podm(values): + values = _check_creation(values) + return db_api.Connection.create_podmanager(values).as_dict() + + +def update_podm(uuid, values): + values = _check_updation(values) + return db_api.Connection.update_podmanager(uuid, values).as_dict() + + +def delete_podm_by_uuid(uuid): + # TODO(hubian) this need to break the links between podm and its Nodes + return db_api.Connection.delete_podmanager(uuid) + + +def get_podm_status(url, authentication): + """get pod manager running status by its url and auth + + :param url: The url of pod manager. + :param authentication: array, The auth(s) info of pod manager. + + :returns: status of the pod manager + """ + for auth in authentication: + # TODO(Hubian) Only consider and support basic auth type here. + # After decided to support other auth type this would be improved. + if auth['type'] == constants.PODM_AUTH_BASIC_TYPE: + username = auth['auth_items']['username'] + password = auth['auth_items']['password'] + return redfish.pod_status(url, username, password) + + return constants.PODM_STATUS_UNKNOWN diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index 501204d..0b69eb4 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -18,8 +18,10 @@ import logging import os import requests +from requests import auth from six.moves import http_client +from valence.common import constants from valence.common import exception from valence.common import utils from valence import config as cfg @@ -106,6 +108,18 @@ def pods(): return json.dumps(pods) +def pod_status(pod_url, username, password): + try: + resp = requests.get(pod_url, + auth=auth.HTTPBasicAuth(username, password)) + if resp.status_code == http_client.OK: + return constants.PODM_STATUS_ONLINE + else: + return constants.PODM_STATUS_OFFLINE + except requests.RequestException: + return constants.PODM_STATUS_OFFLINE + + def urls2list(url): # This will extract the url values from @odata.id inside Members resp = send_request(url) @@ -268,7 +282,7 @@ def get_systembyid(systemid): def get_nodebyid(nodeid): node = nodes_list({"Id": nodeid}) if not node: - raise exception.NotFound() + raise exception.NotFound(detail='Node: %s not found' % nodeid) return node[0] diff --git a/valence/tests/unit/controller/test_podmanagers.py b/valence/tests/unit/controller/test_podmanagers.py new file mode 100644 index 0000000..db22464 --- /dev/null +++ b/valence/tests/unit/controller/test_podmanagers.py @@ -0,0 +1,165 @@ +# 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. + +import copy +import mock +import unittest + +from valence.common import constants +from valence.common.exception import BadRequest +from valence.controller import podmanagers + + +class TestPodManagers(unittest.TestCase): + + @mock.patch('valence.controller.podmanagers.get_podm_list') + @mock.patch('valence.controller.podmanagers.get_podm_status') + def test_check_creation(self, mock_get_podm_status, mock_get_podm_list): + mock_get_podm_list.return_value = [ + {"name": "test1", + "url": "https://10.0.0.1"}, + {"name": "test2", + "url": "https://10.0.0.2"} + ] + mock_get_podm_status.return_value = constants.PODM_STATUS_ONLINE + + values = {"name": "podm_name", + "url": "https://10.240.212.123", + "authentication": [ + { + "type": "basic", + "auth_items": { + "username": "xxxxxxx", + "password": "xxxxxxx" + } + } + ]} + + result_values = copy.deepcopy(values) + result_values['status'] = constants.PODM_STATUS_ONLINE + + self.assertEqual(podmanagers._check_creation(values), result_values) + mock_get_podm_status.assert_called_once_with(values['url'], + values['authentication']) + mock_get_podm_list.assert_called_once_with() + + def test_check_creation_incomplete_parameters(self): + incomplete_values = { + 'name': 'name', + 'url': 'url' + } + self.assertRaises(BadRequest, + podmanagers._check_creation, + incomplete_values) + + def test_check_creation_invalid_authentication(self): + invalid_authentication_values = { + "name": "podm_name", + "url": "https://10.0.0.2", + 'authentication': { + "username": "username", + "password": "password" + } + } + self.assertRaises(BadRequest, + podmanagers._check_creation, + invalid_authentication_values) + + @mock.patch('valence.controller.podmanagers.get_podm_list') + def test_check_creation_duplicate_Exception(self, mock_get_podm_list): + mock_get_podm_list.return_value = [ + {"name": "test1", + "url": "https://10.0.0.1", + 'authentication': "authentication"}, + {"name": "test2", + "url": "https://10.0.0.2", + 'authentication': "authentication" + } + ] + + name_duplicate_values = {"name": "test1", + "url": "https://10.240.212.123", + 'authentication': [ + { + "type": "basic", + "auth_items": { + "username": "username", + "password": "password" + } + } + ]} + url_duplicate_values = {"name": "podm_name", + "url": "https://10.0.0.2", + 'authentication': [ + { + "type": "basic", + "auth_items": { + "username": "username", + "password": "password" + } + } + ]} + + self.assertRaises(BadRequest, + podmanagers._check_creation, + name_duplicate_values) + self.assertRaises(BadRequest, + podmanagers._check_creation, + url_duplicate_values) + self.assertEqual(mock_get_podm_list.call_count, 2) + + def test_check_updation_ignore_url_uuid(self): + values = { + "uuid": "uuid", + "url": "url", + "name": "name" + } + result_values = copy.deepcopy(values) + result_values.pop('url') + result_values.pop('uuid') + + self.assertEqual(podmanagers._check_updation(values), result_values) + + @mock.patch('valence.redfish.redfish.pod_status') + def test_get_podm_status(self, mock_pod_status): + mock_pod_status.return_value = constants.PODM_STATUS_ONLINE + authentication = [ + { + "type": "basic", + "auth_items": { + "username": "username", + "password": "password" + } + } + ] + self.assertEqual(podmanagers.get_podm_status('url', authentication), + constants.PODM_STATUS_ONLINE) + mock_pod_status.asset_called_once_with('url', "username", "password") + + def test_get_podm_status_unknown(self): + """not basic type authentication podm status set value to be unknown""" + authentication = [ + { + "type": "CertificateAuthority", + "auth_items": { + "public_key": "xxxxxxx" + } + }, + { + "type": "DynamicCode", + "auth_items": { + "code": "xxxxxxx" + } + } + ] + self.assertEqual(podmanagers.get_podm_status('url', authentication), + constants.PODM_STATUS_UNKNOWN) diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index 5cc48ea..8da5ac2 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -13,9 +13,12 @@ from unittest import TestCase import mock +import requests +from requests import auth from requests.compat import urljoin from six.moves import http_client +from valence.common import constants from valence.common import exception from valence import config as cfg from valence.redfish import redfish @@ -193,9 +196,9 @@ class TestRedfish(TestCase): mock_make_response): mock_get_url.return_value = '/redfish/v1/Nodes' delete_result = fakes.fake_delete_composednode_ok() - fake_delete_resopnse = fakes.mock_request_get(delete_result, + fake_delete_response = fakes.mock_request_get(delete_result, http_client.NO_CONTENT) - mock_request.return_value = fake_delete_resopnse + mock_request.return_value = fake_delete_response redfish.delete_composednode(101) mock_request.assert_called_with('/redfish/v1/Nodes/101', 'DELETE') expected_content = { @@ -218,3 +221,42 @@ class TestRedfish(TestCase): self.assertRaises(exception.RedfishException, redfish.delete_composednode, 101) self.assertFalse(mock_make_response.called) + + @mock.patch('requests.get') + def test_get_podm_status_Offline_by_wrong_auth(self, mock_get): + fake_resp = fakes.mock_request_get({}, 401) + mock_get.return_value = fake_resp + self.assertEqual(redfish.pod_status('url', 'username', 'password'), + constants.PODM_STATUS_OFFLINE) + mock_get.asset_called_once_with('url', + auth=auth.HTTPBasicAuth('username', + 'password')) + + @mock.patch('requests.get') + def test_get_podm_status_Offline_by_http_exception(self, mock_get): + mock_get.side_effect = requests.ConnectionError + self.assertEqual(redfish.pod_status('url', 'username', 'password'), + constants.PODM_STATUS_OFFLINE) + mock_get.asset_called_once_with('url', + auth=auth.HTTPBasicAuth('username', + 'password')) + # SSL Error + mock_get.side_effect = requests.exceptions.SSLError + self.assertEqual(redfish.pod_status('url', 'username', 'password'), + constants.PODM_STATUS_OFFLINE) + self.assertEqual(mock_get.call_count, 2) + # Timeout + mock_get.side_effect = requests.Timeout + self.assertEqual(redfish.pod_status('url', 'username', 'password'), + constants.PODM_STATUS_OFFLINE) + self.assertEqual(mock_get.call_count, 3) + + @mock.patch('requests.get') + def test_get_podm_status_Online(self, mock_get): + fake_resp = fakes.mock_request_get({}, http_client.OK) + mock_get.return_value = fake_resp + self.assertEqual(redfish.pod_status('url', 'username', 'password'), + constants.PODM_STATUS_ONLINE) + mock_get.asset_called_once_with('url', + auth=auth.HTTPBasicAuth('username', + 'password'))