From e2cfdbac2eabccb9ca16ec6b7043900b91a261f6 Mon Sep 17 00:00:00 2001 From: Madhuri Kumari Date: Tue, 21 Mar 2017 17:38:38 +0000 Subject: [PATCH] Add node register API: /v1/nodes/{ID}/register This API does following: * Creates a node in Ironic of driver type `redfish` and details like the redfish URL, username, password, system URL of node. * Creates a port for the above node in Ironic. * Updates the field `managed_by` to `ironic` in Valence db. Change-Id: Ia81a2eb6ecb2b48efc3a8c99183d12bbc1635702 --- requirements.txt | 3 + setup.cfg | 3 + valence/api/route.py | 3 + valence/api/v1/nodes.py | 7 ++ valence/common/clients.py | 56 +++++++++++++ valence/common/exception.py | 39 +++++++++ valence/conf/__init__.py | 2 + valence/conf/ironic_client.py | 68 ++++++++++++++++ valence/controller/nodes.py | 12 +++ valence/db/etcd_driver.py | 3 +- valence/db/models.py | 3 + valence/provision/__init__.py | 0 valence/provision/driver.py | 69 ++++++++++++++++ valence/provision/ironic/__init__.py | 0 valence/provision/ironic/driver.py | 80 +++++++++++++++++++ valence/provision/ironic/utils.py | 25 ++++++ valence/redfish/redfish.py | 3 +- valence/tests/unit/api/test_route.py | 1 + valence/tests/unit/common/test_clients.py | 54 +++++++++++++ valence/tests/unit/controller/test_nodes.py | 5 ++ valence/tests/unit/db/test_db_api.py | 5 +- valence/tests/unit/provision/__init__.py | 0 .../tests/unit/provision/ironic/__init__.py | 0 .../unit/provision/ironic/test_driver.py | 72 +++++++++++++++++ .../tests/unit/provision/ironic/test_utils.py | 29 +++++++ valence/tests/unit/provision/test_driver.py | 40 ++++++++++ 26 files changed, 578 insertions(+), 4 deletions(-) create mode 100644 valence/common/clients.py create mode 100644 valence/conf/ironic_client.py create mode 100644 valence/provision/__init__.py create mode 100644 valence/provision/driver.py create mode 100644 valence/provision/ironic/__init__.py create mode 100644 valence/provision/ironic/driver.py create mode 100644 valence/provision/ironic/utils.py create mode 100644 valence/tests/unit/common/test_clients.py create mode 100644 valence/tests/unit/provision/__init__.py create mode 100644 valence/tests/unit/provision/ironic/__init__.py create mode 100644 valence/tests/unit/provision/ironic/test_driver.py create mode 100644 valence/tests/unit/provision/ironic/test_utils.py create mode 100644 valence/tests/unit/provision/test_driver.py diff --git a/requirements.txt b/requirements.txt index c41ecf1..366f321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,6 @@ python-etcd>=0.4.3 # MIT License oslo.utils>=3.20.0 # Apache-2.0 oslo.config>=3.22.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 +python-ironicclient>=1.11.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 +stevedore>=1.20.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 2675a3d..85875d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,3 +62,6 @@ console_scripts = oslo.config.opts = valence = valence.opts:list_opts valence.conf = valence.conf.opts:list_opts + +valence.provision.driver = + ironic = valence.provision.ironic.driver:IronicDriver diff --git a/valence/api/route.py b/valence/api/route.py index b2b9aca..33c9aed 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -83,6 +83,9 @@ api.add_resource(v1_nodes.NodeManage, '/v1/nodes/manage', api.add_resource(v1_nodes.NodesStorage, '/v1/nodes//storages', endpoint='nodes_storages') +api.add_resource(v1_nodes.NodeRegister, + '/v1/nodes//register', + endpoint='node_register') # System(s) operations api.add_resource(v1_systems.SystemsList, '/v1/systems', endpoint='systems') diff --git a/valence/api/v1/nodes.py b/valence/api/v1/nodes.py index 8f0167f..b0a9475 100644 --- a/valence/api/v1/nodes.py +++ b/valence/api/v1/nodes.py @@ -67,3 +67,10 @@ class NodesStorage(Resource): def get(self, nodeid): return abort(http_client.NOT_IMPLEMENTED) + + +class NodeRegister(Resource): + + def post(self, node_uuid): + return utils.make_response(http_client.OK, nodes.Node.node_register( + node_uuid, request.get_json())) diff --git a/valence/common/clients.py b/valence/common/clients.py new file mode 100644 index 0000000..dd974c5 --- /dev/null +++ b/valence/common/clients.py @@ -0,0 +1,56 @@ +# Copyright 2017 Intel. +# +# 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. + +from ironicclient import client as ironicclient + +from valence.common import exception +import valence.conf + +CONF = valence.conf.CONF + + +class OpenStackClients(object): + """Convenience class to create and cache client instances.""" + + def __init__(self, context=None): + self.context = context + self._ironic = None + + def _get_client_option(self, client, option): + return getattr(getattr(valence.conf.CONF, '%s_client' % client), + option) + + @exception.wrap_keystone_exception + def ironic(self): + if self._ironic: + return self._ironic + + ironicclient_version = self._get_client_option('ironic', 'api_version') + args = { + 'os_auth_url': self._get_client_option('ironic', 'auth_url'), + 'os_username': self._get_client_option('ironic', 'username'), + 'os_password': self._get_client_option('ironic', 'password'), + 'os_project_name': self._get_client_option('ironic', 'project'), + 'os_project_domain_id': self._get_client_option( + 'ironic', 'project_domain_id'), + 'os_user_domain_id': self._get_client_option( + 'ironic', 'user_domain_id'), + 'os_cacert': self._get_client_option('ironic', 'os_cacert'), + 'os_cert': self._get_client_option('ironic', 'os_cert'), + 'os_key': self._get_client_option('ironic', 'os_key'), + 'insecure': self._get_client_option('ironic', 'insecure') + } + self._ironic = ironicclient.get_client(ironicclient_version, **args) + + return self._ironic diff --git a/valence/common/exception.py b/valence/common/exception.py index 148a4ed..52f1569 100644 --- a/valence/common/exception.py +++ b/valence/common/exception.py @@ -12,6 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import functools +import sys + +from keystoneclient import exceptions as keystone_exceptions from six.moves import http_client from valence.common import base @@ -70,6 +74,16 @@ class ValenceConfirmation(base.ObjectBase): } +class ValenceException(ValenceError): + def __init__(self, detail, status=None, + request_id=FAKE_REQUEST_ID): + self.request_id = request_id + self.status = status or http_client.SERVICE_UNAVAILABLE + self.code = "ValenceError" + self.title = http_client.responses.get(self.status) + self.detail = detail + + class RedfishException(ValenceError): def __init__(self, responsejson, request_id=FAKE_REQUEST_ID, @@ -123,6 +137,13 @@ class ValidationError(BadRequest): code='ValidationError') +class AuthorizationFailure(ValenceError): + def __init__(self, detail, request_id=None): + message = "Keystone authorization error. %s" % detail + super(AuthorizationFailure, self).__init__(detail=message, + code='AuthorizationFailure') + + def _error(error_code, http_status, error_title, error_detail, request_id=FAKE_REQUEST_ID): # responseobj - the response object of Requests framework @@ -151,3 +172,21 @@ def confirmation(request_id=FAKE_REQUEST_ID, confirm_code='', confirm_obj.code = confirm_code confirm_obj.detail = confirm_detail return confirm_obj.as_dict() + + +def wrap_keystone_exception(func): + """Wrap keystone exceptions and throw Valence specific exceptions.""" + @functools.wraps(func) + def wrapped(*args, **kw): + try: + return func(*args, **kw) + except keystone_exceptions.AuthorizationFailure: + message = ("%s connection failed. Reason: " + "%s" % (func.__name__, sys.exc_info()[1])) + raise AuthorizationFailure(detail=message) + except keystone_exceptions.ClientException: + message = ("%s connection failed. Unexpected keystone client " + "error occurred: %s" % (func.__name__, + sys.exc_info()[1])) + raise AuthorizationFailure(detail=message) + return wrapped diff --git a/valence/conf/__init__.py b/valence/conf/__init__.py index 1a850be..73b219f 100644 --- a/valence/conf/__init__.py +++ b/valence/conf/__init__.py @@ -16,10 +16,12 @@ from oslo_config import cfg from valence.conf import api from valence.conf import etcd +from valence.conf import ironic_client from valence.conf import podm CONF = cfg.CONF api.register_opts(CONF) etcd.register_opts(CONF) +ironic_client.register_opts(CONF) podm.register_opts(CONF) diff --git a/valence/conf/ironic_client.py b/valence/conf/ironic_client.py new file mode 100644 index 0000000..b1a03c6 --- /dev/null +++ b/valence/conf/ironic_client.py @@ -0,0 +1,68 @@ +# Copyright 2017 Intel. +# +# 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. + +from oslo_config import cfg + +from valence.common.i18n import _ + + +ironic_group = cfg.OptGroup(name='ironic_client', + title='Options for the Ironic client') + +common_security_opts = [ + cfg.StrOpt('os_cacert', + help=_('Optional CA cert file to use in SSL connections.')), + cfg.StrOpt('os_cert', + help=_('Optional PEM-formatted certificate chain file.')), + cfg.StrOpt('os_key', + help=_('Optional PEM-formatted file that contains the ' + 'private key.')), + cfg.BoolOpt('insecure', + default=False, + help=_("If set, then the server's certificate will not " + "be verified."))] + +ironic_client_opts = [ + cfg.StrOpt('username', + help=_('The name of user to interact with Ironic API ' + 'service.')), + cfg.StrOpt('password', + help=_('Password of the user specified to authorize to ' + 'communicate with the Ironic API service.')), + cfg.StrOpt('project', + help=_('The project name which the user belongs to.')), + cfg.StrOpt('auth_url', + help=_('The OpenStack Identity Service endpoint to authorize ' + 'the user against.')), + cfg.StrOpt('user_domain_id', + help=_( + 'ID of a domain the user belongs to.')), + cfg.StrOpt('project_domain_id', + help=_( + 'ID of a domain the project belongs to.')), + cfg.StrOpt('api_version', + default='1', + help=_('Version of Ironic API to use in ironicclient.'))] + + +ALL_OPTS = (ironic_client_opts + common_security_opts) + + +def register_opts(conf): + conf.register_group(ironic_group) + conf.register_opts(ALL_OPTS, group=ironic_group) + + +def list_opts(): + return {ironic_group: ALL_OPTS} diff --git a/valence/controller/nodes.py b/valence/controller/nodes.py index eac333d..aa35275 100644 --- a/valence/controller/nodes.py +++ b/valence/controller/nodes.py @@ -20,6 +20,7 @@ from valence.common import exception from valence.common import utils from valence.controller import flavors from valence.db import api as db_api +from valence.provision import driver from valence.redfish import redfish LOG = logging.getLogger(__name__) @@ -194,3 +195,14 @@ class Node(object): # Get node detail from db, and map node uuid to index index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index return redfish.node_action(index, request_body) + + @classmethod + def node_register(cls, node_uuid, request_body): + """Register a node to provisioning services. + + :param node_uuid: UUID of composed node to register + :param request_body: parameter of register node with + :returns: response from provisioning services + """ + resp = driver.node_register(node_uuid, request_body) + return resp diff --git a/valence/db/etcd_driver.py b/valence/db/etcd_driver.py index 9f0f5e4..c5aa462 100644 --- a/valence/db/etcd_driver.py +++ b/valence/db/etcd_driver.py @@ -20,6 +20,7 @@ import etcd from oslo_utils import uuidutils import six +from valence.common import exception from valence.common import singleton import valence.conf from valence.db import models @@ -167,7 +168,7 @@ class EtcdDriver(object): except etcd.EtcdKeyNotFound: # TODO(lin.a.yang): after exception module got merged, raise # valence specific DBNotFound exception here - raise Exception( + raise exception.NotFound( 'Composed node not found {0} in database.'.format( composed_node_uuid)) diff --git a/valence/db/models.py b/valence/db/models.py index 4d4f40a..77f7d7a 100644 --- a/valence/db/models.py +++ b/valence/db/models.py @@ -207,5 +207,8 @@ class ComposedNode(ModelBaseWithTimeStamp): }, 'links': { 'validate': types.List(types.Dict).validate + }, + 'managed_by': { + 'validate': types.Text.validate } } diff --git a/valence/provision/__init__.py b/valence/provision/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/provision/driver.py b/valence/provision/driver.py new file mode 100644 index 0000000..ca5ac36 --- /dev/null +++ b/valence/provision/driver.py @@ -0,0 +1,69 @@ +# Copyright 2017 Intel. +# +# 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 abc +import logging + +import stevedore + +from valence.common import exception + +LOG = logging.getLogger(__name__) + + +def load_driver(driver='ironic'): + """Load an provisioning driver module. + + Load the provisioning driver module specified by the driver + configuration option or, if supplied, the driver name supplied as an + argument. + :param driver: provisioning driver name to override config opt + :returns: a ProvisioningDriver instance + """ + LOG.info("Loading provisioning driver '%s'" % driver) + try: + driver = stevedore.driver.DriverManager( + "valence.provision.driver", + driver, + invoke_on_load=True).driver + + if not isinstance(driver, ProvisioningDriver): + raise Exception('Expected driver of type: %s' % + str(ProvisioningDriver)) + + return driver + except Exception: + LOG.exception("Unable to load the provisioning driver") + raise exception.ValenceException("Failed to load %s driver" % driver) + + +def node_register(node, param): + driver = load_driver() + return driver.node_register(node, param) + + +class ProvisioningDriver(object): + '''Base class for provisioning driver. + + ''' + + @abc.abstractmethod + def register(self, node_uuid, param=None): + """Register a node.""" + raise NotImplementedError() + + @abc.abstractmethod + def deregister(self, node_uuid): + """Unregister a node.""" + raise NotImplementedError() diff --git a/valence/provision/ironic/__init__.py b/valence/provision/ironic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/provision/ironic/driver.py b/valence/provision/ironic/driver.py new file mode 100644 index 0000000..1e09edb --- /dev/null +++ b/valence/provision/ironic/driver.py @@ -0,0 +1,80 @@ +# Copyright 2017 Intel. +# +# 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 six + +from valence.common import exception +import valence.conf +from valence.controller import nodes +from valence.db import api as db_api +from valence.provision import driver +from valence.provision.ironic import utils + +CONF = valence.conf.CONF + +LOG = logging.getLogger(__name__) + + +class IronicDriver(driver.ProvisioningDriver): + + def __init__(self): + super(IronicDriver, self).__init__() + + def node_register(self, node_uuid, param): + LOG.debug('Registering node %s with ironic' % node_uuid) + node_info = nodes.Node.get_composed_node_by_uuid(node_uuid) + try: + ironic = utils.create_ironicclient() + except Exception as e: + message = ('Error occurred while communicating to ' + 'Ironic: %s' % six.text_type(e)) + LOG.error(message) + raise exception.ValenceException(message) + try: + # NOTE(mkrai): Below implementation will be changed in future to + # support the multiple pod manager in which we access pod managers' + # detail from podm object associated with a node. + driver_info = { + 'redfish_address': CONF.podm.url, + 'redfish_username': CONF.podm.username, + 'redfish_password': CONF.podm.password, + 'redfish_verify_ca': CONF.podm.verify_ca, + 'redfish_system_id': node_info['computer_system']} + node_args = {} + if param: + if param.get('driver_info', None): + driver_info.update(param.get('driver_info')) + del param['driver_info'] + node_args.update({'driver': 'redfish', 'name': node_info['name'], + 'driver_info': driver_info}) + if param: + node_args.update(param) + ironic_node = ironic.node.create(**node_args) + port_args = {'node_uuid': ironic_node.uuid, + 'address': node_info['metadata']['network'][0]['mac']} + ironic.port.create(**port_args) + db_api.Connection.update_composed_node(node_uuid, + {'managed_by': 'ironic'}) + return exception.confirmation( + confirm_code="Node Registered", + confirm_detail="The composed node {0} has been registered " + "with Ironic successfully.".format(node_uuid)) + except Exception as e: + message = ('Unexpected error while registering node with ' + 'Ironic: %s' % six.text_type(e)) + LOG.error(message) + raise exception.ValenceException(message) diff --git a/valence/provision/ironic/utils.py b/valence/provision/ironic/utils.py new file mode 100644 index 0000000..ce118bf --- /dev/null +++ b/valence/provision/ironic/utils.py @@ -0,0 +1,25 @@ +# Copyright 2017 Intel. +# +# 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. + +from valence.common import clients + + +def create_ironicclient(): + """Creates ironic client object. + + :returns: Ironic client object + """ + osc = clients.OpenStackClients() + return osc.ironic() diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index 383c227..ea6c3d4 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -456,7 +456,8 @@ def get_node_by_id(node_index, show_detail=True): "network": [show_network_details(i.get("@odata.id")) for i in respdata.get("Links", {}).get( "EthernetInterfaces", [])] - } + }, + "computer_system": respdata.get("Links").get("ComputerSystem") }) return node_detail diff --git a/valence/tests/unit/api/test_route.py b/valence/tests/unit/api/test_route.py index a7014d0..48fa701 100644 --- a/valence/tests/unit/api/test_route.py +++ b/valence/tests/unit/api/test_route.py @@ -42,6 +42,7 @@ class TestRoute(unittest.TestCase): self.assertEqual(self.api.owns_endpoint('nodes'), True) self.assertEqual(self.api.owns_endpoint('node'), True) self.assertEqual(self.api.owns_endpoint('nodes_storages'), True) + self.assertEqual(self.api.owns_endpoint('node_register'), True) self.assertEqual(self.api.owns_endpoint('systems'), True) self.assertEqual(self.api.owns_endpoint('system'), True) self.assertEqual(self.api.owns_endpoint('flavors'), True) diff --git a/valence/tests/unit/common/test_clients.py b/valence/tests/unit/common/test_clients.py new file mode 100644 index 0000000..4c1f4ef --- /dev/null +++ b/valence/tests/unit/common/test_clients.py @@ -0,0 +1,54 @@ +# 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 unittest + +import mock + +from ironicclient import client as ironicclient + +from valence.common import clients +import valence.conf + + +class ClientsTest(unittest.TestCase): + + def setUp(self): + super(ClientsTest, self).setUp() + + valence.conf.CONF.set_override('auth_url', + 'http://server.test:5000/v2.0', + group='ironic_client') + valence.conf.CONF.set_override('api_version', 1, + group='ironic_client') + + @mock.patch.object(ironicclient, 'get_client') + def test_clients_ironic(self, mock_client): + obj = clients.OpenStackClients() + obj._ironic = None + obj.ironic() + mock_client.assert_called_once_with( + valence.conf.CONF.ironic_client.api_version, + os_auth_url='http://server.test:5000/v2.0', os_username=None, + os_project_name=None, + os_project_domain_id=None, + os_user_domain_id=None, + os_password=None, os_cacert=None, os_cert=None, + os_key=None, insecure=False) + + @mock.patch.object(ironicclient, 'get_client') + def test_clients_ironic_cached(self, mock_client): + obj = clients.OpenStackClients() + obj._ironic = None + ironic = obj.ironic() + ironic_cached = obj.ironic() + self.assertEqual(ironic, ironic_cached) diff --git a/valence/tests/unit/controller/test_nodes.py b/valence/tests/unit/controller/test_nodes.py index 62ccbae..7720679 100644 --- a/valence/tests/unit/controller/test_nodes.py +++ b/valence/tests/unit/controller/test_nodes.py @@ -236,3 +236,8 @@ class TestAPINodes(unittest.TestCase): nodes.Node.node_action("fake_uuid", action) mock_node_action.assert_called_once_with("1", action) + + @mock.patch("valence.provision.driver.node_register") + def test_node_register(self, mock_node_register): + nodes.Node.node_register("fake_uuid", {"foo": "bar"}) + mock_node_register.assert_called_once_with("fake_uuid", {"foo": "bar"}) diff --git a/valence/tests/unit/db/test_db_api.py b/valence/tests/unit/db/test_db_api.py index d562fc4..53147b3 100644 --- a/valence/tests/unit/db/test_db_api.py +++ b/valence/tests/unit/db/test_db_api.py @@ -18,6 +18,7 @@ import etcd import freezegun import mock +from valence.common import exception from valence.db import api as db_api from valence.tests.unit.db import utils @@ -216,11 +217,11 @@ class TestDBAPI(unittest.TestCase): node = utils.get_test_composed_node_db_info() mock_etcd_read.side_effect = etcd.EtcdKeyNotFound - with self.assertRaises(Exception) as context: # noqa: H202 + with self.assertRaises(exception.NotFound) as context: # noqa: H202 db_api.Connection.get_composed_node_by_uuid(node['uuid']) self.assertTrue('Composed node not found {0} in database.'.format( - node['uuid']) in str(context.exception)) + node['uuid']) in str(context.exception.detail)) mock_etcd_read.assert_called_once_with( '/nodes/' + node['uuid']) diff --git a/valence/tests/unit/provision/__init__.py b/valence/tests/unit/provision/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/tests/unit/provision/ironic/__init__.py b/valence/tests/unit/provision/ironic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/tests/unit/provision/ironic/test_driver.py b/valence/tests/unit/provision/ironic/test_driver.py new file mode 100644 index 0000000..f4254e9 --- /dev/null +++ b/valence/tests/unit/provision/ironic/test_driver.py @@ -0,0 +1,72 @@ +# Copyright 2016 Intel. +# +# 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 mock + +from oslotest import base + +from valence.common import exception +from valence.provision.ironic import driver + + +class TestDriver(base.BaseTestCase): + def setUp(self): + super(TestDriver, self).setUp() + self.ironic = driver.IronicDriver() + + def tearDown(self): + super(TestDriver, self).tearDown() + + @mock.patch("valence.controller.nodes.Node.get_composed_node_by_uuid") + def test_node_register_node_not_found(self, mock_db): + mock_db.side_effect = exception.NotFound + self.assertRaises(exception.NotFound, + self.ironic.node_register, + 'fake-uuid', {}) + + @mock.patch("valence.controller.nodes.Node.get_composed_node_by_uuid") + @mock.patch("valence.provision.ironic.utils.create_ironicclient") + def test_node_register_ironic_client_failure(self, mock_client, + mock_db): + mock_client.side_effect = Exception() + self.assertRaises(exception.ValenceException, + self.ironic.node_register, + 'fake-uuid', {}) + + @mock.patch("valence.db.api.Connection.update_composed_node") + @mock.patch("valence.controller.nodes.Node.get_composed_node_by_uuid") + @mock.patch("valence.provision.ironic.utils.create_ironicclient") + def test_node_register(self, mock_client, + mock_node_get, mock_node_update): + ironic = mock.MagicMock() + mock_client.return_value = ironic + mock_node_get.return_value = { + 'name': 'test', 'metadata': + {'network': [{'mac': 'fake-mac'}]}, + 'computer_system': '/redfish/v1/Systems/437XR1138R2'} + ironic.node.create.return_value = mock.MagicMock(uuid='ironic-uuid') + port_arg = {'node_uuid': 'ironic-uuid', 'address': 'fake-mac'} + resp = self.ironic.node_register('fake-uuid', + {"extra": {"foo": "bar"}}) + self.assertEqual({ + 'code': 'Node Registered', + 'detail': 'The composed node fake-uuid has been ' + 'registered with Ironic successfully.', + 'request_id': '00000000-0000-0000-0000-000000000000'}, resp) + mock_client.assert_called_once() + mock_node_get.assert_called_once_with('fake-uuid') + mock_node_update.assert_called_once_with('fake-uuid', + {'managed_by': 'ironic'}) + ironic.node.create.assert_called_once() + ironic.port.create.assert_called_once_with(**port_arg) diff --git a/valence/tests/unit/provision/ironic/test_utils.py b/valence/tests/unit/provision/ironic/test_utils.py new file mode 100644 index 0000000..207a7b2 --- /dev/null +++ b/valence/tests/unit/provision/ironic/test_utils.py @@ -0,0 +1,29 @@ +# copyright (c) 2017 Intel, Inc. +# +# 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 mock + +from oslotest import base + +from valence.provision.ironic import utils + + +class TestUtils(base.BaseTestCase): + def setUp(self): + super(TestUtils, self).setUp() + + @mock.patch('valence.common.clients.OpenStackClients.ironic') + def test_create_ironicclient(self, mock_ironic): + ironic = utils.create_ironicclient() + self.assertTrue(ironic) + mock_ironic.assert_called_once_with() diff --git a/valence/tests/unit/provision/test_driver.py b/valence/tests/unit/provision/test_driver.py new file mode 100644 index 0000000..636dbdc --- /dev/null +++ b/valence/tests/unit/provision/test_driver.py @@ -0,0 +1,40 @@ +# Copyright 2017 Intel. +# +# 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 mock + +from oslotest import base + +from valence.common import exception +import valence.conf +from valence.provision import driver + +CONF = valence.conf.CONF + + +class TestDriver(base.BaseTestCase): + def setUp(self): + super(TestDriver, self).setUp() + + def test_load_driver_failure(self): + self.assertRaises(exception.ValenceException, driver.load_driver, + 'UnknownDriver') + + def test_load_driver(self): + self.assertTrue(driver.load_driver, 'ironic.IronicDriver') + + @mock.patch("valence.provision.driver.load_driver") + def test_node_register(self, mock_driver): + driver.node_register('fake-uuid', {}) + mock_driver.assert_called_once()