Skip to content

Commit

Permalink
Merge pull request #153 from tzumainn/lease-update
Browse files Browse the repository at this point in the history
Allow leases to update end_time
  • Loading branch information
tzumainn authored Apr 2, 2024
2 parents 326fff7 + 39bb68f commit 1f670df
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 127 deletions.
19 changes: 19 additions & 0 deletions esi_leap/api/controllers/v1/lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,25 @@ def post(self, new_lease):
lease.create(request)
return Lease(**utils.lease_get_dict_with_added_info(lease))

@wsme_pecan.wsexpose(Lease, wtypes.text, body={wtypes.text: wtypes.text})
def patch(self, lease_uuid, patch=None):
request = pecan.request.context

lease = utils.check_lease_policy_and_retrieve(
request, 'esi_leap:lease:update', lease_uuid)

# check that patch has acceptable fields; only end_time for now
patch_keys = patch.keys()
if not('end_time' in patch_keys and len(patch_keys) == 1):
raise exception.LeaseInvalidPatch()

new_end_time = datetime.datetime.strptime(
patch['end_time'], '%Y-%m-%dT%H:%M:%S')
updates = {'end_time': new_end_time}
lease.update(updates, request)

return Lease(**utils.lease_get_dict_with_added_info(lease))

@wsme_pecan.wsexpose(Lease, wtypes.text)
def delete(self, lease_id):
request = pecan.request.context
Expand Down
4 changes: 4 additions & 0 deletions esi_leap/common/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class LeaseExceedMaxTimeRange(ESILeapException):
'time range. The max time range is %(max_time)s days.')


class LeaseInvalidPatch(ESILeapException):
msg_fmt = _('Only the end_time field may be updated')


class HTTPForbidden(ESILeapException):
code = http_client.FORBIDDEN
msg_fmt = _('Access was denied to %(rule)s.')
Expand Down
5 changes: 5 additions & 0 deletions esi_leap/common/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
'rule:is_admin or rule:is_owner',
'Create lease',
[{'path': '/leases', 'method': 'POST'}]),
policy.DocumentedRuleDefault(
'esi_leap:lease:update',
'rule:is_admin',
'Update lease',
[{'path': '/leases/{lease_ident}', 'method': 'PATCH'}]),
policy.DocumentedRuleDefault(
'esi_leap:lease:get',
'rule:is_admin or rule:is_lease_owner or rule:is_lease_lessee',
Expand Down
104 changes: 70 additions & 34 deletions esi_leap/objects/lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,47 +160,48 @@ def get_all(cls, filters, context=None):

def create(self, context=None):
updates = self.obj_get_changes()
resource_type = updates['resource_type']
resource_uuid = updates['resource_uuid']
start_time = updates['start_time']
end_time = updates['end_time']
with utils.lock(utils.get_resource_lock_name(resource_type,
resource_uuid),
external=True):
self.verify_time_range(
start_time, end_time,
updates.get('offer_uuid', None),
updates.get('parent_lease_uuid', None),
resource_type, resource_uuid)

with utils.lock(utils.get_resource_lock_name(updates['resource_type'],
updates['resource_uuid']),
db_lease = self.dbapi.lease_create(updates)
self._from_db_object(context, self, db_lease)

def update(self, updates, context=None):
# only allow updates to end_time right now
if 'end_time' not in updates:
return
new_end_time = updates['end_time']
with utils.lock(utils.get_resource_lock_name(self.resource_type,
self.resource_uuid),
external=True):
if updates['start_time'] >= updates['end_time']:
if self.start_time >= new_end_time:
raise exception.InvalidTimeRange(
resource='lease',
start_time=str(updates['start_time']),
end_time=str(updates['end_time'])
start_time=str(self.start_time),
end_time=str(new_end_time)
)

# check availability
if 'offer_uuid' in updates:
# lease is being created from an offer
related_offer = offer_obj.Offer.get(updates['offer_uuid'])

if related_offer.status != statuses.AVAILABLE:
raise exception.OfferNotAvailable(
offer_uuid=related_offer.uuid,
status=related_offer.status)

related_offer.verify_availability(updates['start_time'],
updates['end_time'])
elif 'parent_lease_uuid' in updates:
# lease is a child of an existing lease
parent_lease = Lease.get(updates['parent_lease_uuid'])

if parent_lease.status != statuses.ACTIVE:
raise exception.LeaseNotActive(
updates['parent_lease_uuid'])

parent_lease.verify_child_availability(updates['start_time'],
updates['end_time'])
else:
ro = get_resource_object(updates['resource_type'],
updates['resource_uuid'])
ro.verify_availability(updates['start_time'],
updates['end_time'])
# only need to check availabilities if new end time is greater
# than previous end time
if new_end_time > self.end_time:
self.verify_time_range(
self.end_time, new_end_time,
self.offer_uuid, self.parent_lease_uuid,
self.resource_type, self.resource_uuid)

db_lease = self.dbapi.lease_create(updates)
self._from_db_object(context, self, db_lease)
# lease is available in new range; set and save
self.end_time = new_end_time
self.save(context)

def cancel(self, context=None):
leases = Lease.get_all(
Expand Down Expand Up @@ -335,3 +336,38 @@ def deactivate(self, context, resource):
notify.emit_end_notification(context, self,
'delete', CRUD_NOTIFY_OBJ,
node=resource)

@staticmethod
def verify_time_range(start_time, end_time,
offer_uuid, parent_lease_uuid,
resource_type, resource_uuid):
if start_time >= end_time:
raise exception.InvalidTimeRange(
resource='lease',
start_time=str(start_time),
end_time=str(end_time)
)

# check availability
if offer_uuid:
# lease is related to an offer
related_offer = offer_obj.Offer.get(offer_uuid)
if related_offer.status != statuses.AVAILABLE:
raise exception.OfferNotAvailable(
offer_uuid=related_offer.uuid,
status=related_offer.status)
related_offer.verify_availability(start_time,
end_time)
elif parent_lease_uuid:
# lease is a child of an existing lease
parent_lease = Lease.get(parent_lease_uuid)
if parent_lease.status != statuses.ACTIVE:
raise exception.LeaseNotActive(
parent_lease_uuid)
parent_lease.verify_child_availability(start_time,
end_time)
else:
ro = get_resource_object(resource_type,
resource_uuid)
ro.verify_availability(start_time, end_time)
return
19 changes: 19 additions & 0 deletions esi_leap/tests/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ def post_json(self, path, params, expect_errors=False, headers=None,
headers=headers, extra_environ=extra_environ,
status=status, method='post')

# borrowed from Ironic
def patch_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None):
"""Sends simulated HTTP PATCH request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
return self._request_json(path=path, params=params,
expect_errors=expect_errors,
headers=headers, extra_environ=extra_environ,
status=status, method='patch')

# borrowed from Ironic
def delete_json(self, path, expect_errors=False, headers=None,
extra_environ=None, status=None):
Expand Down
66 changes: 66 additions & 0 deletions esi_leap/tests/api/controllers/v1/test_lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,72 @@ def test_post_non_admin_no_parent_lease(self, mock_create, mock_cra,
mock_create.assert_not_called()
self.assertEqual(http_client.FORBIDDEN, request.status_int)

@mock.patch('esi_leap.api.controllers.v1.utils.'
'lease_get_dict_with_added_info')
@mock.patch('esi_leap.objects.lease.Lease.update')
@mock.patch('esi_leap.api.controllers.v1.utils.'
'check_lease_policy_and_retrieve')
def test_patch(self, mock_clpar, mock_lease_update, mock_lgdwai):
mock_clpar.return_value = self.test_lease

data = {
'end_time': '2016-09-16T19:20:30'
}
request = self.patch_json(
"/leases/%s" % self.test_lease.uuid, data)

mock_clpar.assert_called_once_with(self.context,
'esi_leap:lease:update',
self.test_lease.uuid)
mock_lease_update.assert_called_once()
mock_lgdwai.assert_called_once()
self.assertEqual(http_client.OK, request.status_int)

@mock.patch('esi_leap.api.controllers.v1.utils.'
'lease_get_dict_with_added_info')
@mock.patch('esi_leap.objects.lease.Lease.update')
@mock.patch('esi_leap.api.controllers.v1.utils.'
'check_lease_policy_and_retrieve')
def test_patch_no_end_time(self, mock_clpar, mock_lease_update,
mock_lgdwai):
mock_clpar.return_value = self.test_lease

data = {
'name': 'foo'
}
request = self.patch_json(
"/leases/%s" % self.test_lease.uuid, data, expect_errors=True)

mock_clpar.assert_called_once_with(self.context,
'esi_leap:lease:update',
self.test_lease.uuid)
mock_lease_update.assert_not_called()
mock_lgdwai.assert_not_called()
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, request.status_int)

@mock.patch('esi_leap.api.controllers.v1.utils.'
'lease_get_dict_with_added_info')
@mock.patch('esi_leap.objects.lease.Lease.update')
@mock.patch('esi_leap.api.controllers.v1.utils.'
'check_lease_policy_and_retrieve')
def test_patch_end_time_and_more(self, mock_clpar, mock_lease_update,
mock_lgdwai):
mock_clpar.return_value = self.test_lease

data = {
'end_time': '2016-09-16T19:20:30',
'name': 'foo'
}
request = self.patch_json(
"/leases/%s" % self.test_lease.uuid, data, expect_errors=True)

mock_clpar.assert_called_once_with(self.context,
'esi_leap:lease:update',
self.test_lease.uuid)
mock_lease_update.assert_not_called()
mock_lgdwai.assert_not_called()
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, request.status_int)

@mock.patch('esi_leap.common.ironic.get_node_list')
@mock.patch('esi_leap.common.keystone.get_project_list')
@mock.patch('esi_leap.api.controllers.v1.utils.'
Expand Down
Loading

0 comments on commit 1f670df

Please sign in to comment.