Modify Ironic driver to support multi-podm arch

This commit adds support for Ironic driver to work in
multi-podm architecture

Change-Id: I09e2be5eb3a2651d6f960ac5610fca89335e5b5b
This commit is contained in:
Anusha Ramineni 2017-08-30 11:00:49 +05:30 committed by akhiljain23
parent 0104b783ba
commit ecd965149b
9 changed files with 116 additions and 88 deletions

View File

@ -45,12 +45,12 @@ class Node(Resource):
def get(self, node_uuid): def get(self, node_uuid):
return utils.make_response( return utils.make_response(
http_client.OK, http_client.OK,
nodes.Node(node_id=node_uuid).get_composed_node_by_uuid(node_uuid)) nodes.Node(node_id=node_uuid).get_composed_node_by_uuid())
def delete(self, node_uuid): def delete(self, node_uuid):
return utils.make_response( return utils.make_response(
http_client.OK, http_client.OK,
nodes.Node(node_id=node_uuid).delete_composed_node(node_uuid)) nodes.Node(node_id=node_uuid).delete_composed_node())
class NodeAction(Resource): class NodeAction(Resource):
@ -59,8 +59,7 @@ class NodeAction(Resource):
def post(self, node_uuid): def post(self, node_uuid):
return utils.make_response( return utils.make_response(
http_client.NO_CONTENT, http_client.NO_CONTENT,
nodes.Node(node_id=node_uuid).node_action(node_uuid, nodes.Node(node_id=node_uuid).node_action(request.get_json()))
request.get_json()))
class NodeManage(Resource): class NodeManage(Resource):

View File

@ -151,7 +151,7 @@ class Node(object):
return self._show_node_brief_info(composed_node) return self._show_node_brief_info(composed_node)
def get_composed_node_by_uuid(self, node_uuid): def get_composed_node_by_uuid(self):
"""Get composed node details """Get composed node details
Get the detail of specific composed node. In some cases db data may be Get the detail of specific composed node. In some cases db data may be
@ -159,7 +159,6 @@ class Node(object):
through valence api. So compare it with node info from redfish, and through valence api. So compare it with node info from redfish, and
update db if it's inconsistent. update db if it's inconsistent.
param node_uuid: uuid of composed node
return: detail of this composed node return: detail of this composed node
""" """
@ -169,15 +168,14 @@ class Node(object):
node_hw.update(self.node) node_hw.update(self.node)
return node_hw return node_hw
def delete_composed_node(self, node_uuid): def delete_composed_node(self):
"""Delete a composed node """Delete a composed node
param node_uuid: uuid of composed node
return: message of this deletion return: message of this deletion
""" """
# Call podmanager to delete node, and delete corresponding entry in db # Call podmanager to delete node, and delete corresponding entry in db
message = self.connection.delete_composed_node(self.node['index']) message = self.connection.delete_composed_node(self.node['index'])
db_api.Connection.delete_composed_node(node_uuid) db_api.Connection.delete_composed_node(self.node['uuid'])
return message return message
@ -190,7 +188,7 @@ class Node(object):
return [cls._show_node_brief_info(node.as_dict()) return [cls._show_node_brief_info(node.as_dict())
for node in db_api.Connection.list_composed_nodes(filters)] for node in db_api.Connection.list_composed_nodes(filters)]
def node_action(self, node_uuid, request_body): def node_action(self, request_body):
"""Post action to a composed node """Post action to a composed node
param node_uuid: uuid of composed node param node_uuid: uuid of composed node

View File

@ -17,13 +17,15 @@ from valence.redfish.sushy import sushy_instance
class PodManagerBase(object): class PodManagerBase(object):
def __init__(self, username, password, podm_url): def __init__(self, username, password, podm_url, **kwargs):
self.podm_url = podm_url self.podm_url = podm_url
self.username = username
self.password = password
self.driver = sushy_instance.RedfishInstance(username=username, self.driver = sushy_instance.RedfishInstance(username=username,
password=password, password=password,
base_url=podm_url) base_url=podm_url)
# TODO(ramineni): rebase on nate's patch # TODO(): use rsd_lib here
def get_status(self): def get_status(self):
pass pass
@ -62,6 +64,28 @@ class PodManagerBase(object):
def get_all_devices(self): def get_all_devices(self):
pass pass
def get_ironic_node_params(self, node_info, **param):
# TODO(): change to 'rsd' once ironic driver is implemented.
driver_info = {
'redfish_address': self.podm_url,
'redfish_username': self.username,
'redfish_password': self.password,
'redfish_system_id': node_info['computer_system']
}
node_args = {}
if param and param.get('driver_info', None):
driver_info.update(param.pop('driver_info'))
node_args.update({'driver': 'redfish', 'name': node_info['name'],
'driver_info': driver_info})
port_args = {'address': node_info['metadata']['network'][0]['mac']}
# update any remaining params passed
if param:
node_args.update(param)
return node_args, port_args
def get_resource_info_by_url(self, resource_url): def get_resource_info_by_url(self, resource_url):
return self.driver.get_resources_by_url(resource_url) return self.driver.get_resources_by_url(resource_url)

View File

@ -33,48 +33,37 @@ class IronicDriver(driver.ProvisioningDriver):
def __init__(self): def __init__(self):
super(IronicDriver, self).__init__() super(IronicDriver, self).__init__()
self.ironic = utils.create_ironicclient()
def node_register(self, node_uuid, param): def node_register(self, node_uuid, param):
LOG.debug('Registering node %s with ironic' % node_uuid) LOG.debug('Registering node %s with ironic' % node_uuid)
node_info = nodes.Node.get_composed_node_by_uuid(node_uuid) node_controller = nodes.Node(node_id=node_uuid)
try: try:
ironic = utils.create_ironicclient() node_info = node_controller.get_composed_node_by_uuid()
except Exception as e: node_args, port_args = (
message = ('Error occurred while communicating to ' node_controller.connection.get_ironic_node_params(node_info,
'Ironic: %s' % six.text_type(e)) **param))
LOG.error(message) ironic_node = self.ironic.node.create(**node_args)
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: except Exception as e:
message = ('Unexpected error while registering node with ' message = ('Unexpected error while registering node with '
'Ironic: %s' % six.text_type(e)) 'Ironic: %s' % six.text_type(e))
LOG.error(message) LOG.error(message)
raise exception.ValenceException(message) raise exception.ValenceException(message)
node_info = db_api.Connection.update_composed_node(
node_uuid, {'managed_by': 'ironic'}).as_dict()
if port_args:
# If MAC provided, create ports, else skip
port_args['node_uuid'] = ironic_node.uuid
self.ironic_port_create(**port_args)
return node_info
def ironic_port_create(self, **port):
try:
self.ironic.port.create(**port)
LOG.debug('Successfully created ironic ports %s', port)
except Exception as e:
LOG.debug("Ironic port creation failed with error %s", str(e))

View File

@ -13,7 +13,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging
from valence.common import clients from valence.common import clients
from valence.common import exception
LOG = logging.getLogger(__name__)
def create_ironicclient(): def create_ironicclient():
@ -21,5 +26,10 @@ def create_ironicclient():
:returns: Ironic client object :returns: Ironic client object
""" """
osc = clients.OpenStackClients() try:
return osc.ironic() osc = clients.OpenStackClients()
return osc.ironic()
except Exception:
message = ('Error occurred while communicating to Ironic')
LOG.exception(message)
raise exception.ValenceException(message)

View File

@ -30,6 +30,7 @@ class TestAPINodes(unittest.TestCase):
@mock.patch('valence.redfish.sushy.sushy_instance.RedfishInstance') @mock.patch('valence.redfish.sushy.sushy_instance.RedfishInstance')
def setUp(self, mock_redfish, mock_connection): def setUp(self, mock_redfish, mock_connection):
self.node_controller = nodes.Node(podm_id='test-podm-1') self.node_controller = nodes.Node(podm_id='test-podm-1')
self.node_controller.node = test_utils.get_test_composed_node_db_info()
self.node_controller.connection = podm_base.PodManagerBase( self.node_controller.connection = podm_base.PodManagerBase(
'fake', 'fake-pass', 'http://fake-url') 'fake', 'fake-pass', 'http://fake-url')
@ -182,7 +183,7 @@ class TestAPINodes(unittest.TestCase):
self.node_controller.node = node_db self.node_controller.node = node_db
mock_redfish_get_node.return_value = node_hw mock_redfish_get_node.return_value = node_hw
result = self.node_controller.get_composed_node_by_uuid("fake_uuid") result = self.node_controller.get_composed_node_by_uuid()
expected = copy.deepcopy(node_hw) expected = copy.deepcopy(node_hw)
expected.update(node_db) expected.update(node_db)
self.assertEqual(expected, result) self.assertEqual(expected, result)
@ -192,7 +193,7 @@ class TestAPINodes(unittest.TestCase):
"""Test delete composed node""" """Test delete composed node"""
node_db = test_utils.get_test_composed_node_db_info() node_db = test_utils.get_test_composed_node_db_info()
self.node_controller.node = node_db self.node_controller.node = node_db
self.node_controller.delete_composed_node(node_db["uuid"]) self.node_controller.delete_composed_node()
mock_db_delete_composed_node.assert_called_once_with( mock_db_delete_composed_node.assert_called_once_with(
node_db["uuid"]) node_db["uuid"])
@ -216,8 +217,8 @@ class TestAPINodes(unittest.TestCase):
"""Test reset composed node status""" """Test reset composed node status"""
action = {"Reset": {"Type": "On"}} action = {"Reset": {"Type": "On"}}
self.node_controller.node = {'index': '1', 'name': 'test-node'} self.node_controller.node = {'index': '1', 'name': 'test-node'}
self.node_controller.node_action("fake_uuid", action) self.node_controller.node_action(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") @mock.patch("valence.provision.driver.node_register")
def test_node_register(self, mock_node_register): def test_node_register(self, mock_node_register):

View File

@ -21,52 +21,52 @@ from valence.provision.ironic import driver
class TestDriver(base.BaseTestCase): class TestDriver(base.BaseTestCase):
def setUp(self):
@mock.patch("valence.provision.ironic.utils.create_ironicclient")
def setUp(self, mock_ironic_client):
super(TestDriver, self).setUp() super(TestDriver, self).setUp()
self.ironic = driver.IronicDriver() self.driver = driver.IronicDriver()
self.driver.ironic = mock.MagicMock()
def tearDown(self): def tearDown(self):
super(TestDriver, self).tearDown() super(TestDriver, self).tearDown()
@mock.patch("valence.controller.nodes.Node.get_composed_node_by_uuid") @mock.patch("valence.db.api.Connection.get_composed_node_by_uuid")
def test_node_register_node_not_found(self, mock_db): def test_node_register_node_not_found(self, mock_db):
mock_db.side_effect = exception.NotFound mock_db.side_effect = exception.NotFound("node not found")
self.assertRaises(exception.NotFound, self.assertRaises(exception.NotFound,
self.ironic.node_register, self.driver.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', {}) 'fake-uuid', {})
@mock.patch('valence.redfish.sushy.sushy_instance.RedfishInstance')
@mock.patch("valence.db.api.Connection.update_composed_node") @mock.patch("valence.db.api.Connection.update_composed_node")
@mock.patch("valence.controller.nodes.Node.get_composed_node_by_uuid") @mock.patch('valence.controller.nodes.Node')
@mock.patch("valence.provision.ironic.utils.create_ironicclient") def test_node_register(self, node_mock, mock_node_update, mock_sushy):
def test_node_register(self, mock_client, ironic = self.driver.ironic
mock_node_get, mock_node_update): node_mock.return_value = mock.MagicMock()
ironic = mock.MagicMock() node_info = {
mock_client.return_value = ironic 'id': 'fake-uuid', 'podm_id': 'fake-podm_id',
mock_node_get.return_value = { 'index': '1',
'name': 'test', 'metadata': 'name': 'test', 'metadata':
{'network': [{'mac': 'fake-mac'}]}, {'network': [{'mac': 'fake-mac'}]},
'computer_system': '/redfish/v1/Systems/437XR1138R2'} 'computer_system': '/redfish/v1/Systems/437XR1138R2'}
node_controller = node_mock.return_value
node_controller.get_composed_node_by_uuid.return_value = node_info
node_info.update({'managed_by': 'ironic'})
mock_node_update.return_value.as_dict.return_value = node_info
node_controller.connection = mock.MagicMock()
ironic.node.create.return_value = mock.MagicMock(uuid='ironic-uuid') ironic.node.create.return_value = mock.MagicMock(uuid='ironic-uuid')
port_arg = {'node_uuid': 'ironic-uuid', 'address': 'fake-mac'} n_args = ({'driver_info': {'username': 'fake1'}}, {})
resp = self.ironic.node_register('fake-uuid', node_controller.connection.get_ironic_node_params.return_value = n_args
resp = self.driver.node_register('fake-uuid',
{"extra": {"foo": "bar"}}) {"extra": {"foo": "bar"}})
self.assertEqual({ self.assertEqual(node_info, resp)
'code': 'Node Registered', node_mock.assert_called_once_with(node_id='fake-uuid')
'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', mock_node_update.assert_called_once_with('fake-uuid',
{'managed_by': 'ironic'}) {'managed_by': 'ironic'})
ironic.node.create.assert_called_once() ironic.node.create.assert_called_once()
ironic.port.create.assert_called_once_with(**port_arg)
def test_ironic_port_create(self):
port_args = {'mac_address': 'fake-mac'}
self.driver.ironic_port_create(**port_args)
self.driver.ironic.port.create.assert_called_once()

View File

@ -15,6 +15,7 @@ import mock
from oslotest import base from oslotest import base
from valence.common import exception
from valence.provision.ironic import utils from valence.provision.ironic import utils
@ -27,3 +28,9 @@ class TestUtils(base.BaseTestCase):
ironic = utils.create_ironicclient() ironic = utils.create_ironicclient()
self.assertTrue(ironic) self.assertTrue(ironic)
mock_ironic.assert_called_once_with() mock_ironic.assert_called_once_with()
@mock.patch('valence.common.clients.OpenStackClients.ironic')
def test_create_ironic_client_failure(self, mock_client):
mock_client.side_effect = Exception()
self.assertRaises(exception.ValenceException,
utils.create_ironicclient)

View File

@ -219,7 +219,7 @@ class TestNodeApi(TestApiValidation):
resp = self.app.post('/v1/nodes/fake-node/action', resp = self.app.post('/v1/nodes/fake-node/action',
content_type='application/json', content_type='application/json',
data=json.dumps(req)) data=json.dumps(req))
mock_action.assert_called_once_with('fake-node', req) mock_action.assert_called_once_with(req)
self.assertEqual(http_client.NO_CONTENT, resp.status_code) self.assertEqual(http_client.NO_CONTENT, resp.status_code)
@mock.patch('valence.controller.nodes.Node.node_action') @mock.patch('valence.controller.nodes.Node.node_action')
@ -235,7 +235,7 @@ class TestNodeApi(TestApiValidation):
resp = self.app.post('/v1/nodes/fake-node/action', resp = self.app.post('/v1/nodes/fake-node/action',
content_type='application/json', content_type='application/json',
data=json.dumps(req)) data=json.dumps(req))
mock_action.assert_called_once_with('fake-node', req) mock_action.assert_called_once_with(req)
self.assertEqual(http_client.NO_CONTENT, resp.status_code) self.assertEqual(http_client.NO_CONTENT, resp.status_code)
@mock.patch('valence.controller.nodes.Node.node_action') @mock.patch('valence.controller.nodes.Node.node_action')
@ -251,7 +251,7 @@ class TestNodeApi(TestApiValidation):
resp = self.app.post('/v1/nodes/fake-node/action', resp = self.app.post('/v1/nodes/fake-node/action',
content_type='application/json', content_type='application/json',
data=json.dumps(req)) data=json.dumps(req))
mock_action.assert_called_once_with('fake-node', req) mock_action.assert_called_once_with(req)
self.assertEqual(http_client.NO_CONTENT, resp.status_code) self.assertEqual(http_client.NO_CONTENT, resp.status_code)
@mock.patch('valence.db.api.Connection.get_composed_node_by_uuid') @mock.patch('valence.db.api.Connection.get_composed_node_by_uuid')