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:
parent
a75dc523c8
commit
e2cfdbac2e
@ -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
|
||||
|
@ -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
|
||||
|
@ -83,6 +83,9 @@ api.add_resource(v1_nodes.NodeManage, '/v1/nodes/manage',
|
||||
api.add_resource(v1_nodes.NodesStorage,
|
||||
'/v1/nodes/<string:nodeid>/storages',
|
||||
endpoint='nodes_storages')
|
||||
api.add_resource(v1_nodes.NodeRegister,
|
||||
'/v1/nodes/<string:node_uuid>/register',
|
||||
endpoint='node_register')
|
||||
|
||||
# System(s) operations
|
||||
api.add_resource(v1_systems.SystemsList, '/v1/systems', endpoint='systems')
|
||||
|
@ -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()))
|
||||
|
56
valence/common/clients.py
Normal file
56
valence/common/clients.py
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
68
valence/conf/ironic_client.py
Normal file
68
valence/conf/ironic_client.py
Normal 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}
|
@ -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
|
||||
|
@ -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))
|
||||
|
||||
|
@ -207,5 +207,8 @@ class ComposedNode(ModelBaseWithTimeStamp):
|
||||
},
|
||||
'links': {
|
||||
'validate': types.List(types.Dict).validate
|
||||
},
|
||||
'managed_by': {
|
||||
'validate': types.Text.validate
|
||||
}
|
||||
}
|
||||
|
0
valence/provision/__init__.py
Normal file
0
valence/provision/__init__.py
Normal file
69
valence/provision/driver.py
Normal file
69
valence/provision/driver.py
Normal 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()
|
0
valence/provision/ironic/__init__.py
Normal file
0
valence/provision/ironic/__init__.py
Normal file
80
valence/provision/ironic/driver.py
Normal file
80
valence/provision/ironic/driver.py
Normal 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)
|
25
valence/provision/ironic/utils.py
Normal file
25
valence/provision/ironic/utils.py
Normal 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()
|
@ -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
|
||||
|
@ -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)
|
||||
|
54
valence/tests/unit/common/test_clients.py
Normal file
54
valence/tests/unit/common/test_clients.py
Normal 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)
|
@ -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"})
|
||||
|
@ -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'])
|
||||
|
||||
|
0
valence/tests/unit/provision/__init__.py
Normal file
0
valence/tests/unit/provision/__init__.py
Normal file
0
valence/tests/unit/provision/ironic/__init__.py
Normal file
0
valence/tests/unit/provision/ironic/__init__.py
Normal file
72
valence/tests/unit/provision/ironic/test_driver.py
Normal file
72
valence/tests/unit/provision/ironic/test_driver.py
Normal 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)
|
29
valence/tests/unit/provision/ironic/test_utils.py
Normal file
29
valence/tests/unit/provision/ironic/test_utils.py
Normal 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()
|
40
valence/tests/unit/provision/test_driver.py
Normal file
40
valence/tests/unit/provision/test_driver.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user