Add BuildRequest object
The BuildRequest object represents a boot request before an instance has been created and written to the database. It will be required to respond to API requests to list/show an instance in cellsv2 where the instance is not written to the database immediately. An upcoming change will modify the boot process to defer instance creation until after scheduling has picked a cell/host and having this BuildRequest object is a prerequisite for that work. Change-Id: Ie9307388c2e068229177bcc4690cb834028c1481 Partially-implements: bp cells-scheduling-interaction
This commit is contained in:
parent
32051990e9
commit
00d6561934
|
@ -2098,3 +2098,7 @@ class RealtimeMaskNotFoundOrInvalid(Invalid):
|
|||
class OsInfoNotFound(NotFound):
|
||||
msg_fmt = _("No configuration information found for operating system "
|
||||
"%(os_name)s")
|
||||
|
||||
|
||||
class BuildRequestNotFound(NotFound):
|
||||
msg_fmt = _("BuildRequest not found for instance %(uuid)s")
|
||||
|
|
|
@ -28,6 +28,7 @@ def register_all():
|
|||
__import__('nova.objects.aggregate')
|
||||
__import__('nova.objects.bandwidth_usage')
|
||||
__import__('nova.objects.block_device')
|
||||
__import__('nova.objects.build_request')
|
||||
__import__('nova.objects.cell_mapping')
|
||||
__import__('nova.objects.compute_node')
|
||||
__import__('nova.objects.dns_domain')
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
# 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_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from nova.db.sqlalchemy import api as db
|
||||
from nova.db.sqlalchemy import api_models
|
||||
from nova import exception
|
||||
from nova.i18n import _LE
|
||||
from nova import objects
|
||||
from nova.objects import base
|
||||
from nova.objects import fields
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OBJECT_FIELDS = ['info_cache', 'security_groups']
|
||||
JSON_FIELDS = ['instance_metadata']
|
||||
IP_FIELDS = ['access_ip_v4', 'access_ip_v6']
|
||||
|
||||
|
||||
@base.NovaObjectRegistry.register
|
||||
class BuildRequest(base.NovaObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'project_id': fields.StringField(),
|
||||
'user_id': fields.StringField(),
|
||||
'display_name': fields.StringField(nullable=True),
|
||||
'instance_metadata': fields.DictOfStringsField(nullable=True),
|
||||
'progress': fields.IntegerField(nullable=True),
|
||||
'vm_state': fields.StringField(nullable=True),
|
||||
'task_state': fields.StringField(nullable=True),
|
||||
'image_ref': fields.StringField(nullable=True),
|
||||
'access_ip_v4': fields.IPV4AddressField(nullable=True),
|
||||
'access_ip_v6': fields.IPV6AddressField(nullable=True),
|
||||
'info_cache': fields.ObjectField('InstanceInfoCache', nullable=True),
|
||||
'security_groups': fields.ObjectField('SecurityGroupList'),
|
||||
'config_drive': fields.BooleanField(default=False),
|
||||
'key_name': fields.StringField(nullable=True),
|
||||
'locked_by': fields.EnumField(['owner', 'admin'], nullable=True),
|
||||
'request_spec': fields.ObjectField('RequestSpec'),
|
||||
# NOTE(alaski): Normally these would come from the NovaPersistentObject
|
||||
# mixin but they're being set explicitly because we only need
|
||||
# created_at/updated_at. There is no soft delete for this object.
|
||||
# These fields should be carried over to the instance when it is
|
||||
# scheduled and created in a cell database.
|
||||
'created_at': fields.DateTimeField(nullable=True),
|
||||
'updated_at': fields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
def _load_request_spec(self, db_spec):
|
||||
self.request_spec = objects.RequestSpec._from_db_object(self._context,
|
||||
objects.RequestSpec(), db_spec)
|
||||
|
||||
def _load_info_cache(self, db_info_cache):
|
||||
self.info_cache = objects.InstanceInfoCache.obj_from_primitive(
|
||||
jsonutils.loads(db_info_cache))
|
||||
|
||||
def _load_security_groups(self, db_sec_group):
|
||||
self.security_groups = objects.SecurityGroupList.obj_from_primitive(
|
||||
jsonutils.loads(db_sec_group))
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(context, req, db_req):
|
||||
for key in req.fields:
|
||||
if isinstance(req.fields[key], fields.ObjectField):
|
||||
try:
|
||||
getattr(req, '_load_%s' % key)(db_req[key])
|
||||
except AttributeError:
|
||||
LOG.exception(_LE('No load handler for %s'), key)
|
||||
elif key in JSON_FIELDS and db_req[key] is not None:
|
||||
setattr(req, key, jsonutils.loads(db_req[key]))
|
||||
else:
|
||||
setattr(req, key, db_req[key])
|
||||
req.obj_reset_changes()
|
||||
req._context = context
|
||||
return req
|
||||
|
||||
@staticmethod
|
||||
@db.api_context_manager.reader
|
||||
def _get_by_instance_uuid_from_db(context, instance_uuid):
|
||||
db_req = (context.session.query(api_models.BuildRequest)
|
||||
.join(api_models.RequestSpec)
|
||||
.with_entities(api_models.BuildRequest,
|
||||
api_models.RequestSpec)
|
||||
.filter(
|
||||
api_models.RequestSpec.instance_uuid == instance_uuid)
|
||||
).first()
|
||||
if not db_req:
|
||||
raise exception.BuildRequestNotFound(uuid=instance_uuid)
|
||||
# db_req is a tuple (api_models.BuildRequest, api_models.RequestSpect)
|
||||
build_req = db_req[0]
|
||||
build_req['request_spec'] = db_req[1]
|
||||
return build_req
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_instance_uuid(cls, context, instance_uuid):
|
||||
db_req = cls._get_by_instance_uuid_from_db(context, instance_uuid)
|
||||
return cls._from_db_object(context, cls(), db_req)
|
||||
|
||||
@staticmethod
|
||||
@db.api_context_manager.writer
|
||||
def _create_in_db(context, updates):
|
||||
db_req = api_models.BuildRequest()
|
||||
db_req.update(updates)
|
||||
db_req.save(context.session)
|
||||
# NOTE: This is done because a later access will trigger a lazy load
|
||||
# outside of the db session so it will fail. We don't lazy load
|
||||
# request_spec on the object later because we never need a BuildRequest
|
||||
# without the RequestSpec.
|
||||
db_req.request_spec
|
||||
return db_req
|
||||
|
||||
def _get_update_primitives(self):
|
||||
updates = self.obj_get_changes()
|
||||
for key, value in six.iteritems(updates):
|
||||
if key in OBJECT_FIELDS and value is not None:
|
||||
updates[key] = jsonutils.dumps(value.obj_to_primitive())
|
||||
elif key in JSON_FIELDS and value is not None:
|
||||
updates[key] = jsonutils.dumps(value)
|
||||
elif key in IP_FIELDS and value is not None:
|
||||
# These are stored as a string in the db and must be converted
|
||||
updates[key] = str(value)
|
||||
req_spec_obj = updates.pop('request_spec', None)
|
||||
if req_spec_obj:
|
||||
updates['request_spec_id'] = req_spec_obj.id
|
||||
return updates
|
||||
|
||||
@base.remotable
|
||||
def create(self):
|
||||
if self.obj_attr_is_set('id'):
|
||||
raise exception.ObjectActionError(action='create',
|
||||
reason='already created')
|
||||
|
||||
updates = self._get_update_primitives()
|
||||
db_req = self._create_in_db(self._context, updates)
|
||||
self._from_db_object(self._context, self, db_req)
|
||||
|
||||
@staticmethod
|
||||
@db.api_context_manager.writer
|
||||
def _destroy_in_db(context, id):
|
||||
context.session.query(api_models.BuildRequest).filter_by(
|
||||
id=id).delete()
|
||||
|
||||
@base.remotable
|
||||
def destroy(self):
|
||||
self._destroy_in_db(self._context, self.id)
|
|
@ -0,0 +1,78 @@
|
|||
# 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_serialization import jsonutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from nova import context
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova.objects import build_request
|
||||
from nova import test
|
||||
from nova.tests import fixtures
|
||||
from nova.tests.unit import fake_build_request
|
||||
from nova.tests.unit import fake_request_spec
|
||||
|
||||
|
||||
class BuildRequestTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(BuildRequestTestCase, self).setUp()
|
||||
# NOTE: This means that we're using a database for this test suite
|
||||
# despite inheriting from NoDBTestCase
|
||||
self.useFixture(fixtures.Database(database='api'))
|
||||
self.context = context.RequestContext('fake-user', 'fake-project')
|
||||
self.build_req_obj = build_request.BuildRequest()
|
||||
self.instance_uuid = uuidutils.generate_uuid()
|
||||
self.project_id = 'fake-project'
|
||||
|
||||
def _create_req(self):
|
||||
req_spec = fake_request_spec.fake_spec_obj(remove_id=True)
|
||||
req_spec.instance_uuid = self.instance_uuid
|
||||
req_spec.create()
|
||||
args = fake_build_request.fake_db_req(
|
||||
request_spec_id=req_spec.id)
|
||||
args.pop('id', None)
|
||||
args.pop('request_spec', None)
|
||||
args['project_id'] = self.project_id
|
||||
return build_request.BuildRequest._from_db_object(self.context,
|
||||
self.build_req_obj,
|
||||
self.build_req_obj._create_in_db(self.context, args))
|
||||
|
||||
def test_get_by_instance_uuid_not_found(self):
|
||||
self.assertRaises(exception.BuildRequestNotFound,
|
||||
self.build_req_obj._get_by_instance_uuid_from_db, self.context,
|
||||
self.instance_uuid)
|
||||
|
||||
def test_get_by_uuid(self):
|
||||
req = self._create_req()
|
||||
db_req = self.build_req_obj._get_by_instance_uuid_from_db(self.context,
|
||||
self.instance_uuid)
|
||||
for key in self.build_req_obj.fields.keys():
|
||||
expected = getattr(req, key)
|
||||
db_value = db_req[key]
|
||||
if key == 'request_spec':
|
||||
# NOTE: The object and db value can't be compared directly as
|
||||
# objects, so serialize them to a comparable form.
|
||||
db_value = jsonutils.dumps(objects.RequestSpec._from_db_object(
|
||||
self.context, objects.RequestSpec(),
|
||||
db_value).obj_to_primitive())
|
||||
expected = jsonutils.dumps(expected.obj_to_primitive())
|
||||
elif key in build_request.OBJECT_FIELDS:
|
||||
expected = jsonutils.dumps(expected.obj_to_primitive())
|
||||
elif key in build_request.JSON_FIELDS:
|
||||
expected = jsonutils.dumps(expected)
|
||||
elif key in build_request.IP_FIELDS:
|
||||
expected = str(expected)
|
||||
elif key in ['created_at', 'updated_at']:
|
||||
# Objects store tz aware datetimes but the db does not.
|
||||
expected = expected.replace(tzinfo=None)
|
||||
self.assertEqual(expected, db_value)
|
|
@ -0,0 +1,112 @@
|
|||
# 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 oslo_serialization import jsonutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from nova.compute import task_states
|
||||
from nova.compute import vm_states
|
||||
from nova import context
|
||||
from nova.network import model as network_model
|
||||
from nova import objects
|
||||
from nova.objects import fields
|
||||
from nova.tests.unit import fake_request_spec
|
||||
|
||||
|
||||
def _req_spec_to_db_format(req_spec):
|
||||
db_spec = {'spec': jsonutils.dumps(req_spec.obj_to_primitive()),
|
||||
'id': req_spec.id,
|
||||
'instance_uuid': req_spec.instance_uuid,
|
||||
}
|
||||
return db_spec
|
||||
|
||||
|
||||
def fake_db_req(**updates):
|
||||
instance_uuid = uuidutils.generate_uuid()
|
||||
info_cache = objects.InstanceInfoCache()
|
||||
info_cache.instance_uuid = instance_uuid
|
||||
info_cache.network_info = network_model.NetworkInfo()
|
||||
req_spec = fake_request_spec.fake_spec_obj(
|
||||
context.RequestContext('fake-user', 'fake-project'))
|
||||
req_spec.id = 42
|
||||
req_spec.obj_reset_changes()
|
||||
db_build_request = {
|
||||
'id': 1,
|
||||
'project_id': 'fake-project',
|
||||
'user_id': 'fake-user',
|
||||
'display_name': '',
|
||||
'instance_metadata': jsonutils.dumps({'foo': 'bar'}),
|
||||
'progress': 0,
|
||||
'vm_state': vm_states.BUILDING,
|
||||
'task_state': task_states.SCHEDULING,
|
||||
'image_ref': None,
|
||||
'access_ip_v4': '1.2.3.4',
|
||||
'access_ip_v6': '::1',
|
||||
'info_cache': jsonutils.dumps(info_cache.obj_to_primitive()),
|
||||
'security_groups': jsonutils.dumps(
|
||||
objects.SecurityGroupList().obj_to_primitive()),
|
||||
'config_drive': False,
|
||||
'key_name': None,
|
||||
'locked_by': None,
|
||||
'request_spec': _req_spec_to_db_format(req_spec),
|
||||
'created_at': datetime.datetime(2016, 1, 16),
|
||||
'updated_at': datetime.datetime(2016, 1, 16),
|
||||
}
|
||||
|
||||
for name, field in objects.BuildRequest.fields.items():
|
||||
if name in db_build_request:
|
||||
continue
|
||||
if field.nullable:
|
||||
db_build_request[name] = None
|
||||
elif field.default != fields.UnspecifiedDefault:
|
||||
db_build_request[name] = field.default
|
||||
else:
|
||||
raise Exception('fake_db_req needs help with %s' % name)
|
||||
|
||||
if updates:
|
||||
db_build_request.update(updates)
|
||||
|
||||
return db_build_request
|
||||
|
||||
|
||||
def fake_req_obj(context, db_req=None):
|
||||
if db_req is None:
|
||||
db_req = fake_db_req()
|
||||
req_obj = objects.BuildRequest(context)
|
||||
for field in req_obj.fields:
|
||||
value = db_req[field]
|
||||
# create() can't be called if this is set
|
||||
if field == 'id':
|
||||
continue
|
||||
if isinstance(req_obj.fields[field], fields.ObjectField):
|
||||
value = value
|
||||
if field == 'request_spec':
|
||||
req_spec = objects.RequestSpec._from_db_object(context,
|
||||
objects.RequestSpec(), value)
|
||||
req_obj.request_spec = req_spec
|
||||
elif field == 'info_cache':
|
||||
setattr(req_obj, field,
|
||||
objects.InstanceInfoCache.obj_from_primitive(
|
||||
jsonutils.loads(value)))
|
||||
elif field == 'security_groups':
|
||||
setattr(req_obj, field,
|
||||
objects.SecurityGroupList.obj_from_primitive(
|
||||
jsonutils.loads(value)))
|
||||
elif field == 'instance_metadata':
|
||||
setattr(req_obj, field, jsonutils.loads(value))
|
||||
else:
|
||||
setattr(req_obj, field, value)
|
||||
# This should never be a changed field
|
||||
req_obj.obj_reset_changes(['id'])
|
||||
return req_obj
|
|
@ -0,0 +1,81 @@
|
|||
# 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 import exception
|
||||
from nova import objects
|
||||
from nova.objects import build_request
|
||||
from nova.tests.unit import fake_build_request
|
||||
from nova.tests.unit.objects import test_objects
|
||||
|
||||
|
||||
class _TestBuildRequestObject(object):
|
||||
|
||||
@mock.patch.object(build_request.BuildRequest,
|
||||
'_get_by_instance_uuid_from_db')
|
||||
def test_get_by_instance_uuid(self, get_by_uuid):
|
||||
fake_req = fake_build_request.fake_db_req()
|
||||
get_by_uuid.return_value = fake_req
|
||||
|
||||
req_obj = build_request.BuildRequest.get_by_instance_uuid(self.context,
|
||||
fake_req['request_spec']['instance_uuid'])
|
||||
|
||||
self.assertEqual(fake_req['request_spec']['instance_uuid'],
|
||||
req_obj.request_spec.instance_uuid)
|
||||
self.assertEqual(fake_req['project_id'], req_obj.project_id)
|
||||
self.assertIsInstance(req_obj.request_spec, objects.RequestSpec)
|
||||
get_by_uuid.assert_called_once_with(self.context,
|
||||
fake_req['request_spec']['instance_uuid'])
|
||||
|
||||
@mock.patch.object(build_request.BuildRequest,
|
||||
'_create_in_db')
|
||||
def test_create(self, create_in_db):
|
||||
fake_req = fake_build_request.fake_db_req()
|
||||
req_obj = fake_build_request.fake_req_obj(self.context, fake_req)
|
||||
|
||||
def _test_create_args(self2, context, changes):
|
||||
for field in [fields for fields in
|
||||
build_request.BuildRequest.fields if fields not in
|
||||
['created_at', 'updated_at', 'request_spec', 'id']]:
|
||||
self.assertEqual(fake_req[field], changes[field])
|
||||
self.assertEqual(fake_req['request_spec']['id'],
|
||||
changes['request_spec_id'])
|
||||
return fake_req
|
||||
|
||||
with mock.patch.object(build_request.BuildRequest, '_create_in_db',
|
||||
_test_create_args):
|
||||
req_obj.create()
|
||||
|
||||
def test_create_id_set(self):
|
||||
req_obj = build_request.BuildRequest(self.context)
|
||||
req_obj.id = 3
|
||||
|
||||
self.assertRaises(exception.ObjectActionError, req_obj.create)
|
||||
|
||||
@mock.patch.object(build_request.BuildRequest, '_destroy_in_db')
|
||||
def test_destroy(self, destroy_in_db):
|
||||
req_obj = build_request.BuildRequest(self.context)
|
||||
req_obj.id = 1
|
||||
req_obj.destroy()
|
||||
|
||||
destroy_in_db.assert_called_once_with(self.context, req_obj.id)
|
||||
|
||||
|
||||
class TestBuildRequestObject(test_objects._LocalTest,
|
||||
_TestBuildRequestObject):
|
||||
pass
|
||||
|
||||
|
||||
class TestRemoteBuildRequestObject(test_objects._RemoteTest,
|
||||
_TestBuildRequestObject):
|
||||
pass
|
|
@ -1105,6 +1105,7 @@ object_data = {
|
|||
'BandwidthUsageList': '1.2-5fe7475ada6fe62413cbfcc06ec70746',
|
||||
'BlockDeviceMapping': '1.16-12319f6f47f740a67a88a23f7c7ee6ef',
|
||||
'BlockDeviceMappingList': '1.17-1e568eecb91d06d4112db9fd656de235',
|
||||
'BuildRequest': '1.0-e4ca475cabb07f73d8176f661afe8c55',
|
||||
'CellMapping': '1.0-7f1a7e85a22bbb7559fc730ab658b9bd',
|
||||
'ComputeNode': '1.16-2436e5b836fa0306a3c4e6d9e5ddacec',
|
||||
'ComputeNodeList': '1.14-3b6f4f5ade621c40e70cb116db237844',
|
||||
|
|
Loading…
Reference in New Issue