Create Statuses objects for Leases

In order to have statuses for leases, we need to add some fieds in the
Lease DB model, but we also need to define some handlers for managing
lease states and persistences in DB.

The proposal here is to have an in-memory object for managing lifecycles
of various Climate concepts (here Lease) with asynchronous method for
writing back the value in DB. By default, the behaviour is synchronous
(using the autosave=True parameter) but can be set to async if needed.

Uses of these State objects can be found in unittests.
Implementation within Climate code (ie. the 'calls') will happen in a
separate commit in order to ease reviews to small chunks (this one is
still big, I know...)

Partially implements: blueprint lease-state

Change-Id: I6e8e03a73c838e8e66a76d5242816dc222cb449b
This commit is contained in:
Sylvain Bauza 2014-04-28 16:36:38 +02:00
parent 8518a48424
commit db86f57cf4
8 changed files with 347 additions and 1 deletions

View File

@ -59,6 +59,15 @@ class Lease(base._Base):
before_end_notification = types.Datetime(service.LEASE_DATE_FORMAT)
"Datetime when notifications will be sent before lease ending"
action = wtypes.text
"The current action running"
status = wtypes.text
"The status of the action running"
status_reason = wtypes.text
"A brief description of the status, if any"
@classmethod
def sample(cls):
return cls(id=u'2bb8720a-0873-4d97-babf-0d906851a1eb',
@ -71,7 +80,10 @@ class Lease(base._Base):
reservations=[{u'resource_id': u'1234',
u'resource_type': u'virtual:instance'}],
events=[],
before_end_notification=u'2014-02-01 10:37'
before_end_notification=u'2014-02-01 10:37',
action=u'START',
status=u'COMPLETE',
status_reason=u'Lease currently running',
)

View File

@ -0,0 +1,54 @@
# Copyright 2014 OpenStack Foundation.
#
# 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.
"""Add status to leases
Revision ID: 23d6240b51b2
Revises: 2bcfe76b0474
Create Date: 2014-04-25 10:41:09.183430
"""
# revision identifiers, used by Alembic.
revision = '23d6240b51b2'
down_revision = '2bcfe76b0474'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('leases', sa.Column(
'action', sa.String(length=255), nullable=True))
op.add_column('leases', sa.Column(
'status', sa.String(length=255), nullable=True))
op.add_column('leases', sa.Column(
'status_reason', sa.String(length=255), nullable=True))
def downgrade():
engine = op.get_bind().engine
if engine.name == 'sqlite':
# Only for testing purposes with sqlite
op.execute('CREATE TABLE tmp_leases as SELECT created_at, updated_at, '
'id, name, user_id, project_id, start_date, '
'end_date, trust_id FROM leases')
op.execute('DROP TABLE leases')
op.execute('ALTER TABLE tmp_leases RENAME TO leases')
return
op.drop_column('leases', 'action')
op.drop_column('leases', 'status')
op.drop_column('leases', 'status_reason')

View File

@ -64,6 +64,9 @@ class Lease(mb.ClimateBase):
backref='lease', lazy='joined')
events = relationship('Event', cascade="all,delete",
backref='lease', lazy='joined')
action = sa.Column(sa.String(255))
status = sa.Column(sa.String(255))
status_reason = sa.Column(sa.String(255))
def to_dict(self):
d = super(Lease, self).to_dict()

View File

@ -128,3 +128,12 @@ class NotEnoughHostsAvailable(exceptions.ClimateException):
class MalformedRequirements(exceptions.ClimateException):
code = 400
msg_fmt = _("Malformed requirements %(rqrms)s")
class InvalidState(exceptions.ClimateException):
code = 409
msg_fmt = _("Invalid State %(state)s for %(id)s")
class InvalidStateUpdate(InvalidState):
msg_fmt = _("Unable to update ID %(id)s state with %(action)s:%(status)s")

98
climate/states.py Normal file
View File

@ -0,0 +1,98 @@
# Copyright (c) 2014 Red Hat.
#
# 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.
"""Actions and states for Climate objects."""
import abc
import six
from climate.db import api as db_api
from climate.db import exceptions as db_exc
from climate.manager import exceptions as mgr_exc
from climate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class ObjectState(object):
ACTIONS = (CREATE, DELETE, UPDATE
) = ('CREATE', 'DELETE', 'UPDATE')
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
id = None
action = None
status = None
status_reason = None
def __init__(self, id, autosave=True,
action=None, status=None, status_reason=None):
self.id = id
self.autosave = autosave
if action is not None and status is not None:
self.update(action, status, status_reason)
def current(self):
return {'action': self.action,
'status': self.status,
'status_reason': self.status_reason}
def update(self, action, status, status_reason=None):
if action not in self.ACTIONS or status not in self.STATUSES:
raise mgr_exc.InvalidStateUpdate(id=self.id, action=action,
status=status)
self.action = action
self.status = status
self.status_reason = status_reason
if self.autosave is True:
self.save()
@abc.abstractmethod
def save():
pass
class LeaseState(ObjectState):
ACTIONS = (CREATE, DELETE, UPDATE, START, STOP
) = ('CREATE', 'DELETE', 'UPDATE', 'START', 'STOP')
def __init__(self, id, autosave=True,
action=None, status=None, status_reason=None):
if action is None or status is None:
# NOTE(sbauza): The lease can be not yet in DB, so lease_get can
# return None
lease = db_api.lease_get(id) or {}
action = lease.get('action', action)
status = lease.get('status', status)
status_reason = lease.get('status_reason', status_reason)
super(LeaseState, self).__init__(id, autosave,
action, status, status_reason)
def save(self):
try:
db_api.lease_update(self.id, self.current())
except db_exc.ClimateDBException:
# Lease can be not yet in DB, we must first write it
raise mgr_exc.InvalidState(id=self.id, state=self.current())
return self.current()
# NOTE(sbauza): For convenient purpose
lease = LeaseState

View File

@ -41,6 +41,9 @@ def fake_lease(**kw):
}
]),
u'events': kw.get('events', []),
u'action': kw.get('action', 'START'),
u'status': kw.get('status', 'COMPLETE'),
u'status_reason': kw.get('status_reason', 'Lease currently running'),
}
@ -49,6 +52,9 @@ def fake_lease_request_body(exclude=[], **kw):
exclude.append('trust_id')
exclude.append('user_id')
exclude.append('project_id')
exclude.append('action')
exclude.append('status')
exclude.append('status_reason')
lease_body = fake_lease(**kw)
return dict((key, lease_body[key])
for key in lease_body if key not in exclude)

View File

@ -0,0 +1,39 @@
# Copyright (c) 2014 Red Hat.
#
# 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.
lease_data = {'id': '1',
'name': u'lease_test',
'start_date': u'2014-01-01 01:23',
'end_date': u'2014-02-01 13:37',
'user_id': u'efd8780712d24b389c705f5c2ac427ff',
'project_id': u'bd9431c18d694ad3803a8d4a6b89fd36',
'trust_id': u'35b17138b3644e6aa1318f3099c5be68',
'reservations': [{u'resource_id': u'1234',
u'resource_type': u'virtual:instance'}],
'events': [],
'before_end_notification': u'2014-02-01 10:37',
'action': None,
'status': None,
'status_reason': None}
def fake_lease(**kw):
_fake_lease = lease_data.copy()
_fake_lease.update(**kw)
return _fake_lease
def fake_lease_update(id, values):
return fake_lease(id=id, **values)

View File

@ -0,0 +1,125 @@
# Copyright (c) 2014 Red Hat.
#
# 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.
"""Test of States."""
from climate.db import api as db_api
from climate.db import exceptions as db_exc
from climate.manager import exceptions as mgr_exc
from climate import states
from climate import tests
from climate.tests import fake_lease
class LeaseStateTestCase(tests.TestCase):
def setUp(self):
super(LeaseStateTestCase, self).setUp()
self.db_api = db_api
self.lease_update = self.patch(self.db_api, 'lease_update')
self.lease_update.side_effect = fake_lease.fake_lease_update
self.lease_get = self.patch(self.db_api, 'lease_get')
self.lease_get.return_value = fake_lease.fake_lease()
def test_state_init(self):
self.leaseState = states.LeaseState(id=1)
self.lease_get.assert_called_once_with(1)
expected = {'action': None,
'status': None,
'status_reason': None}
self.assertEqual(expected, self.leaseState.current())
def test_state_init_with_args(self):
self.leaseState = states.LeaseState(
id=1,
action=states.lease.CREATE,
status=states.lease.IN_PROGRESS)
self.assertEqual(0, self.lease_get.call_count)
expected = {'action': 'CREATE',
'status': 'IN_PROGRESS',
'status_reason': None}
self.assertEqual(expected, self.leaseState.current())
def test_state_init_with_no_existing_lease(self):
self.lease_get.return_value = None
self.leaseState = states.LeaseState(id=1)
expected = {'action': None,
'status': None,
'status_reason': None}
self.assertEqual(expected, self.leaseState.current())
def test_state_attributes(self):
self.leaseState = states.LeaseState(
id=1,
action=states.lease.CREATE,
status=states.lease.IN_PROGRESS)
self.assertEqual(self.leaseState.action, states.lease.CREATE)
self.assertEqual(self.leaseState.status, states.lease.IN_PROGRESS)
self.assertEqual(self.leaseState.status_reason, None)
def test_update_state_with_autosave(self):
self.leaseState = states.LeaseState(id=1, autosave=False)
self.leaseState.autosave = True
self.leaseState.update(action=states.lease.CREATE,
status=states.lease.IN_PROGRESS,
status_reason="Creating Lease...")
expected = {'action': 'CREATE',
'status': 'IN_PROGRESS',
'status_reason': "Creating Lease..."}
self.lease_update.assert_called_once_with(1, expected)
self.assertEqual(expected, self.leaseState.current())
def test_update_state_with_noautosave(self):
self.leaseState = states.LeaseState(id=1, autosave=False)
self.leaseState.update(action=states.lease.CREATE,
status=states.lease.IN_PROGRESS,
status_reason="Creating Lease...")
expected = {'action': 'CREATE',
'status': 'IN_PROGRESS',
'status_reason': "Creating Lease..."}
self.assertEqual(0, self.lease_update.call_count)
self.assertEqual(expected, self.leaseState.current())
def test_update_state_with_incorrect_action_status(self):
self.leaseState = states.LeaseState(id=1)
self.assertRaises(mgr_exc.InvalidStateUpdate, self.leaseState.update,
action='foo', status=states.lease.IN_PROGRESS)
self.assertRaises(mgr_exc.InvalidStateUpdate, self.leaseState.update,
action=states.lease.CREATE, status='bar')
def test_save_state(self):
self.leaseState = states.LeaseState(id=1, autosave=False)
self.leaseState.update(action=states.lease.CREATE,
status=states.lease.IN_PROGRESS,
status_reason="Creating Lease...")
self.assertEqual(0, self.lease_update.call_count)
self.leaseState.save()
values = {'action': 'CREATE',
'status': 'IN_PROGRESS',
'status_reason': "Creating Lease..."}
self.lease_update.assert_called_once_with(1, values)
def test_save_state_with_nonexisting_lease(self):
def fake_lease_update_raise(id, values):
raise db_exc.ClimateDBException
self.lease_update.side_effect = fake_lease_update_raise
self.leaseState = states.LeaseState(id=1, autosave=False)
self.leaseState.update(action=states.lease.CREATE,
status=states.lease.IN_PROGRESS,
status_reason="Creating Lease...")
self.assertRaises(mgr_exc.InvalidState, self.leaseState.save)