diff --git a/README.rst b/README.rst index 0ac4ae8..5b29409 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,6 @@ blazar-nova ============ -Blazar Nova related changes. Includes filters for Host reservation and Nova API -extensions for the instance reservation feature. +This package contains the Blazar filter - a Nova scheduler filter used for the +host reservation feature. -NOTE: The instance reservation feature does not work currently because it -depends on Nova API extensions. Support for these extensions was completely -removed from Nova in Newton. Expect the instance reservation feature to be -implemented differently in a future release. diff --git a/blazarnova/api/__init__.py b/blazarnova/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/blazarnova/api/extensions/__init__.py b/blazarnova/api/extensions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/blazarnova/api/extensions/default_reservation.py b/blazarnova/api/extensions/default_reservation.py deleted file mode 100644 index 7387b14..0000000 --- a/blazarnova/api/extensions/default_reservation.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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. - -""" -Default reservation extension may be used if there is need to reserve all -VMs by default (like in the developer clouds: every VM may be not just booted, -but reserved for some amount of time, after which VM should be deleted or -suspended or whatever). -""" - -import datetime -import json - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova import compute -from nova import utils -from oslo_config import cfg -from webob import exc - - -reservation_opts = [ - cfg.StrOpt('reservation_start_date', - default='now', - help='Specify date for all leases to be started for every VM'), - cfg.IntOpt('reservation_length_days', - default=30, - help='Number of days for VM to be reserved.'), - cfg.IntOpt('reservation_length_hours', - default=0, - help='Number of hours for VM to be reserved.'), - cfg.IntOpt('reservation_length_minutes', - default=0, - help='Number of minutes for VM to be reserved.') -] - -CONF = cfg.CONF -CONF.register_opts(reservation_opts) - - -class DefaultReservationController(wsgi.Controller): - """Add default reservation flags to every VM started.""" - - def __init__(self, *args, **kwargs): - super(DefaultReservationController, self).__init__(*args, **kwargs) - self.compute_api = compute.API() - - @wsgi.extends - def create(self, req, body): - """Add additional hints to the create server request. - - They will be managed by Reservation Extension. - """ - - if not self.is_valid_body(body, 'server'): - raise exc.HTTPUnprocessableEntity() - - if 'os:scheduler_hints' in body: - scheduler_hints = body['os:scheduler_hints'] - else: - scheduler_hints = body.get('OS-SCH-HNT:scheduler_hints', {}) - - lease_params = json.loads(scheduler_hints.get('lease_params', '{}')) - - delta_days = datetime.timedelta(days=CONF.reservation_length_days) - delta_hours = datetime.timedelta(hours=CONF.reservation_length_hours) - delta_minutes = datetime.timedelta( - minutes=CONF.reservation_length_minutes - ) - - if CONF.reservation_start_date == 'now': - base = datetime.datetime.utcnow() - else: - base = datetime.datetime.strptime(CONF.reservation_start_date, - "%Y-%m-%d %H:%M") - - lease_params.setdefault('name', utils.generate_uid('lease', size=6)) - lease_params.setdefault('start', CONF.reservation_start_date) - lease_params.setdefault( - 'end', (base + delta_days + delta_hours + delta_minutes). - strftime('%Y-%m-%d %H:%M')) - - default_hints = {'lease_params': json.dumps(lease_params)} - - if 'os:scheduler_hints' in body: - body['os:scheduler_hints'].update(default_hints) - elif 'OS-SCH-HNT:scheduler_hints' in body: - body['OS-SCH-HNT:scheduler_hints'].update(default_hints) - else: - body['os:scheduler_hints'] = default_hints - - -class Default_reservation(extensions.V21APIExtensionBase): - """Instance reservation system.""" - - name = "DefaultReservation" - alias = "os-default-instance-reservation" - updated = "2016-11-30T00:00:00Z" - version = 1 - - def get_controller_extensions(self): - return [] - - def get_resources(self): - controller = DefaultReservationController() - extension = extensions.ResourceExtension( - self.alias, controller, member_actions={"action": "POST"}) - return [extension] diff --git a/blazarnova/api/extensions/reservation.py b/blazarnova/api/extensions/reservation.py deleted file mode 100644 index f4e7191..0000000 --- a/blazarnova/api/extensions/reservation.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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. - -""" -Reservation extension parses instance creation request to find hints referring -to use Blazar. If finds, create instance, shelve it not to use compute -capacity while instance is not used and sent lease creation request to Blazar. -""" - -import json -import time -import traceback - -try: - from blazarclient import client as blazar_client -except ImportError: - blazar_client = None - -from blazarnova.i18n import _ # noqa -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova import compute -from nova import exception -from oslo_log import log as logging - - -LOG = logging.getLogger(__name__) - - -class ReservationController(wsgi.Controller): - """Reservation controller to support VMs wake up.""" - def __init__(self, *args, **kwargs): - super(ReservationController, self).__init__(*args, **kwargs) - self.compute_api = compute.API() - - @wsgi.extends - def create(self, req, resp_obj, body): - """Support Blazar usage for Nova VMs.""" - - if 'os:scheduler_hints' in body: - scheduler_hints = body['os:scheduler_hints'] - else: - scheduler_hints = body.get('OS-SCH-HNT:scheduler_hints', {}) - - lease_params = scheduler_hints.get('lease_params', {}) - - if lease_params: - try: - lease_params = json.loads(lease_params) - except ValueError: - raise ValueError(_('Wrong JSON format for lease parameters ' - 'passed %s') % lease_params) - - if 'events' not in lease_params: - lease_params.update({'events': []}) - - instance_id = resp_obj.obj['server']['id'] - lease_params.update({ - 'reservations': [{'resource_id': instance_id, - 'resource_type': 'virtual:instance'}], - }) - - service_catalog = json.loads(req.environ['HTTP_X_SERVICE_CATALOG']) - user_roles = req.environ['HTTP_X_ROLES'].split(',') - auth_token = req.environ['HTTP_X_AUTH_TOKEN'] - nova_ctx = req.environ["nova.context"] - instance = self.compute_api.get(nova_ctx, instance_id, - want_objects=True) - - lease_transaction = LeaseTransaction() - - with lease_transaction: - while instance.vm_state != 'active': - if instance.vm_state == 'error': - raise exception.InstanceNotRunning( - _("Instance %s responded with error state") % - instance_id - ) - instance = self.compute_api.get(nova_ctx, instance_id, - want_objects=True) - time.sleep(1) - self.compute_api.shelve(nova_ctx, instance) - - # shelve instance - while instance.vm_state != 'shelved_offloaded': - instance = self.compute_api.get(nova_ctx, instance_id, - want_objects=True) - time.sleep(1) - - # send lease creation request to Blazar - # this operation should be last, because otherwise Blazar - # Manager may try unshelve instance when it's still active - blazar_cl = self.get_blazar_client(service_catalog, - user_roles, auth_token) - lease_transaction.set_params(instance_id, blazar_cl, nova_ctx) - lease = blazar_cl.lease.create(**lease_params) - - try: - lease_transaction.set_lease_id(lease['id']) - except Exception: - raise exception.InternalError( - _('Lease creation request failed.') - ) - - def get_blazar_client(self, catalog, user_roles, auth_token): - if not blazar_client: - raise ImportError(_('No Blazar client installed to the ' - 'environment. Please install it to use ' - 'reservation for Nova instances.')) - - blazar_endpoints = None - for service in catalog: - if service['type'] == 'reservation': - blazar_endpoints = service['endpoints'][0] - if not blazar_endpoints: - raise exception.NotFound(_('No Blazar endpoint found in service ' - 'catalog.')) - - blazar_url = None - if 'admin' in user_roles: - blazar_url = blazar_endpoints.get('adminURL') - if blazar_url is None: - blazar_url = blazar_endpoints.get('publicURL') - if blazar_url is None: - raise exception.NotFound(_('No Blazar URL found in service ' - 'catalog.')) - - blazar_cl = blazar_client.Client(blazar_url=blazar_url, - auth_token=auth_token) - return blazar_cl - - -class LeaseTransaction(object): - def __init__(self): - self.lease_id = None - self.instance_id = None - self.blazar_cl = None - self.nova_ctx = None - - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_value, exc_traceback): - if exc_type and self.instance_id: - msg = '\n'.join(traceback.format_exception( - exc_type, exc_value, exc_traceback)) - LOG.error(_('Error occurred while lease creation. ' - 'Traceback: \n%s') % msg) - if self.lease_id: - self.blazar_cl.lease.delete(self.lease_id) - - api = compute.API() - api.delete(self.nova_ctx, api.get(self.nova_ctx, self.instance_id, - want_objects=True)) - - def set_lease_id(self, lease_id): - self.lease_id = lease_id - - def set_params(self, instance_id, blazar_cl, nova_ctx): - self.instance_id = instance_id - self.blazar_cl = blazar_cl - self.nova_ctx = nova_ctx - - -class Reservation(extensions.V21APIExtensionBase): - """Instance reservation system.""" - - name = "Reservation" - alias = "os-instance-reservation" - updated = "2016-11-30T00:00:00Z" - version = 1 - - def get_controller_extensions(self): - controller = ReservationController() - extension = extensions.ControllerExtension(self, 'servers', controller) - return [extension] - - def get_resources(self): - return [] diff --git a/blazarnova/tests/api/__init__.py b/blazarnova/tests/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/blazarnova/tests/api/extensions/__init__.py b/blazarnova/tests/api/extensions/__init__.py deleted file mode 100644 index 9a9856e..0000000 --- a/blazarnova/tests/api/extensions/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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 datetime - -import mock - -from nova.api.openstack import compute -from nova.compute import api as compute_api -from nova import context -from nova import test -from nova.tests.unit.api.openstack import fakes -from nova import utils -from oslo_config import cfg - - -UUID = fakes.FAKE_UUID -CONF = cfg.CONF -CONF.import_opt('reservation_start_date', - 'blazarnova.api.extensions.default_reservation') -CONF.import_opt('reservation_length_hours', - 'blazarnova.api.extensions.default_reservation') -CONF.import_opt('reservation_length_days', - 'blazarnova.api.extensions.default_reservation') -CONF.import_opt('reservation_length_minutes', - 'blazarnova.api.extensions.default_reservation') - - -class InstanceWrapper(object): - # this wrapper is needed to make dict look like object with fields and - # dictionary at the same time - - def __init__(self, dct): - self._dct = dct - - def __getattr__(self, item): - return self._dct.get(item) - - __getitem__ = __getattr__ - - -class BaseExtensionTestCase(test.TestCase): - - def setUp(self): - """Set up testing environment.""" - super(BaseExtensionTestCase, self).setUp() - self.fake_instance = fakes.stub_instance(1, uuid=UUID) - - self.lease_controller = mock.MagicMock() - self.mock_client = mock.MagicMock() - self.mock_client.lease = self.lease_controller - - self.req = fakes.HTTPRequestV21.blank('/fake/servers') - self.req.method = 'POST' - self.req.content_type = 'application/json' - self.req.environ.update({ - 'HTTP_X_SERVICE_CATALOG': '[{"type": "reservation", ' - '"endpoints": [{"publicURL": "fake"}]}]', - 'HTTP_X_ROLES': '12,34', - 'HTTP_X_AUTH_TOKEN': 'fake_token', - 'nova.context': context.RequestContext('fake', 'fake'), - }) - - self.l_name = 'lease_123' - self.app = compute.APIRouterV21() - self.stubs.Set(utils, 'generate_uid', lambda name, size: self.l_name) - self.stubs.Set(compute_api.API, 'get', self._fake_get) - self.stubs.Set(compute_api.API, 'shelve', self._fake_shelve) - - delta_days = datetime.timedelta(days=CONF.reservation_length_days) - delta_hours = datetime.timedelta(hours=CONF.reservation_length_hours) - delta_minutes = datetime.timedelta( - minutes=CONF.reservation_length_minutes) - self.delta = delta_days + delta_hours + delta_minutes - lease_end = datetime.datetime.utcnow() + self.delta - self.default_lease_end = lease_end.strftime('%Y-%m-%d %H:%M') - self.default_lease_start = 'now' - - def _fake_get(self, *args, **kwargs): - self.fake_instance['vm_state'] = 'active' - return InstanceWrapper(self.fake_instance) - - def _fake_shelve(self, *args): - self.fake_instance['vm_state'] = 'shelved_offloaded' diff --git a/blazarnova/tests/api/extensions/test_default_reservation.py b/blazarnova/tests/api/extensions/test_default_reservation.py deleted file mode 100644 index 445e25f..0000000 --- a/blazarnova/tests/api/extensions/test_default_reservation.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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 blazarnova.api.extensions import default_reservation -from blazarnova.api.extensions import reservation -from blazarnova.tests.api import extensions -from nova.api.openstack import wsgi -from oslo_serialization import jsonutils - - -class BlazarDefaultReservationTestCase(extensions.BaseExtensionTestCase): - """Blazar API extensions test case. - - This test case provides tests for Default_reservation extension working - together with Reservation extension passing hints to Nova and - sending lease creation request to Blazar. - """ - - def setUp(self): - """Set up testing environment.""" - super(BlazarDefaultReservationTestCase, self).setUp() - self.rsrv_controller = reservation.ReservationController() - self.default_rsrv_controller = default_reservation\ - .DefaultReservationController() - - @mock.patch('blazarnova.api.extensions.reservation.blazar_client') - def test_create_with_default(self, mock_module): - """Test extension work with default lease parameters.""" - - mock_module.Client.return_value = self.mock_client - - # here we set no Blazar related hints, so all lease info should be - # default one - dates got from CONF and auto generated name - body = { - 'server': { - 'name': 'server_test', - 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', - 'flavorRef': '1', - } - } - - self.req.body = jsonutils.dumps(body) - self.default_rsrv_controller.create(self.req, body) - resp_obj = wsgi.ResponseObject({'server': {'id': 'fakeId'}}) - self.rsrv_controller.create(self.req, resp_obj, body) - - mock_module.Client.assert_called_once_with(blazar_url='fake', - auth_token='fake_token') - - self.lease_controller.create.assert_called_once_with( - reservations=[ - {'resource_type': 'virtual:instance', - 'resource_id': 'fakeId'}], - end=self.default_lease_end, - events=[], - start='now', - name=self.l_name) - - @mock.patch('blazarnova.api.extensions.reservation.blazar_client') - def test_create_with_passed_args(self, mock_module): - """Test extension work if some lease param would be passed.""" - # here we pass non default lease name to Nova - # Default_reservation extension should not rewrite it, - # Reservation extension should pass it to Blazar - - mock_module.Client.return_value = self.mock_client - - body = { - 'server': { - 'name': 'server_test', - 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', - 'flavorRef': '1', - }, - 'os:scheduler_hints': {'lease_params': '{"name": "other_name"}'}, - } - - self.req.body = jsonutils.dumps(body) - self.default_rsrv_controller.create(self.req, body) - resp_obj = wsgi.ResponseObject({'server': {'id': 'fakeId'}}) - self.rsrv_controller.create(self.req, resp_obj, body) - - mock_module.Client.assert_called_once_with(blazar_url='fake', - auth_token='fake_token') - - self.lease_controller.create.assert_called_once_with( - reservations=[ - {'resource_type': 'virtual:instance', - 'resource_id': 'fakeId'}], - end=self.default_lease_end, - events=[], - start='now', - name='other_name') diff --git a/blazarnova/tests/api/extensions/test_reservation.py b/blazarnova/tests/api/extensions/test_reservation.py deleted file mode 100644 index 4577baf..0000000 --- a/blazarnova/tests/api/extensions/test_reservation.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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 blazarnova.api.extensions import reservation -from blazarnova.tests.api import extensions -from nova.api.openstack import wsgi -from oslo_serialization import jsonutils - - -class BlazarReservationTestCase(extensions.BaseExtensionTestCase): - """Blazar API extensions test case. - - This test case provides tests for Reservation extension working - together with Reservation extension passing hints to Nova and - sending lease creation request to Blazar. - """ - - def setUp(self): - """Set up testing environment.""" - super(BlazarReservationTestCase, self).setUp() - self.controller = reservation.ReservationController() - - @mock.patch('blazarnova.api.extensions.reservation.blazar_client') - def test_create(self, mock_module): - """Test extension work with passed lease parameters.""" - - mock_module.Client.return_value = self.mock_client - - body = { - 'server': { - 'name': 'server_test', - 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', - 'flavorRef': '1', - }, - 'os:scheduler_hints': { - 'lease_params': '{"name": "some_name", ' - '"start": "2014-02-09 12:00", ' - '"end": "2014-02-10 12:00"}'} - } - - self.req.body = jsonutils.dumps(body) - resp_obj = wsgi.ResponseObject({'server': {'id': 'fakeId'}}) - self.controller.create(self.req, resp_obj, body) - - mock_module.Client.assert_called_once_with(blazar_url='fake', - auth_token='fake_token') - - self.lease_controller.create.assert_called_once_with( - reservations=[ - {'resource_type': 'virtual:instance', - 'resource_id': 'fakeId'}], - end='2014-02-10 12:00', - events=[], - start='2014-02-09 12:00', - name='some_name') diff --git a/requirements.txt b/requirements.txt index a861191..9dbc011 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -kombu!=4.0.2,>=4.0.0 # BSD -