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:
Andrew Laski 2016-02-09 17:12:28 -05:00 committed by Matt Riedemann
parent 32051990e9
commit 00d6561934
7 changed files with 439 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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