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
|
||||
# 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,8 +181,23 @@ 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 = []
|
||||
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(
|
||||
@ -139,6 +213,23 @@ class BareMetalNodeController(wsgi.Controller):
|
||||
def show(self, req, id):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
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:
|
||||
@ -153,6 +244,9 @@ class BareMetalNodeController(wsgi.Controller):
|
||||
|
||||
@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)
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user