diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 80f42427f96..c35ca0ef40a 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -65,6 +65,8 @@ "compute_extension:v3:os-attach-interfaces": "", "compute_extension:v3:os-attach-interfaces:discoverable": "", "compute_extension:baremetal_nodes": "rule:admin_api", + "compute_extension:v3:os-baremetal-nodes": "rule:admin_api", + "compute_extension:v3:os-baremetal-nodes:discoverable": "", "compute_extension:v3:os-block-device-mapping-v1:discoverable": "", "compute_extension:cells": "rule:admin_api", "compute_extension:cells:create": "rule:admin_api", diff --git a/nova/api/openstack/compute/plugins/v3/baremetal_nodes.py b/nova/api/openstack/compute/plugins/v3/baremetal_nodes.py new file mode 100644 index 00000000000..3582e880904 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/baremetal_nodes.py @@ -0,0 +1,173 @@ +# Copyright (c) 2013 NTT DOCOMO, INC. +# Copyright 2014 IBM Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The bare-metal admin extension.""" + +from oslo.config import cfg +from oslo.utils import importutils +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.i18n import _ + +ironic_client = importutils.try_import('ironicclient.client') + +CONF = cfg.CONF +ALIAS = "os-baremetal-nodes" +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + +node_fields = ['id', 'cpus', 'local_gb', 'memory_mb', 'pm_address', + '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.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') + + +def _interface_dict(interface_ref): + d = {} + for f in interface_fields: + d[f] = interface_ref.get(f) + return d + + +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}) + + +class BareMetalNodeController(wsgi.Controller): + """The Bare-Metal Node API controller for the OpenStack API.""" + + def _node_dict(self, node_ref): + d = {} + for f in node_fields: + d[f] = node_ref.get(f) + for f in node_ext_fields: + d[f] = node_ref.get(f) + return d + + @extensions.expected_errors(404) + def index(self, req): + context = req.environ['nova.context'] + authorize(context) + nodes = [] + # 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) + return {'nodes': nodes} + + @extensions.expected_errors(404) + def show(self, req, id): + context = req.environ['nova.context'] + authorize(context) + # 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}) + return {'node': node} + + @extensions.expected_errors(400) + def create(self, req, body): + _no_ironic_proxy("port-create") + + @extensions.expected_errors(400) + def delete(self, req, id): + _no_ironic_proxy("port-create") + + @wsgi.action('add_interface') + @extensions.expected_errors(400) + def _add_interface(self, req, id, body): + _no_ironic_proxy("port-create") + + @wsgi.action('remove_interface') + @extensions.expected_errors(400) + def _remove_interface(self, req, id, body): + _no_ironic_proxy("port-delete") + + +class BareMetalNodes(extensions.V3APIExtensionBase): + """Admin-only bare-metal node administration.""" + + name = "BareMetalNodes" + alias = ALIAS + version = 1 + + def get_resources(self): + resource = [extensions.ResourceExtension(ALIAS, + BareMetalNodeController(), + member_actions={"action": "POST"})] + return resource + + def get_controller_extensions(self): + """It's an abstract function V3APIExtensionBase and the extension + will not be loaded without it. + """ + return [] diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index 5ddf116ad6a..af8bb228c53 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -100,3 +100,9 @@ }, 'additionalProperties': False } + + +mac_address = { + 'type': 'string', + 'pattern': '^([0-9a-fA-F]{2})(:[0-9a-fA-F]{2}){5}$' +} diff --git a/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py b/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py index 62a61afd697..9650f9560a0 100644 --- a/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py +++ b/nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py @@ -14,17 +14,16 @@ # 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.compute.contrib import baremetal_nodes as b_nodes_v2 +from nova.api.openstack.compute.plugins.v3 import baremetal_nodes \ + as b_nodes_v21 from nova.api.openstack import extensions from nova import context from nova import test from nova.tests.virt.ironic import utils as ironic_utils -CONF = cfg.CONF - class FakeRequest(object): @@ -64,18 +63,19 @@ def fake_node_ext_status(**updates): FAKE_IRONIC_CLIENT = ironic_utils.FakeClient() -@mock.patch.object(baremetal_nodes, '_get_ironic_client', +@mock.patch.object(b_nodes_v21, '_get_ironic_client', lambda *_: FAKE_IRONIC_CLIENT) -class BareMetalNodesTest(test.NoDBTestCase): - +class BareMetalNodesTestV21(test.NoDBTestCase): def setUp(self): - super(BareMetalNodesTest, self).setUp() + super(BareMetalNodesTestV21, self).setUp() - self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self._setup() self.context = context.get_admin_context() - self.controller = baremetal_nodes.BareMetalNodeController(self.ext_mgr) self.request = FakeRequest(self.context) + def _setup(self): + self.controller = b_nodes_v21.BareMetalNodeController() + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list') def test_index_ironic(self, mock_list): properties = {'cpus': 2, 'memory_mb': 1024, 'local_gb': 20} @@ -149,3 +149,11 @@ def test_remove_interface_ironic_not_supported(self): self.assertRaises(exc.HTTPBadRequest, self.controller._remove_interface, self.request, 'fake-id', 'fake-body') + + +@mock.patch.object(b_nodes_v2, '_get_ironic_client', + lambda *_: FAKE_IRONIC_CLIENT) +class BareMetalNodesTestV2(BareMetalNodesTestV21): + def _setup(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = b_nodes_v2.BareMetalNodeController(self.ext_mgr) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 7321ff11897..d2cd13fcdb3 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -138,6 +138,7 @@ "compute_extension:attach_interfaces": "", "compute_extension:v3:os-attach-interfaces": "", "compute_extension:baremetal_nodes": "", + "compute_extension:v3:os-baremetal-nodes": "", "compute_extension:cells": "", "compute_extension:cells:create": "rule:admin_api", "compute_extension:cells:delete": "rule:admin_api", diff --git a/setup.cfg b/setup.cfg index 48b8008303e..2eadcb1a4a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,7 @@ nova.api.v3.extensions = aggregates = nova.api.openstack.compute.plugins.v3.aggregates:Aggregates attach_interfaces = nova.api.openstack.compute.plugins.v3.attach_interfaces:AttachInterfaces availability_zone = nova.api.openstack.compute.plugins.v3.availability_zone:AvailabilityZone + baremetal_nodes = nova.api.openstack.compute.plugins.v3.baremetal_nodes:BareMetalNodes block_device_mapping = nova.api.openstack.compute.plugins.v3.block_device_mapping:BlockDeviceMapping cells = nova.api.openstack.compute.plugins.v3.cells:Cells certificates = nova.api.openstack.compute.plugins.v3.certificates:Certificates