From d577c88d4a3eb2d16127e88239e6032bb496d390 Mon Sep 17 00:00:00 2001 From: wanghao Date: Mon, 3 Jul 2017 14:28:04 +0800 Subject: [PATCH] Manage existing BMs: Part-1 This patch introduce a new API: 'GET: /manageable_servers' to list the adoptable nodes from drivers to operators. As a reference, now we implement api in the Ironic driver. APIImpact Implements: bp manage-existing-bms Change-Id: I56340ce534c3b8d4e855a4c753ecf90a07147d29 --- api-ref/source/v1/manageable_servers.inc | 40 +++++++ api-ref/source/v1/parameters.yaml | 54 +++++++++ .../manageable-servers-list-resp.json | 26 ++++ mogan/api/controllers/v1/__init__.py | 13 ++ .../api/controllers/v1/manageable_servers.py | 86 ++++++++++++++ mogan/baremetal/driver.py | 7 ++ mogan/baremetal/ironic/driver.py | 111 ++++++++++++++++++ mogan/common/exception.py | 4 + mogan/common/policy.py | 3 + mogan/engine/api.py | 9 ++ mogan/engine/manager.py | 3 + mogan/engine/rpcapi.py | 4 + .../unit/api/v1/test_manageable_servers.py | 48 ++++++++ mogan/tests/unit/engine/test_manager.py | 18 +++ 14 files changed, 426 insertions(+) create mode 100644 api-ref/source/v1/manageable_servers.inc create mode 100644 api-ref/source/v1/samples/manageable_servers/manageable-servers-list-resp.json create mode 100644 mogan/api/controllers/v1/manageable_servers.py create mode 100644 mogan/tests/unit/api/v1/test_manageable_servers.py diff --git a/api-ref/source/v1/manageable_servers.inc b/api-ref/source/v1/manageable_servers.inc new file mode 100644 index 00000000..afd3a317 --- /dev/null +++ b/api-ref/source/v1/manageable_servers.inc @@ -0,0 +1,40 @@ +.. -*- rst -*- + +=================== + Manageable Servers +=================== + +Lists manageable servers. + +List manageable servers information +=================================== + +.. rest_method:: GET /manageable_servers + +Lists manageable servers information. + +Normal response codes: 200 + +Error response codes: unauthorized(401), forbidden(403) + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - manageable_servers: manageable_servers + - uuid: manageable_servers_uuid + - name: manageable_servers_name + - resource_class: manageable_servers_resource_class + - power_state: manageable_servers_power_state + - provision_state: manageable_servers_provision_state + - ports: manageable_servers_ports + - portgroups: manageable_servers_portgroups + - image_source: manageable_servers_image_source + +| + +**Example List manageable servers information** + +.. literalinclude:: samples/manageable_servers/manageable-servers-list-resp.json + :language: javascript diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 1d251293..7567ce18 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -425,6 +425,60 @@ lock_state: in: body required: true type: boolean +manageable_servers: + description: | + An array of manageable servers information. + in: body + required: true + type: array +manageable_servers_image_source: + description: | + Image source uuid of manageable server. + in: body + required: true + type: string +manageable_servers_name: + description: | + Name of manageable server. + in: body + required: true + type: string +manageable_servers_portgroups: + description: | + The portgroups of manageable server. + in: body + required: true + type: array +manageable_servers_ports: + description: | + The ports of manageable server. + in: body + required: true + type: array +manageable_servers_power_state: + description: | + The power state of manageable server. + in: body + required: true + type: string +manageable_servers_provision_state: + description: | + The provision state of manageable server. + in: body + required: true + type: string +manageable_servers_resource_class: + description: | + Resource class of manageable server. + in: body + required: true + type: string +manageable_servers_uuid: + description: | + UUID of manageable server. + in: body + required: true + type: string max_count_body: description: | The max number of servers to be created. Defaults to the value of ``min_count``. diff --git a/api-ref/source/v1/samples/manageable_servers/manageable-servers-list-resp.json b/api-ref/source/v1/samples/manageable_servers/manageable-servers-list-resp.json new file mode 100644 index 00000000..b754613c --- /dev/null +++ b/api-ref/source/v1/samples/manageable_servers/manageable-servers-list-resp.json @@ -0,0 +1,26 @@ +{ + "manageable_servers": [ + { + "uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94", + "name": "test_node", + "resource_class": "gold", + "power_state": "power on", + "provision_state": "active", + "ports": [ + { + "address": "a4:dc:be:0e:82:a5", + "uuid": "1ec01153-685a-49b5-a6d3-45a4e7dddf53", + "neutron_port_id": "a9b94592-1d8e-46bb-836b-c7ba935b0136" + } + ], + "portgroups": [ + { + "address": "a4:dc:be:0e:82:a6", + "uuid": "1ec01153-685a-49b5-a6d3-45a4e7dddf54", + "neutron_port_id": "a9b94592-1d8e-46bb-836b-c7ba935b0137" + } + ], + "image_source": "03239419-e588-42b6-a70f-94f23ed0c9e2" + } + ] +} diff --git a/mogan/api/controllers/v1/__init__.py b/mogan/api/controllers/v1/__init__.py index 2a477881..e4d1dcad 100644 --- a/mogan/api/controllers/v1/__init__.py +++ b/mogan/api/controllers/v1/__init__.py @@ -29,6 +29,7 @@ from mogan.api.controllers.v1 import aggregates from mogan.api.controllers.v1 import availability_zones from mogan.api.controllers.v1 import flavors from mogan.api.controllers.v1 import keypairs +from mogan.api.controllers.v1 import manageable_servers from mogan.api.controllers.v1 import nodes from mogan.api.controllers.v1 import server_groups from mogan.api.controllers.v1 import servers @@ -62,6 +63,9 @@ class V1(base.APIBase): server_groups = [link.Link] """Links to the server groups resource""" + manageable_servers = [link.Link] + """Links to the manageable servers resource""" + @staticmethod def convert(): v1 = V1() @@ -120,6 +124,14 @@ class V1(base.APIBase): 'server_groups', '', bookmark=True) ] + v1.manageable_servers = [link.Link.make_link('self', + pecan.request.public_url, + 'manageable_servers', ''), + link.Link.make_link('bookmark', + pecan.request.public_url, + 'manageable_servers', '', + bookmark=True) + ] return v1 @@ -133,6 +145,7 @@ class Controller(rest.RestController): aggregates = aggregates.AggregateController() nodes = nodes.NodeController() server_groups = server_groups.ServerGroupController() + manageable_servers = manageable_servers.ManageableServersController() @expose.expose(V1) def get(self): diff --git a/mogan/api/controllers/v1/manageable_servers.py b/mogan/api/controllers/v1/manageable_servers.py new file mode 100644 index 00000000..4364c790 --- /dev/null +++ b/mogan/api/controllers/v1/manageable_servers.py @@ -0,0 +1,86 @@ +# Copyright 2017 Fiberhome Integration Technologies Co.,LTD. +# 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. + +import pecan +from pecan import rest +from wsme import types as wtypes + +from mogan.api.controllers import base +from mogan.api.controllers.v1 import types +from mogan.api import expose +from mogan.common import policy + + +class ManageableServer(base.APIBase): + """API representation of manageable server.""" + + uuid = types.uuid + """The UUID of the manageable server""" + + name = wtypes.text + """The name of the manageable server""" + + resource_class = wtypes.text + """The resource_class of the manageable server""" + + power_state = wtypes.text + """The power_state of the manageable server""" + + provision_state = wtypes.text + """The provision_state of the manageable server""" + + ports = types.jsontype + """The ports of the manageable server""" + + portgroups = types.jsontype + """The portgroups of the manageable server""" + + image_source = types.uuid + """The UUID of the image id which manageable server use""" + + def __init__(self, **kwargs): + super(ManageableServer, self).__init__(**kwargs) + self.fields = [] + for field in kwargs.keys(): + if not hasattr(self, field): + continue + self.fields.append(field) + setattr(self, field, kwargs.get(field, wtypes.Unset)) + + +class ManageableServerCollection(base.APIBase): + """API representation of a collection of manageable server.""" + + manageable_servers = [ManageableServer] + """A list containing manageable server objects""" + + @staticmethod + def convert_with_list_of_dicts(manageable_servers): + collection = ManageableServerCollection() + collection.manageable_servers = [ManageableServer(**mserver) + for mserver in manageable_servers] + return collection + + +class ManageableServersController(rest.RestController): + """REST controller for manage existing servers.""" + + @policy.authorize_wsgi("mogan:manageable_servers", "get_all", False) + @expose.expose(ManageableServerCollection) + def get_all(self): + """List manageable servers from driver.""" + nodes = pecan.request.engine_api.get_manageable_servers( + pecan.request.context) + return ManageableServerCollection.convert_with_list_of_dicts(nodes) diff --git a/mogan/baremetal/driver.py b/mogan/baremetal/driver.py index 38b10ac5..f55e070c 100644 --- a/mogan/baremetal/driver.py +++ b/mogan/baremetal/driver.py @@ -137,6 +137,13 @@ class BaseEngineDriver(object): """ raise NotImplementedError() + def get_manageable_nodes(self): + """Retrieve all manageable nodes information. + + :returns:A list of describing manageable nodes + """ + raise NotImplementedError() + def load_engine_driver(engine_driver): """Load a engine driver module. diff --git a/mogan/baremetal/ironic/driver.py b/mogan/baremetal/ironic/driver.py index a55b4d65..e955d214 100644 --- a/mogan/baremetal/ironic/driver.py +++ b/mogan/baremetal/ironic/driver.py @@ -44,6 +44,10 @@ _NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state', 'target_provision_state', 'last_error', 'maintenance', 'properties', 'instance_uuid') +TENANT_VIF_KEY = 'tenant_vif_port_id' + +VIF_KEY = 'vif_port_id' + def map_power_state(state): try: @@ -99,6 +103,37 @@ class IronicDriver(base_driver.BaseEngineDriver): except ironic_exc.NotFound: raise exception.ServerNotFound(server=server.uuid) + def _node_resource(self, node): + """Helper method to create resource dict from node stats.""" + + dic = { + 'resource_class': str(node.resource_class), + 'ports': node.ports, + 'portgroups': node.portgroups, + 'name': node.name, + 'power_state': node.power_state, + 'provision_state': node.provision_state, + 'image_source': node.instance_info.get('image_source'), + } + + return dic + + def _port_or_group_resource(self, port_or_pg): + """Helper method to create resource dict from port or portgroup + + stats. + + """ + + neutron_port_id = (port_or_pg.internal_info.get(TENANT_VIF_KEY) or + port_or_pg.extra.get(VIF_KEY)) + dic = { + 'address': port_or_pg.address, + 'uuid': port_or_pg.uuid, + 'neutron_port_id': neutron_port_id, + } + return dic + def _add_server_info_to_node(self, node, server): patch = list() # Associate the node with a server @@ -356,6 +391,67 @@ class IronicDriver(base_driver.BaseEngineDriver): LOG.info('Successfully unprovisioned Ironic node %s', node.uuid, server=server) + def _get_manageable_nodes(self): + """Helper function to return the list of manageable nodes. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of raw node from ironic + + """ + + # Retrieve nodes + params = { + 'maintenance': False, + 'detail': True, + 'provision_state': ironic_states.ACTIVE, + 'associated': False, + 'limit': 0 + } + try: + node_list = self.ironicclient.call("node.list", **params) + except client_e.ClientException as e: + LOG.exception("Could not get nodes from ironic. Reason: " + "%(detail)s", {'detail': six.text_type(e)}) + raise e + + # Retrive ports + params = { + 'limit': 0, + 'fields': ('uuid', 'node_uuid', 'extra', 'address', + 'internal_info') + } + + try: + port_list = self.ironicclient.call("port.list", **params) + except client_e.ClientException as e: + LOG.exception("Could not get ports from ironic. Reason: " + "%(detail)s", {'detail': six.text_type(e)}) + port_list = [] + + # Retrive portgroups + try: + portgroup_list = self.ironicclient.call("portgroup.list", **params) + except client_e.ClientException as e: + LOG.exception("Could not get portgroups from ironic. Reason: " + "%(detail)s", {'detail': six.text_type(e)}) + portgroup_list = [] + + node_resources = {} + for node in node_list: + if node.resource_class is None: + continue + # Add ports to the associated node + node.ports = [self._port_or_group_resource(port) + for port in port_list + if node.uuid == port.node_uuid] + # Add portgroups to the associated node + node.portgroups = [self._port_or_group_resource(portgroup) + for portgroup in portgroup_list + if node.uuid == portgroup.node_uuid] + node_resources[node.uuid] = self._node_resource(node) + return node_resources + def get_maintenance_node_list(self): """Helper function to return the list of maintenance nodes. @@ -602,3 +698,18 @@ class IronicDriver(base_driver.BaseEngineDriver): """ return (not node.instance_uuid and node.provision_state == ironic_states.AVAILABLE) + + def get_manageable_nodes(self): + nodes = self._get_manageable_nodes() + manageable_nodes = [] + for node_uuid, node in nodes.items(): + manageable_nodes.append( + {'uuid': node_uuid, + 'name': node.get('name'), + 'resource_class': node.get('resource_class'), + 'power_state': node.get('power_state'), + 'provision_state': node.get('provision_state'), + 'ports': node.get('ports'), + 'portgroups': node.get('portgroups'), + 'image_source': node.get('image_source')}) + return manageable_nodes diff --git a/mogan/common/exception.py b/mogan/common/exception.py index e1917849..178596aa 100644 --- a/mogan/common/exception.py +++ b/mogan/common/exception.py @@ -464,4 +464,8 @@ class ServerGroupNotFound(NotFound): class ServerGroupExists(Conflict): _msg_fmt = _("Sever group %(group_uuid)s already exists.") + +class GetManageableServersFailed(MoganException): + _msg_fmt = _("Failed to get manageable servers from driver: %(reason)s") + ObjectActionError = obj_exc.ObjectActionError diff --git a/mogan/common/policy.py b/mogan/common/policy.py index 13495ad5..82ea712e 100644 --- a/mogan/common/policy.py +++ b/mogan/common/policy.py @@ -183,6 +183,9 @@ server_policies = [ policy.RuleDefault('mogan:server_group:delete', 'rule:default', description='Delete a server group'), + policy.RuleDefault('mogan:manageable_servers:get_all', + 'rule:admin_api', + description='Get manageable nodes from driver'), ] diff --git a/mogan/engine/api.py b/mogan/engine/api.py index 08aba407..6e43a828 100644 --- a/mogan/engine/api.py +++ b/mogan/engine/api.py @@ -588,3 +588,12 @@ class API(object): def list_node_aggregates(self, context, node): """Get the node aggregates list.""" return self.engine_rpcapi.list_node_aggregates(context, node) + + def get_manageable_servers(self, context): + """Get manageable servers list""" + mservers = [] + try: + mservers = self.engine_rpcapi.get_manageable_servers(context) + except Exception as e: + raise exception.GetManageableServersFailed(reason=e) + return mservers diff --git a/mogan/engine/manager.py b/mogan/engine/manager.py index 13c61b48..af6fbd74 100644 --- a/mogan/engine/manager.py +++ b/mogan/engine/manager.py @@ -605,3 +605,6 @@ class EngineManager(base_manager.BaseEngineManager): aggregates = self.scheduler_client.reportclient \ .get_aggregates_from_node(node) return aggregates + + def get_manageable_servers(self, context): + return self.driver.get_manageable_nodes() diff --git a/mogan/engine/rpcapi.py b/mogan/engine/rpcapi.py index b4e4258f..2cd77659 100644 --- a/mogan/engine/rpcapi.py +++ b/mogan/engine/rpcapi.py @@ -120,3 +120,7 @@ class EngineAPI(object): def list_node_aggregates(self, context, node): cctxt = self.client.prepare(topic=self.topic, server=CONF.host) return cctxt.call(context, 'list_node_aggregates', node=node) + + def get_manageable_servers(self, context): + cctxt = self.client.prepare(topic=self.topic, server=CONF.host) + return cctxt.call(context, 'get_manageable_servers') diff --git a/mogan/tests/unit/api/v1/test_manageable_servers.py b/mogan/tests/unit/api/v1/test_manageable_servers.py new file mode 100644 index 00000000..8542dcef --- /dev/null +++ b/mogan/tests/unit/api/v1/test_manageable_servers.py @@ -0,0 +1,48 @@ +# +# Copyright 2017 Fiberhome +# +# 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. + +import mock +from oslo_utils import uuidutils + +from mogan.tests.functional.api import v1 as v1_test + + +class TestManageableServers(v1_test.APITestV1): + + DENY_MESSAGE = "Access was denied to the following resource: mogan:%s" + + def setUp(self): + super(TestManageableServers, self).setUp() + self.project_id = "0abcdef1-2345-6789-abcd-ef123456abc1" + # evil_project is an wicked tenant, is used for unauthorization test. + self.evil_project = "0abcdef1-2345-6789-abcd-ef123456abc9" + + def test_server_get_manageable_servers_with_invalid_rule(self): + self.context.tenant = self.evil_project + headers = self.gen_headers(self.context, roles="no-admin") + resp = self.get_json('/manageable_servers', True, headers=headers) + error = self.parser_error_body(resp) + self.assertEqual(self.DENY_MESSAGE % 'manageable_servers:get_all', + error['faultstring']) + + @mock.patch('mogan.engine.api.API.get_manageable_servers') + def test_server_get_manageable_servers(self, mock_get): + mock_get.return_value = [{'uuid': uuidutils.generate_uuid(), + 'name': "test_node", + 'resource_class': "gold"}] + self.context.tenant = self.project_id + headers = self.gen_headers(self.context, roles="admin") + resp = self.get_json('/manageable_servers', headers=headers) + self.assertIn("uuid", resp['manageable_servers'][0]) diff --git a/mogan/tests/unit/engine/test_manager.py b/mogan/tests/unit/engine/test_manager.py index ccfa8108..84f8e39e 100644 --- a/mogan/tests/unit/engine/test_manager.py +++ b/mogan/tests/unit/engine/test_manager.py @@ -202,3 +202,21 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin, manager.EngineManager, self.context, server=server) self.assertFalse(called['fault_added']) + + @mock.patch.object(IronicDriver, 'get_manageable_nodes') + def test_get_manageable_servers_failed(self, get_manageable_mock): + get_manageable_mock.side_effect = exception.MoganException() + self._start_service() + self.assertRaises(exception.MoganException, + self.service.get_manageable_servers, + self.context) + self._stop_service() + get_manageable_mock.assert_called_once() + + @mock.patch.object(IronicDriver, 'get_manageable_nodes') + def test_get_manageable_servers(self, get_manageable_mock): + get_manageable_mock.return_value = {} + self._start_service() + self.service.get_manageable_servers(self.context) + self._stop_service() + get_manageable_mock.assert_called_once()