Add Host Reservation Admin API

Initial release of Host Reservation Admin API by adding new classes
to the current Lease API mapped to /os-hosts/ context.

The goal of this API is to be admin-only, defining CRUD methods for
adding a host to Compute, aiming to dedicate it for lease management.

As a result, the added host won't be available for booting VMs on it
unless Climate specifically picks it from a specific freepool to the
user' pcloud associated with the host reservation.

For testing, please patch RPC methods like this :
    call = lambda *args, **kwargs: True
    cast = lambda *args, **kwargs: True

TODO :
 - Add @policy.authorize() to climate.api.oshosts.service controllers
   once review 57200 (Policy mgmt) is merged

Implements bp:host-provisioning-api

Change-Id: I58d986fe8344b9578b2f15399ec19f7649cc3035
This commit is contained in:
sbauza 2013-10-17 16:10:27 +02:00 committed by sbauza
parent 9f63fe76ae
commit 8624ca116f
10 changed files with 329 additions and 0 deletions

View File

@ -19,6 +19,7 @@ from keystoneclient.middleware import auth_token
from oslo.config import cfg
from werkzeug import exceptions as werkzeug_exceptions
from climate.api.oshosts import v1_0 as host_api_v1_0
from climate.api import utils as api_utils
from climate.api import v1_0 as api_v1_0
from climate.openstack.common import log
@ -70,6 +71,13 @@ def make_app():
app.route('/', methods=['GET'])(version_list)
app.register_blueprint(api_v1_0.rest, url_prefix='/v1')
LOG.debug("List of plugins: %s", cfg.CONF.manager.plugins)
# TODO(sbauza) : Change this whole crap by removing hardcoded values and
# maybe using stevedore for achieving this
if cfg.CONF.manager.plugins \
and 'physical.host.plugin' in cfg.CONF.manager.plugins:
app.register_blueprint(host_api_v1_0.rest, url_prefix='/v1/os-hosts')
for code in werkzeug_exceptions.default_exceptions.iterkeys():
app.error_handler_spec[None][code] = make_json_error

View File

View File

@ -0,0 +1,75 @@
# Copyright (c) 2013 Bull.
#
# 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.
from climate import exceptions
from climate.manager.oshosts import rpcapi as manager_rpcapi
from climate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class API(object):
def __init__(self):
self.manager_rpcapi = manager_rpcapi.ManagerRPCAPI()
def get_computehosts(self):
"""List all existing computehosts."""
return self.manager_rpcapi.list_computehosts()
def create_computehost(self, data):
"""Create new computehost.
:param data: New computehost characteristics.
:type data: dict
"""
# here API should go to Keystone API v3 and create trust
trust = 'trust'
data.update({'trust': trust})
return self.manager_rpcapi.create_computehost(data)
def get_computehost(self, host_id):
"""Get computehost by its ID.
:param host_id: ID of the computehost in Climate DB.
:type host_id: str
"""
return self.manager_rpcapi.get_computehost(host_id)
def update_computehost(self, host_id, data):
"""Update computehost. Only name changing may be proceeded.
:param host_id: ID of the computehost in Climate DB.
:type host_id: str
:param data: New computehost characteristics.
:type data: dict
"""
new_name = data.pop('name', None)
if len(data) > 0:
raise exceptions.ClimateException('Only name changing may be '
'proceeded.')
data = {}
if new_name:
data['name'] = new_name
return self.manager_rpcapi.update_computehost(host_id, data)
def delete_computehost(self, host_id):
"""Delete specified computehost.
:param host_id: ID of the computehost in Climate DB.
:type host_id: str
"""
self.manager_rpcapi.delete_computehost(host_id)

View File

@ -0,0 +1,64 @@
# Copyright (c) 2013 Bull.
#
# 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.
from climate.api.oshosts import service
from climate.api import utils as api_utils
from climate.api import validation
from climate import utils
rest = api_utils.Rest('host_v1_0', __name__)
_api = utils.LazyProxy(service.API)
## Computehosts operations
@rest.get('')
def computehosts_list():
"""List all existing computehosts."""
return api_utils.render(hosts=_api.get_computehosts())
@rest.post('')
def computehosts_create(data):
"""Create new computehost."""
return api_utils.render(host=_api.create_computehost(data))
@rest.get('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_get(host_id):
"""Get computehost by its ID."""
return api_utils.render(host=_api.get_computehost(host_id))
@rest.put('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_update(host_id, data):
"""Update computehost. Only name changing may be proceeded.
"""
if len(data) == 0:
return api_utils.internal_error(status_code=400,
descr="No data to update")
else:
return api_utils.render(host=_api.update_computehost(host_id, data))
@rest.delete('/<host_id>')
@validation.check_exists(_api.get_computehost, host_id='host_id')
def computehosts_delete(host_id):
"""Delete specified computehost."""
_api.delete_computehost(host_id)
return api_utils.render()

View File

View File

@ -0,0 +1,56 @@
# Copyright (c) 2013 Bull.
#
# 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.
from oslo.config import cfg
from climate.utils import service
CONF = cfg.CONF
CONF.import_opt('rpc_topic', 'climate.manager.service', 'manager')
class ManagerRPCAPI(service.RpcProxy):
"""Client side for the Manager RPC API.
Used from other services to communicate with climate-manager service.
"""
BASE_RPC_API_VERSION = '1.0'
def __init__(self):
"""Initiate RPC API client with needed topic and RPC version."""
super(ManagerRPCAPI, self).__init__(
topic=CONF.manager.rpc_topic,
default_version=self.BASE_RPC_API_VERSION,
)
def get_computehost(self, host_id):
"""Get detailed info about some computehost."""
return self.call('get_computehost', host_id=host_id)
def list_computehosts(self):
"""List all computehosts."""
return self.call('list_computehosts')
def create_computehost(self, host_values):
"""Create computehost with specified parameters."""
return self.call('create_computehost', host_values=host_values)
def update_computehost(self, host_id, values):
"""Update computehost with passes values dictionary."""
return self.call('update_computehost', host_id=host_id, values=values)
def delete_computehost(self, host_id):
"""Delete specified computehost."""
return self.cast('delete_computehost', host_id=host_id)

View File

View File

@ -0,0 +1,46 @@
# Copyright (c) 2013 Bull.
#
# 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.
from climate.api.oshosts import service as service_api
from climate import tests
class RPCApiTestCase(tests.TestCase):
def setUp(self):
super(RPCApiTestCase, self).setUp()
self.s_api = service_api
self.fake_list = []
self.fake_computehost = {}
self.patch(self.s_api.API, "get_computehosts").\
return_value = self.fake_list
self.patch(self.s_api.API, "create_computehost").return_value = True
self.patch(self.s_api.API, "get_computehost").\
return_value = self.fake_computehost
self.patch(self.s_api.API, "update_computehost").return_value = True
self.patch(self.s_api.API, "delete_computehost").return_value = True
def test_get_computehost(self):
pass
def test_create_computehost(self):
pass
def test_update_computehost(self):
pass
def test_delete_computehost(self):
pass

View File

@ -0,0 +1,60 @@
# Copyright (c) 2013 Bull.
#
# 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.
from climate.api.oshosts import service as service_api
from climate.api.oshosts import v1_0 as api
from climate.api import utils as utils_api
from climate import tests
class RESTApiTestCase(tests.TestCase):
def setUp(self):
super(RESTApiTestCase, self).setUp()
self.api = api
self.u_api = utils_api
self.s_api = service_api
self.render = self.patch(self.u_api, "render")
self.get_computehosts = self.patch(self.s_api.API,
'get_computehosts')
self.create_computehost = self.patch(self.s_api.API,
'create_computehost')
self.get_computehost = self.patch(self.s_api.API, 'get_computehost')
self.update_computehost = self.patch(self.s_api.API,
'update_computehost')
self.delete_computehost = self.patch(self.s_api.API,
'delete_computehost')
self.fake_id = '1'
def test_computehost_list(self):
self.api.computehosts_list()
self.render.assert_called_once_with(hosts=self.get_computehosts())
def test_computehosts_create(self):
self.api.computehosts_create(data=None)
self.render.assert_called_once_with(host=self.create_computehost())
def test_computehosts_get(self):
self.api.computehosts_get(host_id=self.fake_id)
self.render.assert_called_once_with(host=self.get_computehost())
def test_computehosts_update(self):
self.api.computehosts_update(host_id=self.fake_id, data=self.fake_id)
self.render.assert_called_once_with(host=self.update_computehost())
def test_computehosts_delete(self):
self.api.computehosts_delete(host_id=self.fake_id)
self.render.assert_called_once()

View File

@ -15,9 +15,11 @@
import flask
from keystoneclient.middleware import auth_token
from oslo.config import cfg
from werkzeug import exceptions as werkzeug_exceptions
from climate.api import app
from climate.api.oshosts import v1_0 as host_api_v1_0
from climate.api import utils as api_utils
from climate import tests
@ -69,3 +71,21 @@ class AppTestCase(tests.TestCase):
auth_version='v2.0',
admin_password='climate',
auth_host='127.0.0.1')
class AppTestCaseForHostsPlugin(tests.TestCase):
def setUp(self):
super(AppTestCaseForHostsPlugin, self).setUp()
cfg.CONF.set_override('plugins', ['physical.host.plugin'], 'manager')
self.app = app
self.host_api_v1_0 = host_api_v1_0
self.flask = flask
self.fake_blueprint = self.patch(self.flask.Flask,
'register_blueprint')
def test_make_app_with_host_plugin(self):
self.app.make_app()
self.fake_blueprint.assert_called_with(self.host_api_v1_0.rest,
url_prefix='/v1/os-hosts')