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.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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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
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
|
# 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
|
||||||
|
@ -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)
|
||||||
|
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.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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
"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
|
||||||
|
@ -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)
|
||||||
|
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)
|
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"})
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
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