Merge "Add BuildRequest object"
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user