Create Nova resources for instance reservations

The instance reservation feature needs to handle Nova resources
(flavors, aggregates, and server groups) for scheduling. This patch
enables instance_plugin to create these resources when the create_lease
API is called.

Partially implements: blueprint new-instance-reservation
Change-Id: Ic69ebf613668d15b566a59702100ad8abfba8a6c
This commit is contained in:
Masahito Muroi 2017-07-04 18:45:25 +09:00
parent 9094488ed0
commit c2b65be7c4
7 changed files with 207 additions and 9 deletions

View File

@ -284,6 +284,13 @@ def instance_reservation_get(instance_reservation_id):
return IMPL.instance_reservation_get(instance_reservation_id)
def instance_reservation_update(instance_reservation_id,
instance_reservation_values):
"""Update instance reservation."""
return IMPL.instance_reservation_update(instance_reservation_id,
instance_reservation_values)
def instance_reservation_destroy(instance_reservation_id):
"""Delete specific instance reservation."""
return IMPL.instance_reservation_destroy(instance_reservation_id)

View File

@ -472,12 +472,30 @@ def instance_reservation_create(values):
return instance_reservation_get(instance_reservation.id)
def instance_reservation_get(instance_reservation_id):
session = get_session()
def instance_reservation_get(instance_reservation_id, session=None):
if not session:
session = get_session()
query = model_query(models.InstanceReservations, session)
return query.filter_by(id=instance_reservation_id).first()
def instance_reservation_update(instance_reservation_id, values):
session = get_session()
with session.begin():
instance_reservation = instance_reservation_get(
instance_reservation_id, session)
if not instance_reservation:
raise db_exc.BlazarDBNotFound(
id=instance_reservation_id, model='InstanceReservations')
instance_reservation.update(values)
instance_reservation.save(session=session)
return instance_reservation_get(instance_reservation_id)
def instance_reservation_destroy(instance_reservation_id):
session = get_session()
with session.begin():

View File

@ -94,6 +94,10 @@ class MissingTrustId(exceptions.BlazarException):
msg_fmt = _("A trust id is required")
class NovaClientError(exceptions.BlazarException):
msg_fmt = _("Failed to create Nova resources for the reservation")
# oshost plugin related exceptions
class CantAddExtraCapability(exceptions.BlazarException):

View File

@ -12,32 +12,43 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import exceptions as nova_exceptions
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils.strutils import bool_from_string
from blazar import context
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.openstack import nova
from blazar.utils import plugins as plugins_utils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
RESOURCE_TYPE = u'virtual:instance'
RESERVATION_PREFIX = 'reservation'
FLAVOR_EXTRA_SPEC = "aggregate_instance_extra_specs:" + RESERVATION_PREFIX
class VirtualInstancePlugin(base.BasePlugin):
class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
"""Plugin for virtual instance resources."""
resource_type = RESOURCE_TYPE
title = 'Virtual Instance Plugin'
def __init__(self):
super(VirtualInstancePlugin, self).__init__()
super(VirtualInstancePlugin, self).__init__(
username=CONF.os_admin_username,
password=CONF.os_admin_password,
user_domain_name=CONF.os_admin_user_domain_name,
project_name=CONF.os_admin_project_name,
project_domain_name=CONF.os_admin_user_domain_name)
self.freepool_name = CONF.nova.aggregate_freepool_name
def filter_hosts_by_reservation(self, hosts, start_date, end_date):
@ -141,6 +152,56 @@ class VirtualInstancePlugin(base.BasePlugin):
"accommodate because of less "
"capacity.")
def _create_resources(self, instance_reservation):
reservation_id = instance_reservation['reservation_id']
ctx = context.current()
user_client = nova.NovaClientWrapper()
reserved_group = user_client.nova.server_groups.create(
RESERVATION_PREFIX + ':' + reservation_id,
'affinity' if instance_reservation['affinity'] else 'anti-affinity'
)
flavor_details = {
'flavorid': reservation_id,
'name': RESERVATION_PREFIX + ":" + reservation_id,
'vcpus': instance_reservation['vcpus'],
'ram': instance_reservation['memory_mb'],
'disk': instance_reservation['disk_gb'],
'is_public': False
}
reserved_flavor = self.nova.nova.flavors.create(**flavor_details)
extra_specs = {
FLAVOR_EXTRA_SPEC: reservation_id,
"affinity_id": reserved_group.id
}
reserved_flavor.set_keys(extra_specs)
pool = nova.ReservationPool()
pool_metadata = {
RESERVATION_PREFIX: reservation_id,
'filter_tenant_id': ctx.project_id,
'affinity_id': reserved_group.id
}
agg = pool.create(name=reservation_id, metadata=pool_metadata)
return reserved_flavor, reserved_group, agg
def cleanup_resources(self, instance_reservation):
def check_and_delete_resource(client, id):
try:
client.delete(id)
except nova_exceptions.NotFound:
pass
reservation_id = instance_reservation['reservation_id']
check_and_delete_resource(self.nova.nova.server_groups,
instance_reservation['server_group_id'])
check_and_delete_resource(self.nova.nova.flavors, reservation_id)
check_and_delete_resource(nova.ReservationPool(), reservation_id)
def validate_reservation_param(self, values):
marshall_attributes = set(['vcpus', 'memory_mb', 'disk_gb',
'amount', 'affinity'])
@ -176,6 +237,19 @@ class VirtualInstancePlugin(base.BasePlugin):
db_api.host_allocation_create({'compute_host_id': host_id,
'reservation_id': reservation_id})
try:
flavor, group, pool = self._create_resources(instance_reservation)
except nova_exceptions.ClientException:
LOG.exception("Failed to create Nova resources "
"for reservation %s" % reservation_id)
self.cleanup_resources(instance_reservation)
raise mgr_exceptions.NovaClientError()
db_api.instance_reservation_update(instance_reservation['id'],
{'flavor_id': flavor.id,
'server_group_id': group.id,
'aggregate_id': pool.id})
return instance_reservation['id']
def update_reservation(self, reservation_id, values):

View File

@ -630,6 +630,25 @@ class SQLAlchemyDBApiTestCase(tests.DBTestCase):
self.check_instance_reservation_values(reservation1_values, '1')
self.check_instance_reservation_values(reservation2_values, '2')
def test_instance_reservation_update(self):
reservation_values = _get_fake_instance_values(id='1')
db_api.instance_reservation_create(reservation_values)
self.check_instance_reservation_values(reservation_values, '1')
updated_values = {
'flavor_id': 'updated-flavor-id',
'aggregate_id': 30,
'server_group_id': 'updated-server-group-id'
}
db_api.instance_reservation_update('1', updated_values)
reservation_values.update(updated_values)
self.check_instance_reservation_values(reservation_values, '1')
def test_update_non_existing_instance_reservation(self):
self.assertRaises(db_exceptions.BlazarDBNotFound,
db_api.instance_reservation_destroy, 'non-exists')
def test_instance_reservation_destroy(self):
reservation_values = _get_fake_instance_values(id='1')
db_api.instance_reservation_create(reservation_values)

View File

@ -16,6 +16,9 @@
import datetime
import uuid
import mock
from blazar import context
from blazar.db import api as db_api
from blazar.db import utils as db_utils
from blazar import exceptions
@ -23,6 +26,7 @@ from blazar.manager import exceptions as mgr_exceptions
from blazar.plugins.instances import instance_plugin
from blazar.plugins import oshosts
from blazar import tests
from blazar.utils.openstack import nova
class TestVirtualInstancePlugin(tests.TestCase):
@ -71,10 +75,20 @@ class TestVirtualInstancePlugin(tests.TestCase):
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'}
fake_instance_reservation = {'id': 'instance-reservation-id1'}
mock_inst_create.return_value = fake_instance_reservation
mock_alloc_create = self.patch(db_api, 'host_allocation_create')
mock_create_resources = self.patch(plugin, '_create_resources')
mock_flavor = mock.MagicMock(id=1)
mock_group = mock.MagicMock(id=2)
mock_pool = mock.MagicMock(id=3)
mock_create_resources.return_value = (mock_flavor,
mock_group, mock_pool)
mock_inst_update = self.patch(db_api, 'instance_reservation_update')
inputs = self.get_input_values(2, 4018, 10, 1, False,
'2030-01-01 08:00', '2030-01-01 08:00',
'lease-1')
@ -95,6 +109,12 @@ class TestVirtualInstancePlugin(tests.TestCase):
'reservation_id': 'res_id1'})
mock_alloc_create.assert_any_call({'compute_host_id': 'host2',
'reservation_id': 'res_id1'})
mock_create_resources.assert_called_once_with(
fake_instance_reservation)
mock_inst_update.assert_called_once_with('instance-reservation-id1',
{'flavor_id': 1,
'server_group_id': 2,
'aggregate_id': 3})
def test_error_with_affinity(self):
plugin = instance_plugin.VirtualInstancePlugin()
@ -371,3 +391,56 @@ class TestVirtualInstancePlugin(tests.TestCase):
ret = plugin.max_usages('fake-host', reservations)
self.assertEqual(expected, ret)
def test_create_resources(self):
instance_reservation = {
'reservation_id': 'reservation-id1',
'vcpus': 2,
'memory_mb': 1024,
'disk_gb': 20,
'affinity': False
}
plugin = instance_plugin.VirtualInstancePlugin()
fake_client = mock.MagicMock()
mock_nova_client = self.patch(nova, 'NovaClientWrapper')
mock_nova_client.return_value = fake_client
fake_server_group = mock.MagicMock(id='server_group_id1')
fake_client.nova.server_groups.create.return_value = \
fake_server_group
self.set_context(context.BlazarContext(project_id='fake-project',
auth_token='fake-token'))
fake_flavor = mock.MagicMock(method='set_keys',
flavorid='reservation-id1')
mock_nova = mock.MagicMock()
type(plugin).nova = mock_nova
mock_nova.nova.flavors.create.return_value = fake_flavor
fake_pool = mock.MagicMock(id='pool-id1')
fake_agg = mock.MagicMock()
fake_pool.create.return_value = fake_agg
mock_pool = self.patch(nova, 'ReservationPool')
mock_pool.return_value = fake_pool
expected = (fake_flavor, fake_server_group, fake_agg)
ret = plugin._create_resources(instance_reservation)
self.assertEqual(expected, ret)
fake_client.nova.server_groups.create.assert_called_once_with(
'reservation:reservation-id1', 'anti-affinity')
mock_nova.nova.flavors.create.assert_called_once_with(
flavorid='reservation-id1',
name='reservation:reservation-id1',
vcpus=2, ram=1024, disk=20, is_public=False)
fake_flavor.set_keys.assert_called_once_with(
{'aggregate_instance_extra_specs:reservation': 'reservation-id1',
'affinity_id': 'server_group_id1'})
fake_pool.create.assert_called_once_with(
name='reservation-id1',
metadata={'reservation': 'reservation-id1',
'filter_tenant_id': 'fake-project',
'affinity_id': 'server_group_id1'})

View File

@ -244,7 +244,7 @@ class ReservationPool(NovaClientWrapper):
def _generate_aggregate_name():
return str(uuidgen.uuid4())
def create(self, name=None, az=None):
def create(self, name=None, az=None, metadata=None):
"""Create a Pool (an Aggregate) with or without Availability Zone.
By default expose to user the aggregate with an Availability Zone.
@ -266,8 +266,11 @@ class ReservationPool(NovaClientWrapper):
LOG.error(e.message)
raise e
meta = {self.config.blazar_owner: project_id}
self.nova.aggregates.set_metadata(agg, meta)
if metadata:
metadata[self.config.blazar_owner] = project_id
else:
metadata = {self.config.blazar_owner: project_id}
self.nova.aggregates.set_metadata(agg, metadata)
return agg
@ -296,7 +299,7 @@ class ReservationPool(NovaClientWrapper):
"'%s')" % (host, agg.id))
self.nova.aggregates.remove_host(agg.id, host)
if freepool_agg.id != agg.id:
if freepool_agg.id != agg.id and host not in freepool_agg.hosts:
self.nova.aggregates.add_host(freepool_agg.id, host)
self.nova.aggregates.delete(agg.id)