Reservation extensions to Nova API
* ReservationController: sends lease creation request to Climate if instance should be reserved. * DefaultReservationController: adds hints to server creation request if every VM should be reserved for some amount of time. Implements: blueprint nova-api-extensions Change-Id: I4aee23c5d8c76ee6f1cb0fee1bc54e784a83345d
This commit is contained in:
parent
66410039b7
commit
7a6bca02c3
|
@ -1,4 +1,6 @@
|
|||
climate-nova
|
||||
============
|
||||
|
||||
Climate Nova filter
|
||||
Climate Nova related changes. Includes filters for Host
|
||||
Reservation and Nova API extensions for the VM reservation
|
||||
feature.
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
# 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.openstack.common import log as logging
|
||||
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)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
authorize = extensions.extension_authorizer('compute', 'default_reservation')
|
||||
|
||||
|
||||
class DefaultReservationController(wsgi.Controller):
|
||||
"""Add default reservation flags to every VM started."""
|
||||
|
||||
@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 'server' in body:
|
||||
scheduler_hints = body['server'].get('scheduler_hints', {})
|
||||
elif '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 'server' in body:
|
||||
if 'scheduler_hints' in body['server']:
|
||||
body['server']['scheduler_hints'].update(default_hints)
|
||||
else:
|
||||
body['server']['scheduler_hints'] = default_hints
|
||||
else:
|
||||
attr = 'OS-SCH-HNT:scheduler_hints'
|
||||
if 'os:scheduler_hints' in body:
|
||||
body['os:scheduler_hints'].update(default_hints)
|
||||
elif attr in body and 'lease_params' not in body[attr]:
|
||||
body[attr].update(default_hints)
|
||||
yield
|
||||
|
||||
|
||||
class Default_reservation(extensions.ExtensionDescriptor):
|
||||
"""Instance reservation system."""
|
||||
|
||||
name = "DefaultReservation"
|
||||
alias = "os-default-instance-reservation"
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = DefaultReservationController()
|
||||
extension = extensions.ControllerExtension(self, 'servers', controller)
|
||||
return [extension]
|
|
@ -0,0 +1,184 @@
|
|||
# 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 Climate. If finds, create instance, shelve it not to use compute
|
||||
capacity while instance is not used and sent lease creation request to Climate.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from climateclient import client as climate_client
|
||||
except ImportError:
|
||||
climate_client = None
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova.openstack.common.gettextutils import _ # noqa
|
||||
from nova.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
authorize = extensions.extension_authorizer('compute', 'reservation')
|
||||
|
||||
|
||||
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 Climate usage for Nova VMs."""
|
||||
|
||||
scheduler_hints = body.get('server', {}).get('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 Climate
|
||||
# this operation should be last, because otherwise Climate
|
||||
# Manager may try unshelve instance when it's still active
|
||||
climate_cl = self.get_climate_client(service_catalog,
|
||||
user_roles, auth_token)
|
||||
lease_transaction.set_params(instance_id, climate_cl, nova_ctx)
|
||||
lease = climate_cl.lease.create(**lease_params)
|
||||
|
||||
try:
|
||||
lease_transaction.set_lease_id(lease['id'])
|
||||
except Exception:
|
||||
raise exception.InternalError(
|
||||
_('Lease creation request failed.')
|
||||
)
|
||||
|
||||
def get_climate_client(self, catalog, user_roles, auth_token):
|
||||
|
||||
if not climate_client:
|
||||
raise ImportError(_('No Climate client installed to the '
|
||||
'environment. Please install it to use '
|
||||
'reservation for Nova instances.'))
|
||||
|
||||
climate_endpoints = None
|
||||
for service in catalog:
|
||||
if service['type'] == 'reservation':
|
||||
climate_endpoints = service['endpoints'][0]
|
||||
if not climate_endpoints:
|
||||
raise exception.NotFound(_('No Climate endpoint found in service '
|
||||
'catalog.'))
|
||||
|
||||
climate_url = None
|
||||
if 'admin' in user_roles:
|
||||
climate_url = climate_endpoints.get('adminURL')
|
||||
if climate_url is None:
|
||||
climate_url = climate_endpoints.get('publicURL')
|
||||
if climate_url is None:
|
||||
raise exception.NotFound(_('No Climate URL found in service '
|
||||
'catalog.'))
|
||||
|
||||
climate_cl = climate_client.Client(climate_url=climate_url,
|
||||
auth_token=auth_token)
|
||||
return climate_cl
|
||||
|
||||
|
||||
class LeaseTransaction(object):
|
||||
def __init__(self):
|
||||
self.lease_id = None
|
||||
self.instance_id = None
|
||||
self.climate_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.climate_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, climate_cl, nova_ctx):
|
||||
self.instance_id = instance_id
|
||||
self.climate_cl = climate_cl
|
||||
self.nova_ctx = nova_ctx
|
||||
|
||||
|
||||
class Reservation(extensions.ExtensionDescriptor):
|
||||
"""Instance reservation system."""
|
||||
|
||||
name = "Reservation"
|
||||
alias = "os-instance-reservation"
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = ReservationController()
|
||||
extension = extensions.ControllerExtension(self, 'servers', controller)
|
||||
return [extension]
|
|
@ -0,0 +1,110 @@
|
|||
# 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
|
||||
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
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 import utils
|
||||
from oslo.config import cfg
|
||||
|
||||
|
||||
UUID = fakes.FAKE_UUID
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib')
|
||||
CONF.import_opt('reservation_start_date',
|
||||
'climatenova.api.extensions.default_reservation')
|
||||
CONF.import_opt('reservation_length_hours',
|
||||
'climatenova.api.extensions.default_reservation')
|
||||
CONF.import_opt('reservation_length_days',
|
||||
'climatenova.api.extensions.default_reservation')
|
||||
CONF.import_opt('reservation_length_minutes',
|
||||
'climatenova.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.flags(
|
||||
osapi_compute_extension=[
|
||||
'nova.api.openstack.compute.contrib.select_extensions',
|
||||
'climatenova.api.extensions.default_reservation.'
|
||||
'Default_reservation',
|
||||
'climatenova.api.extensions.reservation.Reservation'
|
||||
],
|
||||
osapi_compute_ext_list=['Scheduler_hints'])
|
||||
|
||||
self.lease_controller = mock.MagicMock()
|
||||
self.lease_controller.create = mock.MagicMock()
|
||||
self.mock_client = mock.MagicMock()
|
||||
self.mock_client.lease = self.lease_controller
|
||||
|
||||
self.req = fakes.HTTPRequest.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.APIRouter(init_only=('servers',))
|
||||
self.stubs.Set(utils, 'generate_uid', lambda name, size: self.l_name)
|
||||
self.stubs.Set(compute_api.API, 'create', self._fake_create)
|
||||
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_create(self, *args, **kwargs):
|
||||
return [self.fake_instance], ''
|
||||
|
||||
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'
|
|
@ -0,0 +1,98 @@
|
|||
# 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 nova.openstack.common import jsonutils
|
||||
|
||||
from climatenova.tests.api import extensions
|
||||
|
||||
|
||||
class ClimateDefaultReservationTestCase(extensions.BaseExtensionTestCase):
|
||||
"""Climate 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 Climate.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up testing environment."""
|
||||
super(ClimateDefaultReservationTestCase, self).setUp()
|
||||
|
||||
@mock.patch('climatenova.api.extensions.reservation.climate_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 Climate 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)
|
||||
res = self.req.get_response(self.app)
|
||||
|
||||
mock_module.Client.assert_called_once_with(climate_url='fake',
|
||||
auth_token='fake_token')
|
||||
self.lease_controller.create.assert_called_once_with(
|
||||
reservations=[
|
||||
{'resource_type': 'virtual:instance',
|
||||
'resource_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'}],
|
||||
end=self.default_lease_end,
|
||||
events=[],
|
||||
start='now',
|
||||
name='lease_123')
|
||||
|
||||
self.assertEqual(202, res.status_int)
|
||||
|
||||
@mock.patch('climatenova.api.extensions.reservation.climate_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 Climate
|
||||
|
||||
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)
|
||||
res = self.req.get_response(self.app)
|
||||
|
||||
mock_module.Client.assert_called_once_with(climate_url='fake',
|
||||
auth_token='fake_token')
|
||||
self.lease_controller.create.assert_called_once_with(
|
||||
reservations=[
|
||||
{'resource_type': 'virtual:instance',
|
||||
'resource_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'}],
|
||||
end=self.default_lease_end,
|
||||
events=[],
|
||||
start='now',
|
||||
name='other_name')
|
||||
|
||||
self.assertEqual(202, res.status_int)
|
|
@ -0,0 +1,72 @@
|
|||
# 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 nova.openstack.common import jsonutils
|
||||
|
||||
from climatenova.tests.api import extensions
|
||||
|
||||
|
||||
class ClimateReservationTestCase(extensions.BaseExtensionTestCase):
|
||||
"""Climate 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 Climate.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up testing environment."""
|
||||
super(ClimateReservationTestCase, self).setUp()
|
||||
self.flags(
|
||||
osapi_compute_extension=[
|
||||
'nova.api.openstack.compute.contrib.select_extensions',
|
||||
'climatenova.api.extensions.reservation.Reservation'
|
||||
],
|
||||
osapi_compute_ext_list=['Scheduler_hints'])
|
||||
|
||||
@mock.patch('climatenova.api.extensions.reservation.climate_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)
|
||||
res = self.req.get_response(self.app)
|
||||
|
||||
mock_module.Client.assert_called_once_with(climate_url='fake',
|
||||
auth_token='fake_token')
|
||||
self.lease_controller.create.assert_called_once_with(
|
||||
reservations=[
|
||||
{'resource_type': 'virtual:instance',
|
||||
'resource_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'}],
|
||||
end='2014-02-10 12:00',
|
||||
events=[],
|
||||
start='2014-02-09 12:00',
|
||||
name='some_name')
|
||||
|
||||
self.assertEqual(202, res.status_int)
|
|
@ -2,3 +2,7 @@
|
|||
scheduler_available_filters = nova.scheduler.filters.all_filters
|
||||
scheduler_available_filters = climatenova.scheduler.filters.climate_filter.ClimateFilter
|
||||
scheduler_default_filters=RetryFilter,AvailabilityZoneFilter,RamFilter,ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,ClimateFilter
|
||||
|
||||
osapi_compute_extension = nova.api.openstack.compute.contrib.standard_extensions
|
||||
osapi_compute_extension = climatenova.api.extensions.default_reservation.Default_reservation
|
||||
osapi_compute_extension = climatenova.api.extensions.reservation.Reservation
|
||||
|
|
|
@ -10,6 +10,6 @@ sphinxcontrib-httpdomain
|
|||
discover
|
||||
fixtures>=0.3.14
|
||||
testrepository>=0.0.17
|
||||
testtools>=0.9.32
|
||||
testtools>=0.9.34
|
||||
|
||||
http://tarballs.openstack.org/nova/nova-master.tar.gz#egg=nova
|
||||
|
|
Loading…
Reference in New Issue