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:
parent
77d19eceb7
commit
d577c88d4a
40
api-ref/source/v1/manageable_servers.inc
Normal file
40
api-ref/source/v1/manageable_servers.inc
Normal 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
|
@ -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``.
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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):
|
||||
|
86
mogan/api/controllers/v1/manageable_servers.py
Normal file
86
mogan/api/controllers/v1/manageable_servers.py
Normal 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)
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
48
mogan/tests/unit/api/v1/test_manageable_servers.py
Normal file
48
mogan/tests/unit/api/v1/test_manageable_servers.py
Normal 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])
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user