Add instance reservation plugin
This patch adds a new instance plugin. It is a first cut for the plugin. Currently, the plugin only creates reservations but doesn't handle starting and ending reservations. Partially implements: blueprint new-instance-reservation Change-Id: I60d93829bd05f0be48ba07b01f76a1f6bf190cf8
This commit is contained in:
parent
1bedac5001
commit
b835f311a1
|
@ -57,6 +57,19 @@ def _get_leases_from_host_id(host_id, start_date, end_date):
|
|||
yield lease
|
||||
|
||||
|
||||
def get_reservations_by_host_id(host_id, start_date, end_date):
|
||||
session = get_session()
|
||||
border0 = sa.and_(models.Lease.start_date < start_date,
|
||||
models.Lease.end_date < start_date)
|
||||
border1 = sa.and_(models.Lease.start_date > end_date,
|
||||
models.Lease.end_date > end_date)
|
||||
query = (session.query(models.Reservation).join(models.Lease)
|
||||
.join(models.ComputeHostAllocation)
|
||||
.filter(models.ComputeHostAllocation.compute_host_id == host_id)
|
||||
.filter(~sa.or_(border0, border1)))
|
||||
return query.all()
|
||||
|
||||
|
||||
def get_free_periods(resource_id, start_date, end_date, duration):
|
||||
"""Returns a list of free periods."""
|
||||
full_periods = get_full_periods(resource_id,
|
||||
|
|
|
@ -103,6 +103,10 @@ def to_dict(func):
|
|||
return decorator
|
||||
|
||||
|
||||
def get_reservations_by_host_id(host_id, start_date, end_date):
|
||||
return IMPL.get_reservations_by_host_id(host_id, start_date, end_date)
|
||||
|
||||
|
||||
def get_free_periods(resource_id, start_date, end_date, duration):
|
||||
"""Returns a list of free periods."""
|
||||
return IMPL.get_free_periods(resource_id, start_date, end_date, duration)
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
# Copyright (c) 2017 NTT.
|
||||
#
|
||||
# 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 oslo_log import log as logging
|
||||
from oslo_utils.strutils import bool_from_string
|
||||
|
||||
from blazar.db import api as db_api
|
||||
from blazar.db import utils as db_utils
|
||||
from blazar import exceptions
|
||||
from blazar.manager import exceptions as mgr_exceptions
|
||||
from blazar.plugins import base
|
||||
from blazar.plugins import oshosts
|
||||
from blazar.utils import plugins as plugins_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
RESOURCE_TYPE = u'virtual:instance'
|
||||
|
||||
|
||||
class VirtualInstancePlugin(base.BasePlugin):
|
||||
"""Plugin for virtual instance resources."""
|
||||
|
||||
resource_type = RESOURCE_TYPE
|
||||
title = 'Virtual Instance Plugin'
|
||||
|
||||
def __init__(self):
|
||||
super(VirtualInstancePlugin, self).__init__()
|
||||
self.freepool_name = CONF.nova.aggregate_freepool_name
|
||||
|
||||
def filter_hosts_by_reservation(self, hosts, start_date, end_date):
|
||||
free = []
|
||||
non_free = []
|
||||
|
||||
for host in hosts:
|
||||
reservations = db_utils.get_reservations_by_host_id(host['id'],
|
||||
start_date,
|
||||
end_date)
|
||||
if reservations == []:
|
||||
free.append({'host': host, 'reservations': None})
|
||||
elif not filter(lambda x: x['resource_type'] ==
|
||||
oshosts.RESOURCE_TYPE, reservations):
|
||||
non_free.append({'host': host, 'reservations': reservations})
|
||||
|
||||
return free, non_free
|
||||
|
||||
def max_usages(self, host, reservations):
|
||||
# TODO(masahito): needs to check the max usage of the host in the
|
||||
# time window
|
||||
return 0, 0, 0
|
||||
|
||||
def pickup_hosts(self, cpus, memory, disk, amount, start_date, end_date):
|
||||
"""Checks whether Blazar can accommodate the request.
|
||||
|
||||
This method filters and pick up hosts for this reservation
|
||||
with following steps.
|
||||
|
||||
1. filter hosts that have a spec enough to accommodate the flavor
|
||||
2. categorize hosts allocated_hosts and not_allocated_hosts
|
||||
at the reservation time frame
|
||||
3. filter out hosts used by physical host reservation from
|
||||
allocate_host
|
||||
4. filter out hosts that can't accommodate the flavor at the
|
||||
time frame because of others reservations
|
||||
"""
|
||||
flavor_definitions = [
|
||||
'and',
|
||||
[">=", "$vcpus", str(cpus)],
|
||||
[">=", "$memory_mb", str(memory)],
|
||||
[">=", "$local_gb", str(disk)],
|
||||
]
|
||||
|
||||
filters = plugins_utils.convert_requirements(flavor_definitions)
|
||||
|
||||
hosts = db_api.host_get_all_by_queries(filters)
|
||||
|
||||
free_hosts, reserved_hosts = \
|
||||
self.filter_hosts_by_reservation(hosts, start_date, end_date)
|
||||
|
||||
host_ids = []
|
||||
for host_info in reserved_hosts:
|
||||
host = host_info['host']
|
||||
reservations = host_info['reservations']
|
||||
max_cpus, max_memory, max_disk = self.max_usages(host,
|
||||
reservations)
|
||||
|
||||
if not (max_cpus + cpus > host['vcpus'] or
|
||||
max_memory + memory > host['memory_mb'] or
|
||||
max_disk + disk > host['local_gb']):
|
||||
host_ids.append(host['id'])
|
||||
|
||||
if len(host_ids) >= int(amount):
|
||||
return host_ids[:int(amount)]
|
||||
elif len(host_ids) + len(free_hosts) >= int(amount):
|
||||
host_ids.extend([h['host']['id'] for h in free_hosts])
|
||||
return host_ids[:int(amount)]
|
||||
else:
|
||||
raise mgr_exceptions.HostNotFound("The reservation can't be "
|
||||
"accommodate because of less "
|
||||
"capacity.")
|
||||
|
||||
def validate_reservation_param(self, values):
|
||||
marshall_attributes = set(['vcpus', 'memory_mb', 'disk_gb',
|
||||
'amount', 'affinity'])
|
||||
missing_params = marshall_attributes - set(values.keys())
|
||||
if missing_params:
|
||||
mgr_exceptions.MissingParameter(param=','.join(missing_params))
|
||||
|
||||
def reserve_resource(self, reservation_id, values):
|
||||
self.validate_reservation_param(values)
|
||||
|
||||
# TODO(masahito) the instance reservation plugin only supports
|
||||
# anti-affinity rule in short-term goal.
|
||||
if bool_from_string(values['affinity']):
|
||||
raise exceptions.BlazarException('affinity = True is not '
|
||||
'supported.')
|
||||
|
||||
host_ids = self.pickup_hosts(values['vcpus'], values['memory_mb'],
|
||||
values['disk_gb'], values['amount'],
|
||||
values['start_date'], values['end_date'])
|
||||
|
||||
instance_reservation_val = {
|
||||
'reservation_id': reservation_id,
|
||||
'vcpus': values['vcpus'],
|
||||
'memory_mb': values['memory_mb'],
|
||||
'disk_gb': values['disk_gb'],
|
||||
'amount': values['amount'],
|
||||
'affinity': bool_from_string(values['affinity']),
|
||||
}
|
||||
instance_reservation = db_api.instance_reservation_create(
|
||||
instance_reservation_val)
|
||||
|
||||
for host_id in host_ids:
|
||||
db_api.host_allocation_create({'compute_host_id': host_id,
|
||||
'reservation_id': reservation_id})
|
||||
|
||||
return instance_reservation['id']
|
||||
|
||||
def update_reservation(self, reservation_id, values):
|
||||
raise NotImplementedError("resource type virtual:instance doesn't "
|
||||
"support updates of reservation.")
|
||||
|
||||
def on_start(self, resource_id):
|
||||
pass
|
||||
|
||||
def on_end(self, resource_id):
|
||||
pass
|
|
@ -71,7 +71,7 @@ def _create_physical_lease(values=_get_fake_phys_lease_values(),
|
|||
for reservation in db_api.reservation_get_all_by_lease_id(lease['id']):
|
||||
allocation_values = {
|
||||
'id': _get_fake_random_uuid(),
|
||||
'compute_host_id': values['reservations'][0]['resource_id'],
|
||||
'compute_host_id': reservation['resource_id'],
|
||||
'reservation_id': reservation['id']
|
||||
}
|
||||
db_api.host_allocation_create(allocation_values)
|
||||
|
@ -109,6 +109,15 @@ class SQLAlchemyDBUtilsTestCase(tests.DBTestCase):
|
|||
_create_physical_lease(values=r2)
|
||||
_create_physical_lease(values=r3)
|
||||
|
||||
def check_reservation(self, expect, host_id, start, end):
|
||||
expect.sort(key=lambda x: x['lease_id'])
|
||||
ret = db_utils.get_reservations_by_host_id(host_id, start, end)
|
||||
|
||||
for i, res in enumerate(sorted(ret, key=lambda x: x['lease_id'])):
|
||||
self.assertEqual(expect[i]['lease_id'], res['lease_id'])
|
||||
self.assertEqual(expect[i]['resource_id'], res['resource_id'])
|
||||
self.assertEqual(expect[i]['resource_type'], res['resource_type'])
|
||||
|
||||
def test_get_free_periods(self):
|
||||
"""Find the free periods."""
|
||||
self._setup_leases()
|
||||
|
@ -328,5 +337,44 @@ class SQLAlchemyDBUtilsTestCase(tests.DBTestCase):
|
|||
end_date=_get_datetime('2030-01-01 13:00')),
|
||||
'lease1')
|
||||
|
||||
def test_get_reservations_by_host_id(self):
|
||||
self._setup_leases()
|
||||
|
||||
self.check_reservation([], 'r1',
|
||||
'2030-01-01 07:00', '2030-01-01 08:59')
|
||||
|
||||
ret = db_api.reservation_get_all_by_lease_id('lease1')
|
||||
self.check_reservation(ret, 'r1',
|
||||
'2030-01-01 08:00', '2030-01-01 10:00')
|
||||
|
||||
ret = db_api.reservation_get_all_by_lease_id('lease1')
|
||||
ret.extend(db_api.reservation_get_all_by_lease_id('lease3'))
|
||||
self.check_reservation(ret, 'r1',
|
||||
'2030-01-01 08:00', '2030-01-01 15:30')
|
||||
|
||||
self.check_reservation([], 'r4',
|
||||
'2030-01-01 07:00', '2030-01-01 15:00')
|
||||
|
||||
def test_get_reservations_by_host_id_with_multi_reservation(self):
|
||||
self._setup_leases()
|
||||
|
||||
fake_lease = _get_fake_phys_lease_values(
|
||||
id='lease-4',
|
||||
name='fake_phys_lease_r4',
|
||||
start_date=_get_datetime('2030-01-01 15:00'),
|
||||
end_date=_get_datetime('2030-01-01 16:00'),
|
||||
resource_id='r4-1')
|
||||
|
||||
fake_lease['reservations'].append(
|
||||
_get_fake_phys_reservation_values(lease_id='lease-4',
|
||||
resource_id='r1'))
|
||||
_create_physical_lease(values=fake_lease)
|
||||
|
||||
expected = db_api.reservation_get_all_by_values(
|
||||
**{'resource_id': 'r1'})
|
||||
self.assertEqual(3, len(expected))
|
||||
self.check_reservation(expected, 'r1',
|
||||
'2030-01-01 08:00', '2030-01-01 17:00')
|
||||
|
||||
# TODO(frossigneux) longest_availability
|
||||
# TODO(frossigneux) shortest_availability
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
# Copyright (c) 2017 NTT.
|
||||
#
|
||||
# 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 blazar.db import api as db_api
|
||||
from blazar.db import utils as db_utils
|
||||
from blazar import exceptions
|
||||
from blazar.manager import exceptions as mgr_exceptions
|
||||
from blazar.plugins.instances import instance_plugin
|
||||
from blazar.plugins import oshosts
|
||||
from blazar import tests
|
||||
|
||||
|
||||
class TestVirtualInstancePlugin(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestVirtualInstancePlugin, self).setUp()
|
||||
|
||||
def get_input_values(self, vcpus, memory, disk, amount, affinity,
|
||||
start, end, lease_id):
|
||||
return {'vcpus': vcpus, 'memory_mb': memory, 'disk_gb': disk,
|
||||
'amount': amount, 'affinity': affinity, 'start_date': start,
|
||||
'end_date': end, 'lease_id': lease_id}
|
||||
|
||||
def generate_host_info(self, id, vcpus, memory, disk):
|
||||
return {'id': id, 'vcpus': vcpus,
|
||||
'memory_mb': memory, 'local_gb': disk}
|
||||
|
||||
def test_reserve_resource(self):
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
mock_pickup_hosts = self.patch(plugin, 'pickup_hosts')
|
||||
mock_pickup_hosts.return_value = ['host1', 'host2']
|
||||
|
||||
mock_inst_create = self.patch(db_api, 'instance_reservation_create')
|
||||
mock_inst_create.return_value = {'id': 'instance-reservation-id1'}
|
||||
|
||||
mock_alloc_create = self.patch(db_api, 'host_allocation_create')
|
||||
|
||||
inputs = self.get_input_values(2, 4018, 10, 1, False,
|
||||
'2030-01-01 08:00', '2030-01-01 08:00',
|
||||
'lease-1')
|
||||
|
||||
expected_ret = 'instance-reservation-id1'
|
||||
|
||||
ret = plugin.reserve_resource('res_id1', inputs)
|
||||
|
||||
self.assertEqual(expected_ret, ret)
|
||||
mock_pickup_hosts.assert_called_once_with(inputs['vcpus'],
|
||||
inputs['memory_mb'],
|
||||
inputs['disk_gb'],
|
||||
inputs['amount'],
|
||||
inputs['start_date'],
|
||||
inputs['end_date'])
|
||||
|
||||
mock_alloc_create.assert_any_call({'compute_host_id': 'host1',
|
||||
'reservation_id': 'res_id1'})
|
||||
mock_alloc_create.assert_any_call({'compute_host_id': 'host2',
|
||||
'reservation_id': 'res_id1'})
|
||||
|
||||
def test_error_with_affinity(self):
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
inputs = self.get_input_values(2, 4018, 10, 1, True,
|
||||
'2030-01-01 08:00', '2030-01-01 08:00',
|
||||
'lease-1')
|
||||
self.assertRaises(exceptions.BlazarException, plugin.reserve_resource,
|
||||
'reservation_id', inputs)
|
||||
|
||||
def test_pickup_host_from_reserved_hosts(self):
|
||||
def fake_max_usages(host, reservations):
|
||||
if host['id'] == 'host-1':
|
||||
return 4, 4096, 2000
|
||||
else:
|
||||
return 0, 0, 0
|
||||
|
||||
def fake_get_reservation_by_host(host_id, start, end):
|
||||
return [
|
||||
{'id': '1', 'resource_type': instance_plugin.RESOURCE_TYPE},
|
||||
{'id': '2', 'resource_type': instance_plugin.RESOURCE_TYPE}]
|
||||
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
|
||||
mock_host_get_query = self.patch(db_api, 'host_get_all_by_queries')
|
||||
hosts_list = [self.generate_host_info('host-1', 4, 4096, 1000),
|
||||
self.generate_host_info('host-2', 4, 4096, 1000),
|
||||
self.generate_host_info('host-3', 4, 4096, 1000)]
|
||||
mock_host_get_query.return_value = hosts_list
|
||||
|
||||
mock_get_reservations = self.patch(db_utils,
|
||||
'get_reservations_by_host_id')
|
||||
|
||||
mock_get_reservations.side_effect = fake_get_reservation_by_host
|
||||
plugin.max_usages = fake_max_usages
|
||||
expected = ['host-2', 'host-3']
|
||||
ret = plugin.pickup_hosts(1, 1024, 20, 2,
|
||||
'2030-01-01 08:00', '2030-01-01 12:00')
|
||||
|
||||
self.assertEqual(expected, ret)
|
||||
expected_query = ['vcpus >= 1', 'memory_mb >= 1024', 'local_gb >= 20']
|
||||
mock_host_get_query.assert_called_once_with(expected_query)
|
||||
|
||||
def test_pickup_host_from_free_hosts(self):
|
||||
def fake_get_reservation_by_host(host_id, start, end):
|
||||
return []
|
||||
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
|
||||
mock_host_get_query = self.patch(db_api, 'host_get_all_by_queries')
|
||||
hosts_list = [self.generate_host_info('host-1', 4, 4096, 1000),
|
||||
self.generate_host_info('host-2', 4, 4096, 1000),
|
||||
self.generate_host_info('host-3', 4, 4096, 1000)]
|
||||
mock_host_get_query.return_value = hosts_list
|
||||
|
||||
mock_get_reservations = self.patch(db_utils,
|
||||
'get_reservations_by_host_id')
|
||||
|
||||
mock_get_reservations.side_effect = fake_get_reservation_by_host
|
||||
|
||||
expected = ['host-1', 'host-2']
|
||||
ret = plugin.pickup_hosts(1, 1024, 20, 2,
|
||||
'2030-01-01 08:00', '2030-01-01 12:00')
|
||||
|
||||
self.assertEqual(expected, ret)
|
||||
expected_query = ['vcpus >= 1', 'memory_mb >= 1024', 'local_gb >= 20']
|
||||
mock_host_get_query.assert_called_once_with(expected_query)
|
||||
|
||||
def test_pickup_host_from_free_and_reserved_host(self):
|
||||
def fake_get_reservation_by_host(host_id, start, end):
|
||||
if host_id in ['host-1', 'host-3']:
|
||||
return [
|
||||
{'id': '1',
|
||||
'resource_type': instance_plugin.RESOURCE_TYPE},
|
||||
{'id': '2',
|
||||
'resource_type': instance_plugin.RESOURCE_TYPE}
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
|
||||
mock_host_get_query = self.patch(db_api, 'host_get_all_by_queries')
|
||||
hosts_list = [self.generate_host_info('host-1', 4, 4096, 1000),
|
||||
self.generate_host_info('host-2', 4, 4096, 1000),
|
||||
self.generate_host_info('host-3', 4, 4096, 1000)]
|
||||
mock_host_get_query.return_value = hosts_list
|
||||
|
||||
mock_get_reservations = self.patch(db_utils,
|
||||
'get_reservations_by_host_id')
|
||||
|
||||
mock_get_reservations.side_effect = fake_get_reservation_by_host
|
||||
|
||||
mock_max_usages = self.patch(plugin, 'max_usages')
|
||||
mock_max_usages.return_value = (0, 0, 0)
|
||||
|
||||
expected = ['host-1', 'host-3']
|
||||
ret = plugin.pickup_hosts(1, 1024, 20, 2,
|
||||
'2030-01-01 08:00', '2030-01-01 12:00')
|
||||
|
||||
self.assertEqual(expected, ret)
|
||||
expected_query = ['vcpus >= 1', 'memory_mb >= 1024', 'local_gb >= 20']
|
||||
mock_host_get_query.assert_called_once_with(expected_query)
|
||||
|
||||
def test_pickup_host_from_less_hosts(self):
|
||||
def fake_get_reservation_by_host(host_id, start, end):
|
||||
if host_id in ['host-1', 'host-3']:
|
||||
return [
|
||||
{'id': '1',
|
||||
'resource_type': oshosts.RESOURCE_TYPE},
|
||||
{'id': '2',
|
||||
'resource_type': instance_plugin.RESOURCE_TYPE}
|
||||
]
|
||||
else:
|
||||
return [
|
||||
{'id': '1',
|
||||
'resource_type': instance_plugin.RESOURCE_TYPE},
|
||||
{'id': '2',
|
||||
'resource_type': instance_plugin.RESOURCE_TYPE}
|
||||
]
|
||||
|
||||
plugin = instance_plugin.VirtualInstancePlugin()
|
||||
|
||||
mock_host_get_query = self.patch(db_api, 'host_get_all_by_queries')
|
||||
hosts_list = [self.generate_host_info('host-1', 4, 4096, 1000),
|
||||
self.generate_host_info('host-2', 4, 4096, 1000),
|
||||
self.generate_host_info('host-3', 4, 4096, 1000)]
|
||||
mock_host_get_query.return_value = hosts_list
|
||||
|
||||
mock_get_reservations = self.patch(db_utils,
|
||||
'get_reservations_by_host_id')
|
||||
|
||||
mock_get_reservations.side_effect = fake_get_reservation_by_host
|
||||
mock_max_usages = self.patch(plugin, 'max_usages')
|
||||
mock_max_usages.return_value = (1, 1024, 100)
|
||||
|
||||
self.assertRaises(mgr_exceptions.HostNotFound, plugin.pickup_hosts,
|
||||
1, 1024, 20, 2,
|
||||
'2030-01-01 08:00', '2030-01-01 12:00')
|
Loading…
Reference in New Issue