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
This commit is contained in:
Madhuri Kumari 2017-03-21 17:38:38 +00:00
parent a75dc523c8
commit e2cfdbac2e
26 changed files with 578 additions and 4 deletions

View File

@ -17,3 +17,6 @@ python-etcd>=0.4.3 # MIT License
oslo.utils>=3.20.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0
oslo.config>=3.22.0 # Apache-2.0 oslo.config>=3.22.0 # Apache-2.0
oslo.i18n>=2.1.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

View File

@ -62,3 +62,6 @@ console_scripts =
oslo.config.opts = oslo.config.opts =
valence = valence.opts:list_opts valence = valence.opts:list_opts
valence.conf = valence.conf.opts:list_opts valence.conf = valence.conf.opts:list_opts
valence.provision.driver =
ironic = valence.provision.ironic.driver:IronicDriver

View File

@ -83,6 +83,9 @@ api.add_resource(v1_nodes.NodeManage, '/v1/nodes/manage',
api.add_resource(v1_nodes.NodesStorage, api.add_resource(v1_nodes.NodesStorage,
'/v1/nodes/<string:nodeid>/storages', '/v1/nodes/<string:nodeid>/storages',
endpoint='nodes_storages') endpoint='nodes_storages')
api.add_resource(v1_nodes.NodeRegister,
'/v1/nodes/<string:node_uuid>/register',
endpoint='node_register')
# System(s) operations # System(s) operations
api.add_resource(v1_systems.SystemsList, '/v1/systems', endpoint='systems') api.add_resource(v1_systems.SystemsList, '/v1/systems', endpoint='systems')

View File

@ -67,3 +67,10 @@ class NodesStorage(Resource):
def get(self, nodeid): def get(self, nodeid):
return abort(http_client.NOT_IMPLEMENTED) 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()))

56
valence/common/clients.py Normal file
View File

@ -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

View File

@ -12,6 +12,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import functools
import sys
from keystoneclient import exceptions as keystone_exceptions
from six.moves import http_client from six.moves import http_client
from valence.common import base 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): class RedfishException(ValenceError):
def __init__(self, responsejson, request_id=FAKE_REQUEST_ID, def __init__(self, responsejson, request_id=FAKE_REQUEST_ID,
@ -123,6 +137,13 @@ class ValidationError(BadRequest):
code='ValidationError') 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, def _error(error_code, http_status, error_title, error_detail,
request_id=FAKE_REQUEST_ID): request_id=FAKE_REQUEST_ID):
# responseobj - the response object of Requests framework # 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.code = confirm_code
confirm_obj.detail = confirm_detail confirm_obj.detail = confirm_detail
return confirm_obj.as_dict() 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

View File

@ -16,10 +16,12 @@ from oslo_config import cfg
from valence.conf import api from valence.conf import api
from valence.conf import etcd from valence.conf import etcd
from valence.conf import ironic_client
from valence.conf import podm from valence.conf import podm
CONF = cfg.CONF CONF = cfg.CONF
api.register_opts(CONF) api.register_opts(CONF)
etcd.register_opts(CONF) etcd.register_opts(CONF)
ironic_client.register_opts(CONF)
podm.register_opts(CONF) podm.register_opts(CONF)

View File

@ -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}

View File

@ -20,6 +20,7 @@ from valence.common import exception
from valence.common import utils from valence.common import utils
from valence.controller import flavors from valence.controller import flavors
from valence.db import api as db_api from valence.db import api as db_api
from valence.provision import driver
from valence.redfish import redfish from valence.redfish import redfish
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -194,3 +195,14 @@ class Node(object):
# Get node detail from db, and map node uuid to index # Get node detail from db, and map node uuid to index
index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index
return redfish.node_action(index, request_body) 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

View File

@ -20,6 +20,7 @@ import etcd
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six import six
from valence.common import exception
from valence.common import singleton from valence.common import singleton
import valence.conf import valence.conf
from valence.db import models from valence.db import models
@ -167,7 +168,7 @@ class EtcdDriver(object):
except etcd.EtcdKeyNotFound: except etcd.EtcdKeyNotFound:
# TODO(lin.a.yang): after exception module got merged, raise # TODO(lin.a.yang): after exception module got merged, raise
# valence specific DBNotFound exception here # valence specific DBNotFound exception here
raise Exception( raise exception.NotFound(
'Composed node not found {0} in database.'.format( 'Composed node not found {0} in database.'.format(
composed_node_uuid)) composed_node_uuid))

View File

@ -207,5 +207,8 @@ class ComposedNode(ModelBaseWithTimeStamp):
}, },
'links': { 'links': {
'validate': types.List(types.Dict).validate 'validate': types.List(types.Dict).validate
},
'managed_by': {
'validate': types.Text.validate
} }
} }

View File

View File

@ -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()

View File

View File

@ -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)

View File

@ -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()

View File

@ -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 "network": [show_network_details(i.get("@odata.id")) for i in
respdata.get("Links", {}).get( respdata.get("Links", {}).get(
"EthernetInterfaces", [])] "EthernetInterfaces", [])]
} },
"computer_system": respdata.get("Links").get("ComputerSystem")
}) })
return node_detail return node_detail

View File

@ -42,6 +42,7 @@ class TestRoute(unittest.TestCase):
self.assertEqual(self.api.owns_endpoint('nodes'), True) self.assertEqual(self.api.owns_endpoint('nodes'), True)
self.assertEqual(self.api.owns_endpoint('node'), True) self.assertEqual(self.api.owns_endpoint('node'), True)
self.assertEqual(self.api.owns_endpoint('nodes_storages'), 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('systems'), True)
self.assertEqual(self.api.owns_endpoint('system'), True) self.assertEqual(self.api.owns_endpoint('system'), True)
self.assertEqual(self.api.owns_endpoint('flavors'), True) self.assertEqual(self.api.owns_endpoint('flavors'), True)

View File

@ -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)

View File

@ -236,3 +236,8 @@ class TestAPINodes(unittest.TestCase):
nodes.Node.node_action("fake_uuid", action) nodes.Node.node_action("fake_uuid", action)
mock_node_action.assert_called_once_with("1", 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"})

View File

@ -18,6 +18,7 @@ import etcd
import freezegun import freezegun
import mock import mock
from valence.common import exception
from valence.db import api as db_api from valence.db import api as db_api
from valence.tests.unit.db import utils from valence.tests.unit.db import utils
@ -216,11 +217,11 @@ class TestDBAPI(unittest.TestCase):
node = utils.get_test_composed_node_db_info() node = utils.get_test_composed_node_db_info()
mock_etcd_read.side_effect = etcd.EtcdKeyNotFound 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']) db_api.Connection.get_composed_node_by_uuid(node['uuid'])
self.assertTrue('Composed node not found {0} in database.'.format( 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( mock_etcd_read.assert_called_once_with(
'/nodes/' + node['uuid']) '/nodes/' + node['uuid'])

View File

View File

@ -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)

View File

@ -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()

View File

@ -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()