Skip to content

Commit

Permalink
Proxy nova baremetal commands to Ironic
Browse files Browse the repository at this point in the history
This patch will proxy baremetal-node-list and baremetal-node-show
commands to Ironic. All other commands will raise an error if the
compute_driver is set to Ironic, otherwise the api will act as
expected.

Co-authored-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
Change-Id: Id9209d1d34a725750342434fbde1ff5681ff06b8
  • Loading branch information
2 people authored and kk7ds committed Sep 12, 2014
1 parent b62072f commit e7b121c
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 25 deletions.
153 changes: 128 additions & 25 deletions nova/api/openstack/compute/contrib/baremetal_nodes.py
Expand Up @@ -13,29 +13,53 @@
# 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
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 = {}
Expand All @@ -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."""

Expand Down Expand Up @@ -103,7 +157,12 @@ def construct(self):


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)
Expand All @@ -122,37 +181,72 @@ def _node_dict(self, node_ref):
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()
Expand All @@ -177,6 +271,9 @@ def create(self, req, body):
return {'node': node}

def delete(self, req, id):
if _use_ironic():
_no_ironic_proxy("node-delete")

context = req.environ['nova.context']
authorize(context)
try:
Expand All @@ -194,6 +291,9 @@ def _check_node_exists(self, context, node_id):
@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)
Expand All @@ -216,6 +316,9 @@ def _add_interface(self, req, id, body):
@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)
Expand Down
93 changes: 93 additions & 0 deletions nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py
Expand Up @@ -13,15 +13,20 @@
# 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
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):

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -371,3 +380,87 @@ def test_is_valid_mac(self):
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')

0 comments on commit e7b121c

Please sign in to comment.