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
This commit is contained in:
wanghao 2017-07-03 14:28:04 +08:00
parent 77d19eceb7
commit d577c88d4a
14 changed files with 426 additions and 0 deletions

View File

@ -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

View File

@ -425,6 +425,60 @@ lock_state:
in: body in: body
required: true required: true
type: boolean 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: max_count_body:
description: | description: |
The max number of servers to be created. Defaults to the value of ``min_count``. The max number of servers to be created. Defaults to the value of ``min_count``.

View File

@ -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"
}
]
}

View File

@ -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 availability_zones
from mogan.api.controllers.v1 import flavors from mogan.api.controllers.v1 import flavors
from mogan.api.controllers.v1 import keypairs 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 nodes
from mogan.api.controllers.v1 import server_groups from mogan.api.controllers.v1 import server_groups
from mogan.api.controllers.v1 import servers from mogan.api.controllers.v1 import servers
@ -62,6 +63,9 @@ class V1(base.APIBase):
server_groups = [link.Link] server_groups = [link.Link]
"""Links to the server groups resource""" """Links to the server groups resource"""
manageable_servers = [link.Link]
"""Links to the manageable servers resource"""
@staticmethod @staticmethod
def convert(): def convert():
v1 = V1() v1 = V1()
@ -120,6 +124,14 @@ class V1(base.APIBase):
'server_groups', '', 'server_groups', '',
bookmark=True) 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 return v1
@ -133,6 +145,7 @@ class Controller(rest.RestController):
aggregates = aggregates.AggregateController() aggregates = aggregates.AggregateController()
nodes = nodes.NodeController() nodes = nodes.NodeController()
server_groups = server_groups.ServerGroupController() server_groups = server_groups.ServerGroupController()
manageable_servers = manageable_servers.ManageableServersController()
@expose.expose(V1) @expose.expose(V1)
def get(self): def get(self):

View File

@ -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)

View File

@ -137,6 +137,13 @@ class BaseEngineDriver(object):
""" """
raise NotImplementedError() 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): def load_engine_driver(engine_driver):
"""Load a engine driver module. """Load a engine driver module.

View File

@ -44,6 +44,10 @@ _NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state',
'target_provision_state', 'last_error', 'maintenance', 'target_provision_state', 'last_error', 'maintenance',
'properties', 'instance_uuid') 'properties', 'instance_uuid')
TENANT_VIF_KEY = 'tenant_vif_port_id'
VIF_KEY = 'vif_port_id'
def map_power_state(state): def map_power_state(state):
try: try:
@ -99,6 +103,37 @@ class IronicDriver(base_driver.BaseEngineDriver):
except ironic_exc.NotFound: except ironic_exc.NotFound:
raise exception.ServerNotFound(server=server.uuid) 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): def _add_server_info_to_node(self, node, server):
patch = list() patch = list()
# Associate the node with a server # Associate the node with a server
@ -356,6 +391,67 @@ class IronicDriver(base_driver.BaseEngineDriver):
LOG.info('Successfully unprovisioned Ironic node %s', LOG.info('Successfully unprovisioned Ironic node %s',
node.uuid, server=server) 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): def get_maintenance_node_list(self):
"""Helper function to return the list of maintenance nodes. """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 == return (not node.instance_uuid and node.provision_state ==
ironic_states.AVAILABLE) 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

View File

@ -464,4 +464,8 @@ class ServerGroupNotFound(NotFound):
class ServerGroupExists(Conflict): class ServerGroupExists(Conflict):
_msg_fmt = _("Sever group %(group_uuid)s already exists.") _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 ObjectActionError = obj_exc.ObjectActionError

View File

@ -183,6 +183,9 @@ server_policies = [
policy.RuleDefault('mogan:server_group:delete', policy.RuleDefault('mogan:server_group:delete',
'rule:default', 'rule:default',
description='Delete a server group'), description='Delete a server group'),
policy.RuleDefault('mogan:manageable_servers:get_all',
'rule:admin_api',
description='Get manageable nodes from driver'),
] ]

View File

@ -588,3 +588,12 @@ class API(object):
def list_node_aggregates(self, context, node): def list_node_aggregates(self, context, node):
"""Get the node aggregates list.""" """Get the node aggregates list."""
return self.engine_rpcapi.list_node_aggregates(context, node) 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

View File

@ -605,3 +605,6 @@ class EngineManager(base_manager.BaseEngineManager):
aggregates = self.scheduler_client.reportclient \ aggregates = self.scheduler_client.reportclient \
.get_aggregates_from_node(node) .get_aggregates_from_node(node)
return aggregates return aggregates
def get_manageable_servers(self, context):
return self.driver.get_manageable_nodes()

View File

@ -120,3 +120,7 @@ class EngineAPI(object):
def list_node_aggregates(self, context, node): def list_node_aggregates(self, context, node):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host) cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'list_node_aggregates', node=node) 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')

View File

@ -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])

View File

@ -202,3 +202,21 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin,
manager.EngineManager, self.context, server=server) manager.EngineManager, self.context, server=server)
self.assertFalse(called['fault_added']) 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()