Merge "Proxy nova baremetal commands to Ironic"
This commit is contained in:
commit
879cc92f88
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue