Merge "Proxy nova baremetal commands to Ironic"

This commit is contained in:
Jenkins 2014-09-13 15:20:36 +00:00 committed by Gerrit Code Review
commit 879cc92f88
2 changed files with 221 additions and 25 deletions

View File

@ -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)

View File

@ -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')