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:
parent
8518a48424
commit
db86f57cf4
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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')
|
@ -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()
|
||||
|
@ -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
98
climate/states.py
Normal 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
|
@ -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)
|
||||
|
39
climate/tests/fake_lease.py
Normal file
39
climate/tests/fake_lease.py
Normal 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)
|
125
climate/tests/test_states.py
Normal file
125
climate/tests/test_states.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user