Skip to content

Commit f0dbfbf

Browse files
authored
Merge pull request #612 from pjbreaux/feature.modify_609
Feature.modify 609
2 parents fcf95c6 + 824e058 commit f0dbfbf

4 files changed

Lines changed: 225 additions & 16 deletions

File tree

f5/bigip/resource.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ class BooleansToReduceHaveSameValue(F5SDKError):
166166
pass
167167

168168

169+
class AttemptedMutationOfReadOnly(F5SDKError):
170+
"""Read only parameters cannot be set."""
171+
pass
172+
173+
169174
class PathElement(LazyAttributeMixin):
170175
"""Base class to represent a URI path element that does not contain data.
171176
@@ -433,12 +438,54 @@ def __init__(self, container):
433438
"""
434439
super(ResourceBase, self).__init__(container)
435440

436-
def _update(self, **kwargs):
437-
"""wrapped with update, override that in a subclass to customize"""
441+
def _modify(self, **patch):
442+
"""Wrapped with modify, override in a subclass to customize."""
443+
444+
requests_params, patch_uri, session, read_only = \
445+
self._prepare_put_or_patch(patch)
446+
self._check_for_boolean_pair_reduction(patch)
447+
448+
read_only_mutations = []
449+
for attr in read_only:
450+
if attr in patch:
451+
read_only_mutations.append(attr)
452+
if read_only_mutations:
453+
msg = 'Attempted to mutate read-only attribute(s): %s' \
454+
% read_only_mutations
455+
raise AttemptedMutationOfReadOnly(msg)
456+
457+
response = session.patch(patch_uri, json=patch, **requests_params)
458+
self._local_update(response.json())
459+
460+
def modify(self, **patch):
461+
"""Modify the configuration of the resource on device based on patch
462+
463+
"""
464+
465+
self._modify(**patch)
466+
467+
def _check_for_boolean_pair_reduction(self, kwargs):
468+
"""Check if boolean pairs should be reduced in this resource."""
469+
470+
if 'reduction_forcing_pairs' in self._meta_data:
471+
for key1, key2 in self._meta_data['reduction_forcing_pairs']:
472+
kwargs = self._reduce_boolean_pair(kwargs, key1, key2)
473+
return kwargs
474+
475+
def _prepare_put_or_patch(self, kwargs):
476+
"""Retrieve the appropriate request items for put or patch calls."""
477+
438478
requests_params = self._handle_requests_params(kwargs)
439479
update_uri = self._meta_data['uri']
440480
session = self._meta_data['bigip']._meta_data['icr_session']
441481
read_only = self._meta_data.get('read_only_attributes', [])
482+
return requests_params, update_uri, session, read_only
483+
484+
def _update(self, **kwargs):
485+
"""wrapped with update, override that in a subclass to customize"""
486+
487+
requests_params, update_uri, session, read_only = \
488+
self._prepare_put_or_patch(kwargs)
442489

443490
# Get the current state of the object on BIG-IP® and check the
444491
# generation Use pop here because we don't want force in the data_dict
@@ -447,10 +494,7 @@ def _update(self, **kwargs):
447494
# generation has a known server-side error
448495
self._check_generation()
449496

450-
# Reduce any boolean pairs as specified by the meta_data entry below
451-
if 'reduction_forcing_pairs' in self._meta_data:
452-
for key1, key2 in self._meta_data['reduction_forcing_pairs']:
453-
kwargs = self._reduce_boolean_pair(kwargs, key1, key2)
497+
kwargs = self._check_for_boolean_pair_reduction(kwargs)
454498

455499
# Save the meta data so we can add it back into self after we
456500
# load the new object.

f5/bigip/test/test_resource.py

Lines changed: 146 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pytest
1717
import requests
1818

19+
from f5.bigip.resource import AttemptedMutationOfReadOnly
1920
from f5.bigip.resource import BooleansToReduceHaveSameValue
2021
from f5.bigip.resource import Collection
2122
from f5.bigip.resource import DeviceProvidesIncompatibleKey
@@ -35,6 +36,7 @@
3536
from f5.bigip.resource import UnnamedResource
3637
from f5.bigip.resource import UnregisteredKind
3738
from f5.bigip.resource import URICreationCollision
39+
from f5.bigip.tm.cm.sync_status import Sync_Status
3840
from f5.bigip.tm.ltm.virtual import Virtual
3941
from f5.sdk_exception import UnsupportedMethod
4042

@@ -57,7 +59,8 @@ def fake_rsrc():
5759
r._meta_data['uri'] = 'URI'
5860
r._meta_data['read_only_attributes'] = [u"READONLY"]
5961
attrs = {'put.return_value': MockResponse({u"generation": 0}),
60-
'get.return_value': MockResponse({u"generation": 0})}
62+
'get.return_value': MockResponse({u"generation": 0}),
63+
'patch.return_value': MockResponse({u"generation": 0})}
6164
mock_session = mock.MagicMock(**attrs)
6265
r._meta_data['bigip']._meta_data = {'icr_session': mock_session}
6366
return r
@@ -217,16 +220,18 @@ def test__create_with_Collision():
217220

218221

219222
class TestResource_update(object):
220-
def itest__check_generation_with_mismatch(self):
223+
def test__check_generation_with_mismatch(self):
221224
# generation is borked server-side
222225
r = Resource(mock.MagicMock())
223226
r._meta_data['allowed_lazy_attributes'] = []
224227
r._meta_data['uri'] = 'URI'
225228
r._meta_data['bigip']._meta_data['icr_session'].get.return_value =\
226229
MockResponse({u"generation": 0})
230+
r._meta_data['bigip']._meta_data['icr_session'].put.return_value =\
231+
MockResponse({u"generation": 0})
227232
r.generation = 1
228233
with pytest.raises(GenerationMismatch) as GMEIO:
229-
r.update(a=u"b")
234+
r.update(a=u"b", force=False)
230235
assert GMEIO.value.message ==\
231236
'The generation of the object on the BigIP (0)'\
232237
' does not match the current object(1)'
@@ -243,6 +248,7 @@ def test__meta_data_state(self):
243248
pre_meta = r._meta_data.copy()
244249
r.update(a=u"b")
245250
assert pre_meta == r._meta_data
251+
assert r.raw == r.__dict__
246252

247253
def test_Collection_removal(self):
248254
r = Resource(mock.MagicMock())
@@ -280,22 +286,22 @@ def test_read_only_removal(self):
280286

281287
def test_reduce_boolean_removes_enabled(self, fake_rsrc):
282288
fake_rsrc.update(enabled=False)
283-
pos, kwargs = fake_rsrc._meta_data['bigip']._meta_data['icr_session'].put.\
284-
call_args
289+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
290+
_meta_data['icr_session'].put.call_args
285291
assert kwargs['json']['disabled'] is True
286292
assert 'enabled' not in kwargs['json']
287293

288294
def test_reduce_boolean_removes_disabled(self, fake_rsrc):
289295
fake_rsrc.update(disabled=False)
290-
pos, kwargs = fake_rsrc._meta_data['bigip']._meta_data['icr_session'].put.\
291-
call_args
296+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
297+
_meta_data['icr_session'].put.call_args
292298
assert kwargs['json']['enabled'] is True
293299
assert 'disabled' not in kwargs['json']
294300

295301
def test_reduce_boolean_removes_nothing(self, fake_rsrc):
296302
fake_rsrc.update(partition='Common', name='test_create', enabled=True)
297-
pos, kwargs = fake_rsrc._meta_data['bigip']._meta_data['icr_session'].put.\
298-
call_args
303+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
304+
_meta_data['icr_session'].put.call_args
299305
assert kwargs['json']['enabled'] is True
300306
assert 'disabled' not in kwargs['json']
301307

@@ -313,6 +319,87 @@ def test_reduce_boolean_same_value(self, fake_rsrc):
313319
assert msg == ex.value.message
314320

315321

322+
class TestResource_modify(object):
323+
324+
def test__meta_data_state(self):
325+
r = Resource(mock.MagicMock())
326+
r._meta_data['allowed_lazy_attributes'] = []
327+
r._meta_data['uri'] = 'URI'
328+
r._meta_data['bigip']._meta_data['icr_session'].get.return_value =\
329+
MockResponse({u"generation": 0})
330+
r._meta_data['bigip']._meta_data['icr_session'].patch.return_value =\
331+
MockResponse({u"generation": 0})
332+
r.generation = 0
333+
pre_meta = r._meta_data.copy()
334+
r.modify(a=u"b")
335+
assert pre_meta == r._meta_data
336+
337+
def test_Collection_removal(self):
338+
r = Resource(mock.MagicMock())
339+
r._meta_data['allowed_lazy_attributes'] = []
340+
r._meta_data['uri'] = 'URI'
341+
attrs = {'patch.return_value': MockResponse({u"generation": 0}),
342+
'get.return_value': MockResponse({u"generation": 0})}
343+
mock_session = mock.MagicMock(**attrs)
344+
r._meta_data['bigip']._meta_data = {'icr_session': mock_session}
345+
r.generation = 0
346+
r.contained = Collection(mock.MagicMock())
347+
assert 'contained' in r.__dict__
348+
r.modify(a=u"b")
349+
submitted = r._meta_data['bigip']. \
350+
_meta_data['icr_session'].patch.call_args[1]['json']
351+
352+
assert 'contained' not in submitted
353+
354+
def test_read_only_validate(self):
355+
r = Resource(mock.MagicMock())
356+
r._meta_data['allowed_lazy_attributes'] = []
357+
r._meta_data['uri'] = 'URI'
358+
r._meta_data['read_only_attributes'] = [u"READONLY"]
359+
attrs = {'patch.return_value': MockResponse({u"generation": 0}),
360+
'get.return_value': MockResponse({u"generation": 0})}
361+
mock_session = mock.MagicMock(**attrs)
362+
r._meta_data['bigip']._meta_data = {'icr_session': mock_session}
363+
r.generation = 0
364+
with pytest.raises(AttemptedMutationOfReadOnly) as AMOROEIO:
365+
r.modify(READONLY=True)
366+
assert "READONLY" in AMOROEIO.value.message
367+
368+
def test_reduce_boolean_removes_enabled(self, fake_rsrc):
369+
fake_rsrc.modify(enabled=False)
370+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
371+
_meta_data['icr_session'].patch.call_args
372+
assert kwargs['json']['disabled'] is True
373+
assert 'enabled' not in kwargs['json']
374+
375+
def test_reduce_boolean_removes_disabled(self, fake_rsrc):
376+
fake_rsrc.modify(disabled=False)
377+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
378+
_meta_data['icr_session'].patch.call_args
379+
assert kwargs['json']['enabled'] is True
380+
assert 'disabled' not in kwargs['json']
381+
382+
def test_reduce_boolean_removes_nothing(self, fake_rsrc):
383+
fake_rsrc.modify(partition='Common', name='test_create', enabled=True)
384+
pos, kwargs = fake_rsrc._meta_data['bigip'].\
385+
_meta_data['icr_session'].patch.call_args
386+
assert kwargs['json']['enabled'] is True
387+
assert 'disabled' not in kwargs['json']
388+
389+
def test_reduce_boolean_same_value(self, fake_rsrc):
390+
with pytest.raises(BooleansToReduceHaveSameValue) as ex:
391+
fake_rsrc.modify(
392+
partition='Common',
393+
name='test_create',
394+
enabled=True,
395+
disabled=True
396+
)
397+
msg = 'Boolean pair, enabled and disabled, have same value: True. ' \
398+
'If both are given to this method, they cannot be the same, as ' \
399+
'this method cannot decide which one should be True.'
400+
assert msg == ex.value.message
401+
402+
316403
class TestResource_delete(object):
317404
def test_success(self):
318405
r = Resource(mock.MagicMock())
@@ -455,6 +542,34 @@ def test_success(self):
455542
x = r.load(partition='Common', name='test_load')
456543
assert x.selfLink == mockuri
457544

545+
def test_URICreationCollision(self):
546+
r = Virtual(mock.MagicMock())
547+
r._meta_data['allowed_lazy_attributes'] = []
548+
mockuri = "https://localhost:443/mgmt/tm/ltm/virtual/~Common~test_load"
549+
attrs = {'get.return_value':
550+
MockResponse(
551+
{
552+
u"generation": 0,
553+
u"selfLink": mockuri,
554+
u"kind": u"tm:ltm:virtual:virtualstate"
555+
}
556+
)}
557+
mock_session = mock.MagicMock(**attrs)
558+
r._meta_data['bigip']._meta_data =\
559+
{'icr_session': mock_session,
560+
'hostname': 'TESTDOMAINNAME',
561+
'uri': 'https://TESTDOMAIN:443/mgmt/tm/'}
562+
r.generation = 0
563+
x = r.load(partition='Common', name='test_load')
564+
assert x.selfLink == mockuri
565+
with pytest.raises(URICreationCollision) as UCCEIO:
566+
x.load(uri='URI')
567+
assert UCCEIO.value.message ==\
568+
"There was an attempt to assign a new uri to this resource, the"\
569+
" _meta_data['uri'] is "\
570+
"https://TESTDOMAIN:443/mgmt/tm/ltm/virtual/"\
571+
"~Common~test_load/ and it should not be changed."
572+
458573

459574
class TestResource_exists(object):
460575
def test_loadable(self):
@@ -621,4 +736,25 @@ def test_create_raises(self):
621736
def test_delete_raises(self):
622737
unnamed_resource = UnnamedResource(mock.MagicMock())
623738
with pytest.raises(UnsupportedMethod):
624-
unnamed_resource.create()
739+
unnamed_resource.delete()
740+
741+
def test_load(self):
742+
r = Sync_Status(mock.MagicMock())
743+
r._meta_data['allowed_lazy_attributes'] = []
744+
mockuri = "https://localhost:443/mgmt/tm/cm/sync-status"
745+
attrs = {'get.return_value':
746+
MockResponse(
747+
{
748+
u"generation": 0,
749+
u"selfLink": mockuri,
750+
u"kind": u"tm:cm:sync-status:sync-statusstats"
751+
}
752+
)}
753+
mock_session = mock.MagicMock(**attrs)
754+
r._meta_data['bigip']._meta_data =\
755+
{'icr_session': mock_session,
756+
'hostname': 'TESTDOMAINNAME',
757+
'uri': 'https://TESTDOMAIN:443/mgmt/tm/'}
758+
r.generation = 0
759+
x = r.load(partition='Common', name='test_load')
760+
assert x.selfLink == 'https://localhost:443/mgmt/tm/cm/sync-status'

test/functional/auth/test_password_policy.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
from pprint import pprint as pp
1717

18+
import copy
19+
1820

1921
class TestPasswordPolicy(object):
2022
def test_load(self, bigip):
@@ -31,3 +33,16 @@ def test_update(self, bigip):
3133
assert password_policy.maxLoginFailures == 10
3234
password_policy.update(maxLoginFailures=0)
3335
assert password_policy.maxLoginFailures == 0
36+
37+
def test_modify(self, mgmt_root):
38+
password_policy = mgmt_root.tm.auth.password_policy.load()
39+
original_dict = copy.copy(password_policy.__dict__)
40+
max_fails = 'maxLoginFailures'
41+
password_policy.modify(maxLoginFailures=25)
42+
for k, v in original_dict.items():
43+
if k != max_fails:
44+
original_dict[k] = password_policy.__dict__[k]
45+
elif k == max_fails:
46+
password_policy.__dict__[k] == 'Cool mod test'
47+
password_policy.modify(maxLoginFailures=0)
48+
assert password_policy.maxLoginFailures == 0

test/functional/tm/ltm/test_virtual.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from f5.bigip.resource import MissingRequiredCreationParameter
1919
from f5.bigip.resource import MissingRequiredReadParameter
2020

21+
import copy
2122
from pprint import pprint as pp
2223
import pytest
2324

@@ -63,6 +64,19 @@ def test_virtual_create_refresh_update_delete_load(
6364
virtual2 = vc1.virtual.load(partition='Common', name='vstest1')
6465
assert virtual2.selfLink == virtual1.selfLink
6566

67+
def test_virtual_modify(self, request, mgmt_root, setup_device_snapshot):
68+
virtual1, vc1 = setup_virtual_test(
69+
request, mgmt_root, 'Common', 'modtest1'
70+
)
71+
original_dict = copy.copy(virtual1.__dict__)
72+
desc = 'description'
73+
virtual1.modify(description='Cool mod test')
74+
for k, v in original_dict.items():
75+
if k != desc:
76+
original_dict[k] = virtual1.__dict__[k]
77+
elif k == desc:
78+
virtual1.__dict__[k] == 'Cool mod test'
79+
6680

6781
def test_profiles_CE(
6882
mgmt_root, opt_release, setup_device_snapshot

0 commit comments

Comments
 (0)