Skip to content

Commit 1f670df

Browse files
authored
Merge pull request #153 from tzumainn/lease-update
Allow leases to update end_time
2 parents 326fff7 + 39bb68f commit 1f670df

File tree

7 files changed

+358
-127
lines changed

7 files changed

+358
-127
lines changed

esi_leap/api/controllers/v1/lease.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,25 @@ def post(self, new_lease):
191191
lease.create(request)
192192
return Lease(**utils.lease_get_dict_with_added_info(lease))
193193

194+
@wsme_pecan.wsexpose(Lease, wtypes.text, body={wtypes.text: wtypes.text})
195+
def patch(self, lease_uuid, patch=None):
196+
request = pecan.request.context
197+
198+
lease = utils.check_lease_policy_and_retrieve(
199+
request, 'esi_leap:lease:update', lease_uuid)
200+
201+
# check that patch has acceptable fields; only end_time for now
202+
patch_keys = patch.keys()
203+
if not('end_time' in patch_keys and len(patch_keys) == 1):
204+
raise exception.LeaseInvalidPatch()
205+
206+
new_end_time = datetime.datetime.strptime(
207+
patch['end_time'], '%Y-%m-%dT%H:%M:%S')
208+
updates = {'end_time': new_end_time}
209+
lease.update(updates, request)
210+
211+
return Lease(**utils.lease_get_dict_with_added_info(lease))
212+
194213
@wsme_pecan.wsexpose(Lease, wtypes.text)
195214
def delete(self, lease_id):
196215
request = pecan.request.context

esi_leap/common/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class LeaseExceedMaxTimeRange(ESILeapException):
4545
'time range. The max time range is %(max_time)s days.')
4646

4747

48+
class LeaseInvalidPatch(ESILeapException):
49+
msg_fmt = _('Only the end_time field may be updated')
50+
51+
4852
class HTTPForbidden(ESILeapException):
4953
code = http_client.FORBIDDEN
5054
msg_fmt = _('Access was denied to %(rule)s.')

esi_leap/common/policy.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
'rule:is_admin or rule:is_owner',
5555
'Create lease',
5656
[{'path': '/leases', 'method': 'POST'}]),
57+
policy.DocumentedRuleDefault(
58+
'esi_leap:lease:update',
59+
'rule:is_admin',
60+
'Update lease',
61+
[{'path': '/leases/{lease_ident}', 'method': 'PATCH'}]),
5762
policy.DocumentedRuleDefault(
5863
'esi_leap:lease:get',
5964
'rule:is_admin or rule:is_lease_owner or rule:is_lease_lessee',

esi_leap/objects/lease.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -160,47 +160,48 @@ def get_all(cls, filters, context=None):
160160

161161
def create(self, context=None):
162162
updates = self.obj_get_changes()
163+
resource_type = updates['resource_type']
164+
resource_uuid = updates['resource_uuid']
165+
start_time = updates['start_time']
166+
end_time = updates['end_time']
167+
with utils.lock(utils.get_resource_lock_name(resource_type,
168+
resource_uuid),
169+
external=True):
170+
self.verify_time_range(
171+
start_time, end_time,
172+
updates.get('offer_uuid', None),
173+
updates.get('parent_lease_uuid', None),
174+
resource_type, resource_uuid)
163175

164-
with utils.lock(utils.get_resource_lock_name(updates['resource_type'],
165-
updates['resource_uuid']),
176+
db_lease = self.dbapi.lease_create(updates)
177+
self._from_db_object(context, self, db_lease)
178+
179+
def update(self, updates, context=None):
180+
# only allow updates to end_time right now
181+
if 'end_time' not in updates:
182+
return
183+
new_end_time = updates['end_time']
184+
with utils.lock(utils.get_resource_lock_name(self.resource_type,
185+
self.resource_uuid),
166186
external=True):
167-
if updates['start_time'] >= updates['end_time']:
187+
if self.start_time >= new_end_time:
168188
raise exception.InvalidTimeRange(
169189
resource='lease',
170-
start_time=str(updates['start_time']),
171-
end_time=str(updates['end_time'])
190+
start_time=str(self.start_time),
191+
end_time=str(new_end_time)
172192
)
173193

174-
# check availability
175-
if 'offer_uuid' in updates:
176-
# lease is being created from an offer
177-
related_offer = offer_obj.Offer.get(updates['offer_uuid'])
178-
179-
if related_offer.status != statuses.AVAILABLE:
180-
raise exception.OfferNotAvailable(
181-
offer_uuid=related_offer.uuid,
182-
status=related_offer.status)
183-
184-
related_offer.verify_availability(updates['start_time'],
185-
updates['end_time'])
186-
elif 'parent_lease_uuid' in updates:
187-
# lease is a child of an existing lease
188-
parent_lease = Lease.get(updates['parent_lease_uuid'])
189-
190-
if parent_lease.status != statuses.ACTIVE:
191-
raise exception.LeaseNotActive(
192-
updates['parent_lease_uuid'])
193-
194-
parent_lease.verify_child_availability(updates['start_time'],
195-
updates['end_time'])
196-
else:
197-
ro = get_resource_object(updates['resource_type'],
198-
updates['resource_uuid'])
199-
ro.verify_availability(updates['start_time'],
200-
updates['end_time'])
194+
# only need to check availabilities if new end time is greater
195+
# than previous end time
196+
if new_end_time > self.end_time:
197+
self.verify_time_range(
198+
self.end_time, new_end_time,
199+
self.offer_uuid, self.parent_lease_uuid,
200+
self.resource_type, self.resource_uuid)
201201

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

205206
def cancel(self, context=None):
206207
leases = Lease.get_all(
@@ -335,3 +336,38 @@ def deactivate(self, context, resource):
335336
notify.emit_end_notification(context, self,
336337
'delete', CRUD_NOTIFY_OBJ,
337338
node=resource)
339+
340+
@staticmethod
341+
def verify_time_range(start_time, end_time,
342+
offer_uuid, parent_lease_uuid,
343+
resource_type, resource_uuid):
344+
if start_time >= end_time:
345+
raise exception.InvalidTimeRange(
346+
resource='lease',
347+
start_time=str(start_time),
348+
end_time=str(end_time)
349+
)
350+
351+
# check availability
352+
if offer_uuid:
353+
# lease is related to an offer
354+
related_offer = offer_obj.Offer.get(offer_uuid)
355+
if related_offer.status != statuses.AVAILABLE:
356+
raise exception.OfferNotAvailable(
357+
offer_uuid=related_offer.uuid,
358+
status=related_offer.status)
359+
related_offer.verify_availability(start_time,
360+
end_time)
361+
elif parent_lease_uuid:
362+
# lease is a child of an existing lease
363+
parent_lease = Lease.get(parent_lease_uuid)
364+
if parent_lease.status != statuses.ACTIVE:
365+
raise exception.LeaseNotActive(
366+
parent_lease_uuid)
367+
parent_lease.verify_child_availability(start_time,
368+
end_time)
369+
else:
370+
ro = get_resource_object(resource_type,
371+
resource_uuid)
372+
ro.verify_availability(start_time, end_time)
373+
return

esi_leap/tests/api/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ def post_json(self, path, params, expect_errors=False, headers=None,
9898
headers=headers, extra_environ=extra_environ,
9999
status=status, method='post')
100100

101+
# borrowed from Ironic
102+
def patch_json(self, path, params, expect_errors=False, headers=None,
103+
extra_environ=None, status=None):
104+
"""Sends simulated HTTP PATCH request to Pecan test app.
105+
106+
:param path: url path of target service
107+
:param params: content for wsgi.input of request
108+
:param expect_errors: Boolean value; whether an error is expected based
109+
on request
110+
:param headers: a dictionary of headers to send along with the request
111+
:param extra_environ: a dictionary of environ variables to send along
112+
with the request
113+
:param status: expected status code of response
114+
"""
115+
return self._request_json(path=path, params=params,
116+
expect_errors=expect_errors,
117+
headers=headers, extra_environ=extra_environ,
118+
status=status, method='patch')
119+
101120
# borrowed from Ironic
102121
def delete_json(self, path, expect_errors=False, headers=None,
103122
extra_environ=None, status=None):

esi_leap/tests/api/controllers/v1/test_lease.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,72 @@ def test_post_non_admin_no_parent_lease(self, mock_create, mock_cra,
276276
mock_create.assert_not_called()
277277
self.assertEqual(http_client.FORBIDDEN, request.status_int)
278278

279+
@mock.patch('esi_leap.api.controllers.v1.utils.'
280+
'lease_get_dict_with_added_info')
281+
@mock.patch('esi_leap.objects.lease.Lease.update')
282+
@mock.patch('esi_leap.api.controllers.v1.utils.'
283+
'check_lease_policy_and_retrieve')
284+
def test_patch(self, mock_clpar, mock_lease_update, mock_lgdwai):
285+
mock_clpar.return_value = self.test_lease
286+
287+
data = {
288+
'end_time': '2016-09-16T19:20:30'
289+
}
290+
request = self.patch_json(
291+
"/leases/%s" % self.test_lease.uuid, data)
292+
293+
mock_clpar.assert_called_once_with(self.context,
294+
'esi_leap:lease:update',
295+
self.test_lease.uuid)
296+
mock_lease_update.assert_called_once()
297+
mock_lgdwai.assert_called_once()
298+
self.assertEqual(http_client.OK, request.status_int)
299+
300+
@mock.patch('esi_leap.api.controllers.v1.utils.'
301+
'lease_get_dict_with_added_info')
302+
@mock.patch('esi_leap.objects.lease.Lease.update')
303+
@mock.patch('esi_leap.api.controllers.v1.utils.'
304+
'check_lease_policy_and_retrieve')
305+
def test_patch_no_end_time(self, mock_clpar, mock_lease_update,
306+
mock_lgdwai):
307+
mock_clpar.return_value = self.test_lease
308+
309+
data = {
310+
'name': 'foo'
311+
}
312+
request = self.patch_json(
313+
"/leases/%s" % self.test_lease.uuid, data, expect_errors=True)
314+
315+
mock_clpar.assert_called_once_with(self.context,
316+
'esi_leap:lease:update',
317+
self.test_lease.uuid)
318+
mock_lease_update.assert_not_called()
319+
mock_lgdwai.assert_not_called()
320+
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, request.status_int)
321+
322+
@mock.patch('esi_leap.api.controllers.v1.utils.'
323+
'lease_get_dict_with_added_info')
324+
@mock.patch('esi_leap.objects.lease.Lease.update')
325+
@mock.patch('esi_leap.api.controllers.v1.utils.'
326+
'check_lease_policy_and_retrieve')
327+
def test_patch_end_time_and_more(self, mock_clpar, mock_lease_update,
328+
mock_lgdwai):
329+
mock_clpar.return_value = self.test_lease
330+
331+
data = {
332+
'end_time': '2016-09-16T19:20:30',
333+
'name': 'foo'
334+
}
335+
request = self.patch_json(
336+
"/leases/%s" % self.test_lease.uuid, data, expect_errors=True)
337+
338+
mock_clpar.assert_called_once_with(self.context,
339+
'esi_leap:lease:update',
340+
self.test_lease.uuid)
341+
mock_lease_update.assert_not_called()
342+
mock_lgdwai.assert_not_called()
343+
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, request.status_int)
344+
279345
@mock.patch('esi_leap.common.ironic.get_node_list')
280346
@mock.patch('esi_leap.common.keystone.get_project_list')
281347
@mock.patch('esi_leap.api.controllers.v1.utils.'

0 commit comments

Comments
 (0)