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:
Masahito Muroi 2017-06-21 17:54:55 +09:00 committed by Hiroaki Kobayashi
parent 1bedac5001
commit b835f311a1
7 changed files with 432 additions and 1 deletions

View File

@ -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,

View File

@ -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)

View File

View File

@ -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

View File

@ -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

View File

@ -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')