diff --git a/nova/api/openstack/compute/contrib/baremetal_nodes.py b/nova/api/openstack/compute/contrib/baremetal_nodes.py index bff5a45be0ef..909937c801c0 100644 --- a/nova/api/openstack/compute/contrib/baremetal_nodes.py +++ b/nova/api/openstack/compute/contrib/baremetal_nodes.py @@ -13,9 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -"""The bare-metal admin extension.""" +"""The bare-metal admin extension with Ironic Proxy.""" import netaddr +from oslo.config import cfg import webob from nova.api.openstack import extensions @@ -23,19 +24,42 @@ from nova.api.openstack import wsgi from nova.api.openstack import xmlutil from nova import exception from nova.i18n import _ +from nova.openstack.common import importutils +from nova.openstack.common import log as logging from nova.virt.baremetal import db +ironic_client = importutils.try_import('ironicclient.client') + authorize = extensions.extension_authorizer('compute', 'baremetal_nodes') node_fields = ['id', 'cpus', 'local_gb', 'memory_mb', 'pm_address', - 'pm_user', - 'service_host', 'terminal_port', 'instance_uuid', - ] + 'pm_user', 'service_host', 'terminal_port', 'instance_uuid'] node_ext_fields = ['uuid', 'task_state', 'updated_at', 'pxe_config_path'] interface_fields = ['id', 'address', 'datapath_id', 'port_no'] +CONF = cfg.CONF + +CONF.import_opt('api_version', + 'nova.virt.ironic.driver', + group='ironic') +CONF.import_opt('api_endpoint', + 'nova.virt.ironic.driver', + group='ironic') +CONF.import_opt('admin_username', + 'nova.virt.ironic.driver', + group='ironic') +CONF.import_opt('admin_password', + 'nova.virt.ironic.driver', + group='ironic') +CONF.import_opt('admin_tenant_name', + 'nova.virt.ironic.driver', + group='ironic') +CONF.import_opt('compute_driver', 'nova.virt.driver') + +LOG = logging.getLogger(__name__) + def _interface_dict(interface_ref): d = {} @@ -56,6 +80,36 @@ def _make_interface_elem(elem): elem.set(f) +def _use_ironic(): + # TODO(lucasagomes): This switch this should also be deleted as + # part of the Nova Baremetal removal effort. At that point, any + # code that checks it should assume True, the False case should be + # removed, and this API will only/always proxy to Ironic. + return 'ironic' in CONF.compute_driver + + +def _get_ironic_client(): + """return an Ironic client.""" + # TODO(NobodyCam): Fix insecure setting + kwargs = {'os_username': CONF.ironic.admin_username, + 'os_password': CONF.ironic.admin_password, + 'os_auth_url': CONF.ironic.admin_url, + 'os_tenant_name': CONF.ironic.admin_tenant_name, + 'os_service_type': 'baremetal', + 'os_endpoint_type': 'public', + 'insecure': 'true', + 'ironic_url': CONF.ironic.api_endpoint} + icli = ironic_client.get_client(CONF.ironic.api_version, **kwargs) + return icli + + +def _no_ironic_proxy(cmd): + raise webob.exc.HTTPBadRequest( + explanation=_("Command Not supported. Please use Ironic " + "command %(cmd)s to perform this " + "action.") % {'cmd': cmd}) + + def is_valid_mac(address): """Verify the format of a MAC address.""" @@ -103,7 +157,12 @@ class InterfaceTemplate(xmlutil.TemplateBuilder): class BareMetalNodeController(wsgi.Controller): - """The Bare-Metal Node API controller for the OpenStack API.""" + """The Bare-Metal Node API controller for the OpenStack API. + + Ironic is used for the following commands: + 'baremetal-node-list' + 'baremetal-node-show' + """ def __init__(self, ext_mgr=None, *args, **kwargs): super(BareMetalNodeController, self).__init__(*args, **kwargs) @@ -122,37 +181,72 @@ class BareMetalNodeController(wsgi.Controller): def index(self, req): context = req.environ['nova.context'] authorize(context) - nodes_from_db = db.bm_node_get_all(context) nodes = [] - for node_from_db in nodes_from_db: - try: - ifs = db.bm_interface_get_all_by_bm_node_id( - context, node_from_db['id']) - except exception.NodeNotFound: - ifs = [] - node = self._node_dict(node_from_db) - node['interfaces'] = [_interface_dict(i) for i in ifs] - nodes.append(node) + if _use_ironic(): + # proxy command to Ironic + icli = _get_ironic_client() + ironic_nodes = icli.node.list(detail=True) + for inode in ironic_nodes: + node = {'id': inode.uuid, + 'interfaces': [], + 'host': 'IRONIC MANAGED', + 'task_state': inode.provision_state, + 'cpus': inode.properties['cpus'], + 'memory_mb': inode.properties['memory_mb'], + 'disk_gb': inode.properties['local_gb']} + nodes.append(node) + else: + # use nova baremetal + nodes_from_db = db.bm_node_get_all(context) + for node_from_db in nodes_from_db: + try: + ifs = db.bm_interface_get_all_by_bm_node_id( + context, node_from_db['id']) + except exception.NodeNotFound: + ifs = [] + node = self._node_dict(node_from_db) + node['interfaces'] = [_interface_dict(i) for i in ifs] + nodes.append(node) return {'nodes': nodes} @wsgi.serializers(xml=NodeTemplate) def show(self, req, id): context = req.environ['nova.context'] authorize(context) - try: - node = db.bm_node_get(context, id) - except exception.NodeNotFound: - raise webob.exc.HTTPNotFound() - try: - ifs = db.bm_interface_get_all_by_bm_node_id(context, id) - except exception.NodeNotFound: - ifs = [] - node = self._node_dict(node) - node['interfaces'] = [_interface_dict(i) for i in ifs] + if _use_ironic(): + # proxy command to Ironic + icli = _get_ironic_client() + inode = icli.node.get(id) + iports = icli.node.list_ports(id) + node = {'id': inode.uuid, + 'interfaces': [], + 'host': 'IRONIC MANAGED', + 'task_state': inode.provision_state, + 'cpus': inode.properties['cpus'], + 'memory_mb': inode.properties['memory_mb'], + 'disk_gb': inode.properties['local_gb'], + 'instance_uuid': inode.instance_uuid} + for port in iports: + node['interfaces'].append({'address': port.address}) + else: + # use nova baremetal + try: + node = db.bm_node_get(context, id) + except exception.NodeNotFound: + raise webob.exc.HTTPNotFound() + try: + ifs = db.bm_interface_get_all_by_bm_node_id(context, id) + except exception.NodeNotFound: + ifs = [] + node = self._node_dict(node) + node['interfaces'] = [_interface_dict(i) for i in ifs] return {'node': node} @wsgi.serializers(xml=NodeTemplate) def create(self, req, body): + if _use_ironic(): + _no_ironic_proxy("node-create") + context = req.environ['nova.context'] authorize(context) values = body['node'].copy() @@ -177,6 +271,9 @@ class BareMetalNodeController(wsgi.Controller): return {'node': node} def delete(self, req, id): + if _use_ironic(): + _no_ironic_proxy("node-delete") + context = req.environ['nova.context'] authorize(context) try: @@ -194,6 +291,9 @@ class BareMetalNodeController(wsgi.Controller): @wsgi.serializers(xml=InterfaceTemplate) @wsgi.action('add_interface') def _add_interface(self, req, id, body): + if _use_ironic(): + _no_ironic_proxy("port-create") + context = req.environ['nova.context'] authorize(context) self._check_node_exists(context, id) @@ -216,6 +316,9 @@ class BareMetalNodeController(wsgi.Controller): @wsgi.response(202) @wsgi.action('remove_interface') def _remove_interface(self, req, id, body): + if _use_ironic(): + _no_ironic_proxy("port-delete") + context = req.environ['nova.context'] authorize(context) self._check_node_exists(context, id) diff --git a/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py b/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py index 3804660cac30..908e2b34c11d 100644 --- a/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py +++ b/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock +from oslo.config import cfg from webob import exc from nova.api.openstack.compute.contrib import baremetal_nodes @@ -20,8 +22,11 @@ from nova.api.openstack import extensions from nova import context from nova import exception from nova import test +from nova.tests.virt.ironic import utils as ironic_utils from nova.virt.baremetal import db +CONF = cfg.CONF + class FakeRequest(object): @@ -69,7 +74,11 @@ def fake_interface(**updates): interface.update(updates) return interface +FAKE_IRONIC_CLIENT = ironic_utils.FakeClient() + +@mock.patch.object(baremetal_nodes, '_get_ironic_client', + lambda *_: FAKE_IRONIC_CLIENT) class BareMetalNodesTest(test.NoDBTestCase): def setUp(self): @@ -371,3 +380,87 @@ class BareMetalNodesTest(test.NoDBTestCase): self.assertTrue(baremetal_nodes.is_valid_mac("AA:BB:CC:DD:EE:FF")) self.assertFalse(baremetal_nodes.is_valid_mac("AA BB CC DD EE FF")) self.assertFalse(baremetal_nodes.is_valid_mac("AA-BB-CC-DD-EE-FF")) + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list') + def test_index_ironic(self, mock_list): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + + properties = {'cpus': 2, 'memory_mb': 1024, 'local_gb': 20} + node = ironic_utils.get_test_node(properties=properties) + mock_list.return_value = [node] + + res_dict = self.controller.index(self.request) + expected_output = {'nodes': + [{'memory_mb': properties['memory_mb'], + 'host': 'IRONIC MANAGED', + 'disk_gb': properties['local_gb'], + 'interfaces': [], + 'task_state': None, + 'id': node.uuid, + 'cpus': properties['cpus']}]} + self.assertEqual(expected_output, res_dict) + mock_list.assert_called_once_with(detail=True) + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports') + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get') + def test_show_ironic(self, mock_get, mock_list_ports): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + + properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10} + node = ironic_utils.get_test_node(properties=properties) + port = ironic_utils.get_test_port() + mock_get.return_value = node + mock_list_ports.return_value = [port] + + res_dict = self.controller.show(self.request, node.uuid) + expected_output = {'node': + {'memory_mb': properties['memory_mb'], + 'instance_uuid': None, + 'host': 'IRONIC MANAGED', + 'disk_gb': properties['local_gb'], + 'interfaces': [{'address': port.address}], + 'task_state': None, + 'id': node.uuid, + 'cpus': properties['cpus']}} + self.assertEqual(expected_output, res_dict) + mock_get.assert_called_once_with(node.uuid) + mock_list_ports.assert_called_once_with(node.uuid) + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports') + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get') + def test_show_ironic_no_interfaces(self, mock_get, mock_list_ports): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + + properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10} + node = ironic_utils.get_test_node(properties=properties) + mock_get.return_value = node + mock_list_ports.return_value = [] + + res_dict = self.controller.show(self.request, node.uuid) + self.assertEqual([], res_dict['node']['interfaces']) + mock_get.assert_called_once_with(node.uuid) + mock_list_ports.assert_called_once_with(node.uuid) + + def test_create_ironic_not_supported(self): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, + self.request, {'node': object()}) + + def test_delete_ironic_not_supported(self): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + self.assertRaises(exc.HTTPBadRequest, + self.controller.delete, + self.request, 'fake-id') + + def test_add_interface_ironic_not_supported(self): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + self.assertRaises(exc.HTTPBadRequest, + self.controller._add_interface, + self.request, 'fake-id', 'fake-body') + + def test_remove_interface_ironic_not_supported(self): + CONF.set_override('compute_driver', 'nova.virt.ironic.driver') + self.assertRaises(exc.HTTPBadRequest, + self.controller._remove_interface, + self.request, 'fake-id', 'fake-body')