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:
parent
9094488ed0
commit
c2b65be7c4
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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'})
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user