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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""The bare-metal admin extension.""" """The bare-metal admin extension with Ironic Proxy."""
import netaddr import netaddr
from oslo.config import cfg
import webob import webob
from nova.api.openstack import extensions from nova.api.openstack import extensions
@ -23,19 +24,42 @@ from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil from nova.api.openstack import xmlutil
from nova import exception from nova import exception
from nova.i18n import _ from nova.i18n import _
from nova.openstack.common import importutils
from nova.openstack.common import log as logging
from nova.virt.baremetal import db from nova.virt.baremetal import db
ironic_client = importutils.try_import('ironicclient.client')
authorize = extensions.extension_authorizer('compute', 'baremetal_nodes') authorize = extensions.extension_authorizer('compute', 'baremetal_nodes')
node_fields = ['id', 'cpus', 'local_gb', 'memory_mb', 'pm_address', node_fields = ['id', 'cpus', 'local_gb', 'memory_mb', 'pm_address',
'pm_user', 'pm_user', 'service_host', 'terminal_port', 'instance_uuid']
'service_host', 'terminal_port', 'instance_uuid',
]
node_ext_fields = ['uuid', 'task_state', 'updated_at', 'pxe_config_path'] node_ext_fields = ['uuid', 'task_state', 'updated_at', 'pxe_config_path']
interface_fields = ['id', 'address', 'datapath_id', 'port_no'] 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): def _interface_dict(interface_ref):
d = {} d = {}
@ -56,6 +80,36 @@ def _make_interface_elem(elem):
elem.set(f) 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): def is_valid_mac(address):
"""Verify the format of a MAC address.""" """Verify the format of a MAC address."""
@ -103,7 +157,12 @@ class InterfaceTemplate(xmlutil.TemplateBuilder):
class BareMetalNodeController(wsgi.Controller): 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): def __init__(self, ext_mgr=None, *args, **kwargs):
super(BareMetalNodeController, self).__init__(*args, **kwargs) super(BareMetalNodeController, self).__init__(*args, **kwargs)
@ -122,37 +181,72 @@ class BareMetalNodeController(wsgi.Controller):
def index(self, req): def index(self, req):
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
nodes_from_db = db.bm_node_get_all(context)
nodes = [] nodes = []
for node_from_db in nodes_from_db: if _use_ironic():
try: # proxy command to Ironic
ifs = db.bm_interface_get_all_by_bm_node_id( icli = _get_ironic_client()
context, node_from_db['id']) ironic_nodes = icli.node.list(detail=True)
except exception.NodeNotFound: for inode in ironic_nodes:
ifs = [] node = {'id': inode.uuid,
node = self._node_dict(node_from_db) 'interfaces': [],
node['interfaces'] = [_interface_dict(i) for i in ifs] 'host': 'IRONIC MANAGED',
nodes.append(node) '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} return {'nodes': nodes}
@wsgi.serializers(xml=NodeTemplate) @wsgi.serializers(xml=NodeTemplate)
def show(self, req, id): def show(self, req, id):
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
try: if _use_ironic():
node = db.bm_node_get(context, id) # proxy command to Ironic
except exception.NodeNotFound: icli = _get_ironic_client()
raise webob.exc.HTTPNotFound() inode = icli.node.get(id)
try: iports = icli.node.list_ports(id)
ifs = db.bm_interface_get_all_by_bm_node_id(context, id) node = {'id': inode.uuid,
except exception.NodeNotFound: 'interfaces': [],
ifs = [] 'host': 'IRONIC MANAGED',
node = self._node_dict(node) 'task_state': inode.provision_state,
node['interfaces'] = [_interface_dict(i) for i in ifs] '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} return {'node': node}
@wsgi.serializers(xml=NodeTemplate) @wsgi.serializers(xml=NodeTemplate)
def create(self, req, body): def create(self, req, body):
if _use_ironic():
_no_ironic_proxy("node-create")
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
values = body['node'].copy() values = body['node'].copy()
@ -177,6 +271,9 @@ class BareMetalNodeController(wsgi.Controller):
return {'node': node} return {'node': node}
def delete(self, req, id): def delete(self, req, id):
if _use_ironic():
_no_ironic_proxy("node-delete")
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
try: try:
@ -194,6 +291,9 @@ class BareMetalNodeController(wsgi.Controller):
@wsgi.serializers(xml=InterfaceTemplate) @wsgi.serializers(xml=InterfaceTemplate)
@wsgi.action('add_interface') @wsgi.action('add_interface')
def _add_interface(self, req, id, body): def _add_interface(self, req, id, body):
if _use_ironic():
_no_ironic_proxy("port-create")
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
self._check_node_exists(context, id) self._check_node_exists(context, id)
@ -216,6 +316,9 @@ class BareMetalNodeController(wsgi.Controller):
@wsgi.response(202) @wsgi.response(202)
@wsgi.action('remove_interface') @wsgi.action('remove_interface')
def _remove_interface(self, req, id, body): def _remove_interface(self, req, id, body):
if _use_ironic():
_no_ironic_proxy("port-delete")
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
self._check_node_exists(context, id) self._check_node_exists(context, id)

View File

@ -13,6 +13,8 @@
# 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 mock
from oslo.config import cfg
from webob import exc from webob import exc
from nova.api.openstack.compute.contrib import baremetal_nodes 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 context
from nova import exception from nova import exception
from nova import test from nova import test
from nova.tests.virt.ironic import utils as ironic_utils
from nova.virt.baremetal import db from nova.virt.baremetal import db
CONF = cfg.CONF
class FakeRequest(object): class FakeRequest(object):
@ -69,7 +74,11 @@ def fake_interface(**updates):
interface.update(updates) interface.update(updates)
return interface return interface
FAKE_IRONIC_CLIENT = ironic_utils.FakeClient()
@mock.patch.object(baremetal_nodes, '_get_ironic_client',
lambda *_: FAKE_IRONIC_CLIENT)
class BareMetalNodesTest(test.NoDBTestCase): class BareMetalNodesTest(test.NoDBTestCase):
def setUp(self): 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.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"))
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')