summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/cells/messaging.py16
-rw-r--r--nova/db/sqlalchemy/api.py8
-rw-r--r--nova/exception.py5
-rw-r--r--nova/tests/cells/test_cells_messaging.py54
-rw-r--r--nova/tests/test_db_api.py48
5 files changed, 131 insertions, 0 deletions
diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py
index 2e2fa735a..d13677f74 100644
--- a/nova/cells/messaging.py
+++ b/nova/cells/messaging.py
@@ -31,6 +31,7 @@ from nova.cells import state as cells_state
from nova.cells import utils as cells_utils
from nova import compute
from nova.compute import rpcapi as compute_rpcapi
+from nova.compute import vm_states
from nova.consoleauth import rpcapi as consoleauth_rpcapi
from nova import context
from nova.db import base
@@ -810,6 +811,21 @@ class _BroadcastMessageMethods(_BaseMessageMethods):
LOG.debug(_("Got update for instance %(instance_uuid)s: "
"%(instance)s") % locals())
+ # To attempt to address out-of-order messages, do some sanity
+ # checking on the VM state.
+ expected_vm_state_map = {
+ # For updates containing 'vm_state' of 'building',
+ # only allow them to occur if the DB already says
+ # 'building' or if the vm_state is None. None
+ # really shouldn't be possible as instances always
+ # start out in 'building' anyway.. but just in case.
+ vm_states.BUILDING: [vm_states.BUILDING, None]}
+
+ expected_vm_states = expected_vm_state_map.get(
+ instance.get('vm_state'))
+ if expected_vm_states:
+ instance['expected_vm_state'] = expected_vm_states
+
# It's possible due to some weird condition that the instance
# was already set as deleted... so we'll attempt to update
# it with permissions that allows us to read deleted.
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 730ca641c..f76f4d272 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -2009,6 +2009,14 @@ def _instance_update(context, instance_uuid, values, copy_old_instance=False):
if actual_state not in expected:
raise exception.UnexpectedTaskStateError(actual=actual_state,
expected=expected)
+ if "expected_vm_state" in values:
+ expected = values.pop("expected_vm_state")
+ if not isinstance(expected, (tuple, list, set)):
+ expected = (expected,)
+ actual_state = instance_ref["vm_state"]
+ if actual_state not in expected:
+ raise exception.UnexpectedVMStateError(actual=actual_state,
+ expected=expected)
instance_hostname = instance_ref['hostname'] or ''
if ("hostname" in values and
diff --git a/nova/exception.py b/nova/exception.py
index a9afe37a7..b4f453846 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -1153,6 +1153,11 @@ class InstanceActionEventNotFound(NovaException):
message = _("Event %(event)s not found for action id %(action_id)s")
+class UnexpectedVMStateError(NovaException):
+ message = _("unexpected VM state: expecting %(expected)s but "
+ "the actual state is %(actual)s")
+
+
class CryptoCAFileNotFound(FileNotFound):
message = _("The CA file for %(project)s could not be found")
diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py
index 728856006..51a792975 100644
--- a/nova/tests/cells/test_cells_messaging.py
+++ b/nova/tests/cells/test_cells_messaging.py
@@ -19,6 +19,7 @@ from oslo.config import cfg
from nova.cells import messaging
from nova.cells import utils as cells_utils
+from nova.compute import vm_states
from nova import context
from nova import db
from nova import exception
@@ -1038,6 +1039,59 @@ class CellsBroadcastMethodsTestCase(test.TestCase):
self.src_msg_runner.instance_update_at_top(self.ctxt, fake_instance)
+ def test_instance_update_at_top_with_building_state(self):
+ fake_info_cache = {'id': 1,
+ 'instance': 'fake_instance',
+ 'other': 'moo'}
+ fake_sys_metadata = [{'id': 1,
+ 'key': 'key1',
+ 'value': 'value1'},
+ {'id': 2,
+ 'key': 'key2',
+ 'value': 'value2'}]
+ fake_instance = {'id': 2,
+ 'uuid': 'fake_uuid',
+ 'security_groups': 'fake',
+ 'volumes': 'fake',
+ 'cell_name': 'fake',
+ 'name': 'fake',
+ 'metadata': 'fake',
+ 'info_cache': fake_info_cache,
+ 'system_metadata': fake_sys_metadata,
+ 'vm_state': vm_states.BUILDING,
+ 'other': 'meow'}
+ expected_sys_metadata = {'key1': 'value1',
+ 'key2': 'value2'}
+ expected_info_cache = {'other': 'moo'}
+ expected_cell_name = 'api-cell!child-cell2!grandchild-cell1'
+ expected_instance = {'system_metadata': expected_sys_metadata,
+ 'cell_name': expected_cell_name,
+ 'other': 'meow',
+ 'vm_state': vm_states.BUILDING,
+ 'expected_vm_state': [vm_states.BUILDING, None],
+ 'uuid': 'fake_uuid'}
+
+ # To show these should not be called in src/mid-level cell
+ self.mox.StubOutWithMock(self.src_db_inst, 'instance_update')
+ self.mox.StubOutWithMock(self.src_db_inst,
+ 'instance_info_cache_update')
+ self.mox.StubOutWithMock(self.mid_db_inst, 'instance_update')
+ self.mox.StubOutWithMock(self.mid_db_inst,
+ 'instance_info_cache_update')
+
+ self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_update')
+ self.mox.StubOutWithMock(self.tgt_db_inst,
+ 'instance_info_cache_update')
+ self.tgt_db_inst.instance_update(self.ctxt, 'fake_uuid',
+ expected_instance,
+ update_cells=False)
+ self.tgt_db_inst.instance_info_cache_update(self.ctxt, 'fake_uuid',
+ expected_info_cache,
+ update_cells=False)
+ self.mox.ReplayAll()
+
+ self.src_msg_runner.instance_update_at_top(self.ctxt, fake_instance)
+
def test_instance_destroy_at_top(self):
fake_instance = {'uuid': 'fake_uuid'}
diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py
index bf7cc003a..4af0483d8 100644
--- a/nova/tests/test_db_api.py
+++ b/nova/tests/test_db_api.py
@@ -24,6 +24,7 @@ import datetime
import types
import uuid as stdlib_uuid
+import mox
from oslo.config import cfg
from sqlalchemy.dialects import sqlite
from sqlalchemy import MetaData
@@ -36,6 +37,7 @@ from nova.db.sqlalchemy import api as sqlalchemy_api
from nova import exception
from nova.openstack.common.db.sqlalchemy import session as db_session
from nova.openstack.common import timeutils
+from nova.openstack.common import uuidutils
from nova import test
from nova.tests import matchers
from nova import utils
@@ -336,6 +338,52 @@ class DbApiTestCase(DbTestCase):
self.assertEqual(0, len(results))
db.instance_update(ctxt, instance['uuid'], {"task_state": None})
+ def test_instance_update_with_expected_vm_state(self):
+ ctxt = context.get_admin_context()
+ uuid = uuidutils.generate_uuid()
+ updates = {'expected_vm_state': 'meow',
+ 'moo': 'cow'}
+
+ class FakeInstance(dict):
+ def save(self, session=None):
+ pass
+
+ fake_instance_values = {'vm_state': 'meow',
+ 'hostname': '',
+ 'metadata': None,
+ 'system_metadata': None}
+ fake_instance = FakeInstance(fake_instance_values)
+
+ self.mox.StubOutWithMock(sqlalchemy_api, '_instance_get_by_uuid')
+ self.mox.StubOutWithMock(fake_instance, 'save')
+
+ sqlalchemy_api._instance_get_by_uuid(ctxt, uuid,
+ session=mox.IgnoreArg()).AndReturn(fake_instance)
+ fake_instance.save(session=mox.IgnoreArg())
+
+ self.mox.ReplayAll()
+
+ result = db.instance_update(ctxt, uuid, updates)
+ expected_instance = dict(fake_instance_values)
+ expected_instance['moo'] = 'cow'
+ self.assertEqual(expected_instance, result)
+
+ def test_instance_update_with_unexpected_vm_state(self):
+ ctxt = context.get_admin_context()
+ uuid = uuidutils.generate_uuid()
+ updates = {'expected_vm_state': 'meow'}
+ fake_instance = {'vm_state': 'nomatch'}
+
+ self.mox.StubOutWithMock(sqlalchemy_api, '_instance_get_by_uuid')
+
+ sqlalchemy_api._instance_get_by_uuid(ctxt, uuid,
+ session=mox.IgnoreArg()).AndReturn(fake_instance)
+
+ self.mox.ReplayAll()
+
+ self.assertRaises(exception.UnexpectedVMStateError,
+ db.instance_update, ctxt, uuid, updates)
+
def test_network_create_safe(self):
ctxt = context.get_admin_context()
values = {'host': 'localhost', 'project_id': 'project1'}