diff options
-rw-r--r-- | nova/conductor/api.py | 12 | ||||
-rw-r--r-- | nova/conductor/manager.py | 8 | ||||
-rw-r--r-- | nova/conductor/rpcapi.py | 9 | ||||
-rw-r--r-- | nova/db/api.py | 41 | ||||
-rw-r--r-- | nova/db/sqlalchemy/api.py | 119 | ||||
-rw-r--r-- | nova/db/sqlalchemy/migrate_repo/versions/148_add_instance_actions.py | 101 | ||||
-rw-r--r-- | nova/db/sqlalchemy/models.py | 29 | ||||
-rw-r--r-- | nova/exception.py | 9 | ||||
-rw-r--r-- | nova/tests/conductor/test_conductor.py | 12 | ||||
-rw-r--r-- | nova/tests/test_db_api.py | 219 |
10 files changed, 558 insertions, 1 deletions
diff --git a/nova/conductor/api.py b/nova/conductor/api.py index a95332f08..87e75f274 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -237,6 +237,12 @@ class LocalAPI(object): def service_get_all_compute_by_host(self, context, host): return self._manager.service_get_all_by(context, 'compute', host) + def action_event_start(self, context, values): + return self._manager.action_event_start(context, values) + + def action_event_finish(self, context, values): + return self._manager.action_event_finish(context, values) + class API(object): """Conductor API that does updates via RPC to the ConductorManager""" @@ -428,3 +434,9 @@ class API(object): def service_get_all_compute_by_host(self, context, host): return self.conductor_rpcapi.service_get_all_by(context, 'compute', host) + + def action_event_start(self, context, values): + return self.conductor_rpcapi.action_event_start(context, values) + + def action_event_finish(self, context, values): + return self.conductor_rpcapi.action_event_finish(context, values) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 9a1a62712..96443c834 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -43,7 +43,7 @@ datetime_fields = ['launched_at', 'terminated_at'] class ConductorManager(manager.SchedulerDependentManager): """Mission: TBD""" - RPC_API_VERSION = '1.24' + RPC_API_VERSION = '1.25' def __init__(self, *args, **kwargs): super(ConductorManager, self).__init__(service_name='conductor', @@ -260,3 +260,9 @@ class ConductorManager(manager.SchedulerDependentManager): result = self.db.service_get_all_by_host(context, host) return jsonutils.to_primitive(result) + + def action_event_start(self, context, values): + return self.db.action_event_start(context, values) + + def action_event_finish(self, context, values): + return self.db.action_event_finish(context, values) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index c7143ade9..f11208e2f 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -57,6 +57,7 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): 1.23 - Added instance_get_all Un-Deprecate instance_get_all_by_host 1.24 - Added instance_get + 1.25 - Added action_event_start and action_event_finish """ BASE_RPC_API_VERSION = '1.0' @@ -261,3 +262,11 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): def instance_get_all_by_host(self, context, host): msg = self.make_msg('instance_get_all_by_host', host=host) return self.call(context, msg, version='1.23') + + def action_event_start(self, context, values): + msg = self.make_msg('action_event_start', values=values) + return self.call(context, msg, version='1.25') + + def action_event_finish(self, context, values): + msg = self.make_msg('action_event_finish', values=values) + return self.call(context, msg, version='1.25') diff --git a/nova/db/api.py b/nova/db/api.py index 7f202862e..b9f188a45 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1621,6 +1621,47 @@ def instance_fault_get_by_instance_uuids(context, instance_uuids): #################### +def action_start(context, values): + """Start an action for an instance""" + return IMPL.action_start(context, values) + + +def action_finish(context, values): + """Finish an action for an instance""" + return IMPL.action_finish(context, values) + + +def actions_get(context, uuid): + """Get all instance actions for the provided instance""" + return IMPL.actions_get(context, uuid) + + +def action_get_by_id(context, uuid, action_id): + """Get the action by id and given instance""" + return IMPL.action_get_by_id(context, uuid, action_id) + + +def action_event_start(context, values): + """Start an event on an instance action""" + return IMPL.action_event_start(context, values) + + +def action_event_finish(context, values): + """Finish an event on an instance action""" + return IMPL.action_event_finish(context, values) + + +def action_events_get(context, action_id): + return IMPL.action_events_get(context, action_id) + + +def action_event_get_by_id(context, action_id, event_id): + return IMPL.action_event_get_by_id(context, action_id, event_id) + + +#################### + + def get_ec2_instance_id_by_uuid(context, instance_id): """Get ec2 id through uuid from instance_id_mappings table""" return IMPL.get_ec2_instance_id_by_uuid(context, instance_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 66ecc8bf6..8687ab87b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -4560,6 +4560,125 @@ def instance_fault_get_by_instance_uuids(context, instance_uuids): ################## +def action_start(context, values): + action_ref = models.InstanceAction() + action_ref.update(values) + action_ref.save() + return action_ref + + +def action_finish(context, values): + session = get_session() + with session.begin(): + action_ref = model_query(context, models.InstanceAction, + session=session).\ + filter_by(instance_uuid=values['instance_uuid']).\ + filter_by(request_id=values['request_id']).\ + first() + + if not action_ref: + raise exception.InstanceActionNotFound( + request_id=values['request_id'], + instance_uuid=values['instance_uuid']) + + action_ref.update(values) + return action_ref + + +def actions_get(context, instance_uuid): + """Get all instance actions for the provided uuid.""" + actions = model_query(context, models.InstanceAction).\ + filter_by(instance_uuid=instance_uuid).\ + order_by(desc("created_at")).\ + all() + return actions + + +def action_get_by_id(context, instance_uuid, action_id): + """Get the action by id and given instance""" + action = model_query(context, models.InstanceAction).\ + filter_by(instance_uuid=instance_uuid).\ + filter_by(id=action_id).\ + first() + + return action + + +def _action_get_by_request_id(context, instance_uuid, request_id, + session=None): + result = model_query(context, models.InstanceAction, session=session).\ + filter_by(instance_uuid=instance_uuid).\ + filter_by(request_id=request_id).\ + first() + return result + + +def action_event_start(context, values): + """Start an event on an instance action""" + session = get_session() + with session.begin(): + action = _action_get_by_request_id(context, values['instance_uuid'], + values['request_id'], session) + + if not action: + raise exception.InstanceActionNotFound( + request_id=values['request_id'], + instance_uuid=values['instance_uuid']) + + values['action_id'] = action['id'] + + event_ref = models.InstanceActionEvent() + event_ref.update(values) + event_ref.save(session=session) + return event_ref + + +def action_event_finish(context, values): + """Finish an event on an instance action""" + session = get_session() + with session.begin(): + action = _action_get_by_request_id(context, values['instance_uuid'], + values['request_id'], session) + + if not action: + raise exception.InstanceActionNotFound( + request_id=values['request_id'], + instance_uuid=values['instance_uuid']) + + event_ref = model_query(context, models.InstanceActionEvent, + session=session).\ + filter_by(action_id=action['id']).\ + filter_by(event=values['event']).\ + first() + + if not event_ref: + raise exception.InstanceActionEventNotFound(action_id=action['id'], + event=values['event']) + event_ref.update(values) + return event_ref + + +def action_events_get(context, action_id): + events = model_query(context, models.InstanceActionEvent).\ + filter_by(action_id=action_id).\ + order_by(desc("created_at")).\ + all() + + return events + + +def action_event_get_by_id(context, action_id, event_id): + event = model_query(context, models.InstanceActionEvent).\ + filter_by(action_id=action_id).\ + filter_by(id=event_id).\ + first() + + return event + + +################## + + @require_context def ec2_instance_create(context, instance_uuid, id=None): """Create ec2 compatable instance by provided uuid""" diff --git a/nova/db/sqlalchemy/migrate_repo/versions/148_add_instance_actions.py b/nova/db/sqlalchemy/migrate_repo/versions/148_add_instance_actions.py new file mode 100644 index 000000000..6adfb1dc1 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/148_add_instance_actions.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# +# 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 sqlalchemy import Boolean +from sqlalchemy import Column +from sqlalchemy import DateTime +from sqlalchemy import ForeignKey +from sqlalchemy import Index +from sqlalchemy import Integer +from sqlalchemy import MetaData +from sqlalchemy import String +from sqlalchemy import Table +from sqlalchemy import Text + +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + instance_actions = Table('instance_actions', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('action', String(length=255)), + Column('instance_uuid', String(length=36)), + Column('request_id', String(length=255)), + Column('user_id', String(length=255)), + Column('project_id', String(length=255)), + Column('start_time', DateTime), + Column('finish_time', DateTime), + Column('message', String(length=255)), + mysql_engine='InnoDB', + mysql_charset='utf8', + ) + + instance_actions_events = Table('instance_actions_events', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('event', String(length=255)), + Column('action_id', Integer, ForeignKey('instance_actions.id')), + Column('start_time', DateTime), + Column('finish_time', DateTime), + Column('result', String(length=255)), + Column('traceback', Text), + mysql_engine='InnoDB', + mysql_charset='utf8', + ) + + try: + instance_actions.create() + instance_actions_events.create() + except Exception: + LOG.exception("Exception while creating table 'instance_actions' or " + "'instance_actions_events'") + meta.drop_all(tables=[instance_actions, instance_actions_events]) + raise + + Index('instance_uuid_idx', + instance_actions.c.instance_uuid).create(migrate_engine) + Index('request_id_idx', + instance_actions.c.request_id).create(migrate_engine) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + try: + instance_actions = Table('instance_actions', meta, autoload=True) + instance_actions.drop() + except Exception: + LOG.exception("Exception dropping table 'instance_actions'") + + try: + instance_actions_events = Table('instance_actions_events', meta, + autoload=True) + instance_actions_events.drop() + except Exception: + LOG.exception("Exception dropping table 'instance_actions_events") diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2d3e23c26..01251cd42 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -984,6 +984,35 @@ class InstanceFault(BASE, NovaBase): details = Column(Text) +class InstanceAction(BASE, NovaBase): + """Track client actions on an instance""" + __tablename__ = 'instance_actions' + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + action = Column(String(255)) + instance_uuid = Column(String(36), + ForeignKey('instances.uuid'), + nullable=False) + request_id = Column(String(255)) + user_id = Column(String(255)) + project_id = Column(String(255)) + start_time = Column(DateTime, default=timeutils.utcnow) + finish_time = Column(DateTime) + message = Column(String(255)) + + +class InstanceActionEvent(BASE, NovaBase): + """Track events that occur during an InstanceAction""" + __tablename__ = 'instance_actions_events' + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + event = Column(String(255)) + action_id = Column(Integer, ForeignKey('instance_actions.id'), + nullable=False) + start_time = Column(DateTime, default=timeutils.utcnow) + finish_time = Column(DateTime) + result = Column(String(255)) + traceback = Column(Text) + + class InstanceIdMapping(BASE, NovaBase): """Compatibility layer for the EC2 instance service""" __tablename__ = 'instance_id_mappings' diff --git a/nova/exception.py b/nova/exception.py index 9507a0088..7ec23d32d 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1074,6 +1074,15 @@ class UnexpectedTaskStateError(NovaException): "the actual state is %(actual)s") +class InstanceActionNotFound(NovaException): + message = _("Action for request_id %(request_id)s on instance" + " %(instance_uuid)s not found") + + +class InstanceActionEventNotFound(NovaException): + message = _("Event %(event)s not found for action id %(action_id)s") + + class CryptoCAFileNotFound(FileNotFound): message = _("The CA file for %(project)s could not be found") diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py index fd87e420b..6b579e131 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -86,6 +86,18 @@ class _BaseTestCase(object): self.assertEqual(instance['vm_state'], vm_states.STOPPED) self.assertEqual(new_inst['vm_state'], instance['vm_state']) + def test_action_event_start(self): + self.mox.StubOutWithMock(db, 'action_event_start') + db.action_event_start(self.context, mox.IgnoreArg()) + self.mox.ReplayAll() + self.conductor.action_event_start(self.context, {}) + + def test_action_event_finish(self): + self.mox.StubOutWithMock(db, 'action_event_finish') + db.action_event_finish(self.context, mox.IgnoreArg()) + self.mox.ReplayAll() + self.conductor.action_event_finish(self.context, {}) + def test_instance_update_invalid_key(self): # NOTE(danms): the real DB API call ignores invalid keys if self.db == None: diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 0aaa42a11..3ea853f0e 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -539,6 +539,225 @@ class DbApiTestCase(test.TestCase): expected = {uuids[0]: [], uuids[1]: []} self.assertEqual(expected, instance_faults) + def test_instance_action_start(self): + """Create an instance action""" + ctxt = context.get_admin_context() + uuid = str(stdlib_uuid.uuid4()) + + start_time = timeutils.utcnow() + action_values = {'action': 'run_instance', + 'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'user_id': ctxt.user_id, + 'project_id': ctxt.project_id, + 'start_time': start_time} + db.action_start(ctxt, action_values) + + # Retrieve the action to ensure it was successfully added + actions = db.actions_get(ctxt, uuid) + self.assertEqual(1, len(actions)) + self.assertEqual('run_instance', actions[0]['action']) + self.assertEqual(start_time, actions[0]['start_time']) + self.assertEqual(ctxt.request_id, actions[0]['request_id']) + self.assertEqual(ctxt.user_id, actions[0]['user_id']) + self.assertEqual(ctxt.project_id, actions[0]['project_id']) + + def test_instance_action_finish(self): + """Create an instance action""" + ctxt = context.get_admin_context() + uuid = str(stdlib_uuid.uuid4()) + + start_time = timeutils.utcnow() + action_start_values = {'action': 'run_instance', + 'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'user_id': ctxt.user_id, + 'project_id': ctxt.project_id, + 'start_time': start_time} + db.action_start(ctxt, action_start_values) + + finish_time = timeutils.utcnow() + datetime.timedelta(seconds=5) + action_finish_values = {'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'finish_time': finish_time} + db.action_finish(ctxt, action_finish_values) + + # Retrieve the action to ensure it was successfully added + actions = db.actions_get(ctxt, uuid) + self.assertEqual(1, len(actions)) + self.assertEqual('run_instance', actions[0]['action']) + self.assertEqual(start_time, actions[0]['start_time']) + self.assertEqual(finish_time, actions[0]['finish_time']) + self.assertEqual(ctxt.request_id, actions[0]['request_id']) + self.assertEqual(ctxt.user_id, actions[0]['user_id']) + self.assertEqual(ctxt.project_id, actions[0]['project_id']) + + def test_instance_actions_get_by_instance(self): + """Ensure we can get actions by UUID""" + ctxt1 = context.get_admin_context() + ctxt2 = context.get_admin_context() + uuid1 = str(stdlib_uuid.uuid4()) + uuid2 = str(stdlib_uuid.uuid4()) + + action_values = {'action': 'run_instance', + 'instance_uuid': uuid1, + 'request_id': ctxt1.request_id, + 'user_id': ctxt1.user_id, + 'project_id': ctxt1.project_id, + 'start_time': timeutils.utcnow()} + db.action_start(ctxt1, action_values) + action_values['action'] = 'resize' + db.action_start(ctxt1, action_values) + + action_values = {'action': 'reboot', + 'instance_uuid': uuid2, + 'request_id': ctxt2.request_id, + 'user_id': ctxt2.user_id, + 'project_id': ctxt2.project_id, + 'start_time': timeutils.utcnow()} + db.action_start(ctxt2, action_values) + db.action_start(ctxt2, action_values) + + # Retrieve the action to ensure it was successfully added + actions = db.actions_get(ctxt1, uuid1) + self.assertEqual(2, len(actions)) + self.assertEqual('resize', actions[0]['action']) + self.assertEqual('run_instance', actions[1]['action']) + + def test_instance_action_get_by_instance_and_action(self): + """Ensure we can get an action by instance UUID and action id""" + ctxt1 = context.get_admin_context() + ctxt2 = context.get_admin_context() + uuid1 = str(stdlib_uuid.uuid4()) + uuid2 = str(stdlib_uuid.uuid4()) + + action_values = {'action': 'run_instance', + 'instance_uuid': uuid1, + 'request_id': ctxt1.request_id, + 'user_id': ctxt1.user_id, + 'project_id': ctxt1.project_id, + 'start_time': timeutils.utcnow()} + db.action_start(ctxt1, action_values) + action_values['action'] = 'resize' + db.action_start(ctxt1, action_values) + + action_values = {'action': 'reboot', + 'instance_uuid': uuid2, + 'request_id': ctxt2.request_id, + 'user_id': ctxt2.user_id, + 'project_id': ctxt2.project_id, + 'start_time': timeutils.utcnow()} + db.action_start(ctxt2, action_values) + db.action_start(ctxt2, action_values) + + actions = db.actions_get(ctxt1, uuid1) + action_id = actions[0]['id'] + action = db.action_get_by_id(ctxt1, uuid1, action_id) + self.assertEqual('resize', action['action']) + self.assertEqual(ctxt1.request_id, action['request_id']) + + def test_instance_action_event_start(self): + """Create an instance action event""" + ctxt = context.get_admin_context() + uuid = str(stdlib_uuid.uuid4()) + + start_time = timeutils.utcnow() + action_values = {'action': 'run_instance', + 'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'user_id': ctxt.user_id, + 'project_id': ctxt.project_id, + 'start_time': start_time} + action = db.action_start(ctxt, action_values) + + event_values = {'event': 'schedule', + 'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'start_time': start_time} + db.action_event_start(ctxt, event_values) + + # Retrieve the event to ensure it was successfully added + events = db.action_events_get(ctxt, action['id']) + self.assertEqual(1, len(events)) + self.assertEqual('schedule', events[0]['event']) + self.assertEqual(start_time, events[0]['start_time']) + + def test_instance_action_event_finish(self): + """Finish an instance action event""" + ctxt = context.get_admin_context() + uuid = str(stdlib_uuid.uuid4()) + + start_time = timeutils.utcnow() + action_values = {'action': 'run_instance', + 'instance_uuid': uuid, + 'request_id': ctxt.request_id, + 'user_id': ctxt.user_id, + 'project_id': ctxt.project_id, + 'start_time': start_time} + action = db.action_start(ctxt, action_values) + + event_values = {'event': 'schedule', + 'request_id': ctxt.request_id, + 'instance_uuid': uuid, + 'start_time': start_time} + db.action_event_start(ctxt, event_values) + + finish_time = timeutils.utcnow() + datetime.timedelta(seconds=5) + event_finish_values = {'event': 'schedule', + 'request_id': ctxt.request_id, + 'instance_uuid': uuid, + 'finish_time': finish_time} + db.action_event_finish(ctxt, event_finish_values) + + # Retrieve the event to ensure it was successfully added + events = db.action_events_get(ctxt, action['id']) + self.assertEqual(1, len(events)) + self.assertEqual('schedule', events[0]['event']) + self.assertEqual(start_time, events[0]['start_time']) + self.assertEqual(finish_time, events[0]['finish_time']) + + def test_instance_action_event_get_by_id(self): + """Get a specific instance action event""" + ctxt1 = context.get_admin_context() + ctxt2 = context.get_admin_context() + uuid1 = str(stdlib_uuid.uuid4()) + uuid2 = str(stdlib_uuid.uuid4()) + + action_values = {'action': 'run_instance', + 'instance_uuid': uuid1, + 'request_id': ctxt1.request_id, + 'user_id': ctxt1.user_id, + 'project_id': ctxt1.project_id, + 'start_time': timeutils.utcnow()} + added_action = db.action_start(ctxt1, action_values) + + action_values = {'action': 'reboot', + 'instance_uuid': uuid2, + 'request_id': ctxt2.request_id, + 'user_id': ctxt2.user_id, + 'project_id': ctxt2.project_id, + 'start_time': timeutils.utcnow()} + db.action_start(ctxt2, action_values) + + start_time = timeutils.utcnow() + event_values = {'event': 'schedule', + 'instance_uuid': uuid1, + 'request_id': ctxt1.request_id, + 'start_time': start_time} + added_event = db.action_event_start(ctxt1, event_values) + + event_values = {'event': 'reboot', + 'instance_uuid': uuid2, + 'request_id': ctxt2.request_id, + 'start_time': timeutils.utcnow()} + db.action_event_start(ctxt2, event_values) + + # Retrieve the event to ensure it was successfully added + event = db.action_event_get_by_id(ctxt1, added_action['id'], + added_event['id']) + self.assertEqual('schedule', event['event']) + self.assertEqual(start_time, event['start_time']) + def test_dns_registration(self): domain1 = 'test.domain.one' domain2 = 'test.domain.two' |