Skip to content

Commit 60abd57

Browse files
authored
Merge pull request #486 from wojtek0806/fixes_378_474_452
Multibugfixes_ #378, #452, #474, #481, #484, #485, #491
2 parents def3f1d + 1dcfa4b commit 60abd57

18 files changed

Lines changed: 407 additions & 93 deletions

f5/bigip/mixins.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
#
1717
# NOTE: Code taken from Effective Python Item 26
1818

19-
import logging
2019

20+
from distutils.version import LooseVersion
2121
from f5.sdk_exception import F5SDKError
22+
import logging
2223

2324

2425
class InvalidCommand(F5SDKError):
@@ -117,10 +118,13 @@ def __getattr__(container, name):
117118

118119
def _check_supported_versions(container, attribute):
119120
tmos_v = container._meta_data['bigip'].tmos_version
120-
if tmos_v not in attribute._meta_data['supported_versions']:
121+
minimum = attribute._meta_data['minimum_version']
122+
if LooseVersion(tmos_v) < LooseVersion(minimum):
121123
error = "There was an attempt to access API which " \
122124
"has not been implemented or supported " \
123-
"in the device's TMOS version: {}".format(tmos_v)
125+
"in the device's TMOS version: {}. "\
126+
"Minimum TMOS version supported is {}".format(
127+
tmos_v, minimum)
124128
raise UnsupportedTmosVersion(error)
125129

126130

@@ -250,7 +254,9 @@ def _exec_cmd(self, command, **kwargs):
250254
'''
251255

252256
kwargs['command'] = command
257+
self._check_exclusive_parameters(**kwargs)
253258
requests_params = self._handle_requests_params(kwargs)
259+
self._check_command_parameters(**kwargs)
254260
session = self._meta_data['bigip']._meta_data['icr_session']
255261
response = session.post(
256262
self._meta_data['uri'], json=kwargs, **requests_params)

f5/bigip/resource.py

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,18 @@
8787
from requests.exceptions import HTTPError
8888

8989

90-
class MissingUpdateParameter(F5SDKError):
91-
"""Raises this when update requires specific
90+
class MissingRequiredCommandParameter(F5SDKError):
91+
"""Various values MUST be provided to execute a command."""
92+
pass
9293

93-
parameters together
9494

95-
"""
95+
class ExclusiveAttributesPresent(F5SDKError):
96+
"""Raises this when exclusive attributes are present."""
97+
pass
98+
99+
100+
class MissingUpdateParameter(F5SDKError):
101+
"""Raises this when update requires specific parameters together."""
96102
pass
97103

98104

@@ -173,7 +179,13 @@ def __init__(self, container):
173179
self._set_meta_data_uri()
174180
# Supported versions for each class will be defined here.
175181
# List can be modified downstream in each sub-class
176-
self._meta_data['supported_versions'] = set(['11.6.0', '12.0.0'])
182+
self._meta_data['minimum_version'] = '11.6.0'
183+
# Commands you can run on a resource or collection, we define it here
184+
self._meta_data['allowed_commands'] = []
185+
# Define required command parameters
186+
self._meta_data['required_command_parameters'] = set()
187+
# You can't have more than one of the attributes in any of these sets.
188+
self._meta_data['exclusive_attributes'] = []
177189

178190
def _set_meta_data_uri(self):
179191
base_uri = self.__class__.__name__.lower()
@@ -190,19 +202,41 @@ def _set_meta_data_uri(self):
190202
self._meta_data['uri'] = final_uri
191203

192204
def _check_load_parameters(self, **kwargs):
193-
'''Params given to load should at least satisfy required params.
205+
"""Params given to load should at least satisfy required params.
194206
195207
:params: kwargs
196208
:raises: MissingRequiredReadParameter
197-
'''
198-
key_set = set(kwargs.keys())
199-
required_minus_received =\
200-
self._meta_data['required_load_parameters'] - key_set
201-
if required_minus_received != set():
202-
error_message = 'Missing required params: %r'\
203-
% required_minus_received
209+
"""
210+
rset = self._meta_data['required_load_parameters']
211+
check = self._missing_required_parameters(rset, **kwargs)
212+
if check:
213+
error_message = 'Missing required params: %s' % check
204214
raise MissingRequiredReadParameter(error_message)
205215

216+
def _check_create_parameters(self, **kwargs):
217+
"""Params given to create should satisfy required params.
218+
219+
:params: kwargs
220+
:raises: MissingRequiredCreateParameter
221+
"""
222+
rset = self._meta_data['required_creation_parameters']
223+
check = self._missing_required_parameters(rset, **kwargs)
224+
if check:
225+
error_message = 'Missing required params: %s' % check
226+
raise MissingRequiredCreationParameter(error_message)
227+
228+
def _check_command_parameters(self, **kwargs):
229+
"""Params given to exec_cmd should satisfy required params.
230+
231+
:params: kwargs
232+
:raises: MissingRequiredCommandParameter
233+
"""
234+
rset = self._meta_data['required_command_parameters']
235+
check = self._missing_required_parameters(rset, **kwargs)
236+
if check:
237+
error_message = 'Missing required params: %s' % check
238+
raise MissingRequiredCommandParameter(error_message)
239+
206240
def _local_update(self, rdict):
207241
"""Call this with a response dictionary to update instance attrs.
208242
@@ -295,6 +329,37 @@ def _handle_requests_params(self, kwargs):
295329
requests_params.update({'params': params})
296330
return requests_params
297331

332+
def _check_exclusive_parameters(self, **kwargs):
333+
"""Check for mutually exclusive attributes in kwargs.
334+
335+
:raises ExclusiveAttributesPresent
336+
"""
337+
if len(self._meta_data['exclusive_attributes']) > 0:
338+
attr_set = set(kwargs.keys())
339+
ex_set = set(self._meta_data['exclusive_attributes'][0])
340+
common_set = attr_set.intersection(ex_set)
341+
if len(common_set) > 1:
342+
cset = ', '.join(common_set)
343+
error = 'Mutually exclusive arguments submitted. ' \
344+
'The following arguments cannot be set ' \
345+
'together: "%s".' % cset
346+
raise ExclusiveAttributesPresent(error)
347+
348+
@staticmethod
349+
def _missing_required_parameters(rqset, **kwargs):
350+
"""Helper function to do operation on sets.
351+
352+
Checks for any missing required parameters.
353+
Returns non-empty or empty list. With empty
354+
list being False.
355+
356+
::returns list
357+
"""
358+
key_set = set(kwargs.keys())
359+
required_minus_received = rqset - key_set
360+
if required_minus_received != set():
361+
return list(required_minus_received)
362+
298363
@property
299364
def raw(self):
300365
"""Display the attributes that the current object has and their values.
@@ -362,8 +427,6 @@ def __init__(self, container):
362427
:param container: instance is an attribute of a ResourceBase container
363428
"""
364429
super(ResourceBase, self).__init__(container)
365-
# Commands you can run on a resource or collection, we define it here
366-
self._meta_data['allowed_commands'] = []
367430

368431
def _update(self, **kwargs):
369432
"""wrapped with update, override that in a subclass to customize"""
@@ -603,8 +666,6 @@ def __init__(self, container):
603666
self._meta_data['required_creation_parameters'] = set(('name',))
604667
# Refresh fails without these.
605668
self._meta_data['required_load_parameters'] = set(('name',))
606-
# You can't have more than one of the attrs in any of these sets.
607-
self._meta_data['exclusive_attributes'] = []
608669
# You can't set these attributes, only 'read' them.
609670
self._meta_data['read_only_attributes'] = []
610671

@@ -665,14 +726,9 @@ def _create(self, **kwargs):
665726
"resource, the _meta_data['uri'] is %s and it should"\
666727
" not be changed." % (self._meta_data['uri'])
667728
raise URICreationCollision(error)
729+
self._check_exclusive_parameters(**kwargs)
668730
requests_params = self._handle_requests_params(kwargs)
669-
key_set = set(kwargs.keys())
670-
required_minus_received =\
671-
self._meta_data['required_creation_parameters'] - key_set
672-
if required_minus_received != set():
673-
error_message = 'Missing required params: %r'\
674-
% required_minus_received
675-
raise MissingRequiredCreationParameter(error_message)
731+
self._check_create_parameters(**kwargs)
676732

677733
# Make convenience variable with short names for this method.
678734
_create_uri = self._meta_data['container']._meta_data['uri']

f5/bigip/test/test_resource.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818

1919
from f5.bigip.resource import Collection
2020
from f5.bigip.resource import DeviceProvidesIncompatibleKey
21+
from f5.bigip.resource import ExclusiveAttributesPresent
2122
from f5.bigip.resource import GenerationMismatch
2223
from f5.bigip.resource import InvalidForceType
2324
from f5.bigip.resource import InvalidResource
2425
from f5.bigip.resource import KindTypeMismatch
26+
from f5.bigip.resource import MissingRequiredCommandParameter
2527
from f5.bigip.resource import MissingRequiredCreationParameter
2628
from f5.bigip.resource import MissingRequiredReadParameter
2729
from f5.bigip.resource import OrganizingCollection
30+
from f5.bigip.resource import PathElement
2831
from f5.bigip.resource import RequestParamKwargCollision
2932
from f5.bigip.resource import Resource
3033
from f5.bigip.resource import ResourceBase
@@ -96,7 +99,7 @@ def test_missing_required_creation_parameter(self):
9699
with pytest.raises(MissingRequiredCreationParameter) as MRCPEIO:
97100
r.create(partition="Common", name='CreateTest')
98101
assert MRCPEIO.value.message ==\
99-
"Missing required params: set(['NONEMPTY'])"
102+
"Missing required params: ['NONEMPTY']"
100103

101104
def test_KindTypeMismatch(self):
102105
r = Resource(mock.MagicMock())
@@ -290,7 +293,7 @@ def test_missing_required_params(self):
290293
with pytest.raises(MissingRequiredReadParameter) as MRREIO:
291294
r.load(partition='Common', name='test_load')
292295
assert MRREIO.value.message ==\
293-
"Missing required params: set(['IMPOSSIBLE'])"
296+
"Missing required params: ['IMPOSSIBLE']"
294297

295298
def test_requests_params_collision(self):
296299
r = Resource(mock.MagicMock())
@@ -443,3 +446,64 @@ def test_collection_s():
443446
}
444447
tc_s = Under_s(MC)
445448
assert tc_s._meta_data['uri'] == 'BASEURI/under/'
449+
450+
451+
class TestPathElement(object):
452+
453+
def test_missing_req_param_true(self):
454+
p = PathElement(mock.MagicMock())
455+
rqset = set(['FOOPAR1', 'FOOPAR2'])
456+
fakearg = {'FOOPAR1': 'FOOVAL'}
457+
mrq = p._missing_required_parameters(rqset, **fakearg)
458+
assert mrq
459+
assert mrq == ['FOOPAR2']
460+
461+
def test_missing_req_param_false(self):
462+
p = PathElement(mock.MagicMock())
463+
rqset = set(['FOOPAR1'])
464+
fakearg = {'FOOPAR1': 'FOOVAL'}
465+
mrq = p._missing_required_parameters(rqset, **fakearg)
466+
assert not mrq
467+
468+
def test_check_load_parameters_fail(self):
469+
p = PathElement(mock.MagicMock())
470+
p._meta_data['required_load_parameters'] = set(['FAKELOAD'])
471+
with pytest.raises(MissingRequiredReadParameter) as RLPEIO:
472+
p._check_load_parameters(FOOLOAD='FOOVAL')
473+
assert "['FAKELOAD']" in RLPEIO.value.message
474+
475+
def test_check_create_parameters_fail(self):
476+
p = PathElement(mock.MagicMock())
477+
p._meta_data['required_creation_parameters'] = set(['FAKECREATE'])
478+
with pytest.raises(MissingRequiredCreationParameter) as RCPEIO:
479+
p._check_create_parameters(FOOCREATE='FOOVAL')
480+
assert "['FAKECREATE']" in RCPEIO.value.message
481+
482+
def test_check_command_parameters_fail(self):
483+
p = PathElement(mock.MagicMock())
484+
p._meta_data['required_command_parameters'] = set(['FAKECOMMAND'])
485+
with pytest.raises(MissingRequiredCommandParameter) as RCPEIO:
486+
p._check_command_parameters(BARCOMMAND='FOOVAL')
487+
assert "['FAKECOMMAND']" in RCPEIO.value.message
488+
489+
def test_check_exclusive_parameters_empty_attr(self):
490+
p = PathElement(mock.MagicMock())
491+
p._meta_data['exclusive_attributes'] = []
492+
fakearg = {'FOOEX': 'FOOVAL'}
493+
# Check that any other exception is not thrown
494+
p._check_exclusive_parameters(**fakearg)
495+
496+
def test_check_exclusive_parameters_pass(self):
497+
p = PathElement(mock.MagicMock())
498+
p._meta_data['exclusive_attributes'] = [('FOOEX', 'BAREX')]
499+
fakearg = {'FOOEX': 'FOOVAL'}
500+
# Check ExclusiveAttributesPresent is not thrown
501+
p._check_exclusive_parameters(**fakearg)
502+
503+
def test_check_exclusive_parameters_fail(self):
504+
p = PathElement(mock.MagicMock())
505+
p._meta_data['exclusive_attributes'] = [('FOOEX', 'BAREX')]
506+
fakearg = {'FOOEX': 'FOOVAL', 'BAREX': 'BARVAL'}
507+
with pytest.raises(ExclusiveAttributesPresent) as EAEIO:
508+
p._check_exclusive_parameters(**fakearg)
509+
assert 'FOOEX, BAREX' in EAEIO.value.message

f5/bigip/tm/cm/device_group.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ class Device_Groups(Collection):
3535
"""BIG-IP® cluster device-groups collection."""
3636
def __init__(self, cm):
3737
super(Device_Groups, self).__init__(cm)
38-
endpoint = 'device-group'
39-
self._meta_data['uri'] =\
40-
self._meta_data['container']._meta_data['uri'] + endpoint + '/'
4138
self._meta_data['allowed_lazy_attributes'] = [Device_Group]
4239
self._meta_data['attribute_registry'] =\
4340
{'tm:cm:device:device-groupstate': Device_Group}

f5/bigip/tm/cm/sync_status.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,14 @@
2828
"""
2929

3030
from f5.bigip.mixins import UnnamedResourceMixin
31-
from f5.bigip.resource import Resource
31+
from f5.bigip.resource import ResourceBase
3232

3333

34-
class Sync_Status(Resource, UnnamedResourceMixin):
34+
class Sync_Status(ResourceBase, UnnamedResourceMixin):
3535
'''A Resource concrete subclass.'''
3636
def __init__(self, cm):
3737
'''Autogenerated constructor.'''
3838
super(Sync_Status, self).__init__(cm)
39-
base_uri = type(self).__name__.replace('_', '-').lower()
40-
self._meta_data['uri'] =\
41-
self._meta_data['container']._meta_data['uri'] + base_uri
4239
self._meta_data['template_generated'] = True
4340
self._meta_data['required_json_kind'] =\
4441
u"tm:cm:sync-status:sync-statusstats"

f5/bigip/tm/cm/traffic_group.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ class Traffic_Groups(Collection):
3535
"""BIG-IP® cluster traffic-group collection"""
3636
def __init__(self, cm):
3737
super(Traffic_Groups, self).__init__(cm)
38-
endpoint = 'traffic-group'
39-
self._meta_data['uri'] =\
40-
self._meta_data['container']._meta_data['uri'] + endpoint + '/'
4138
self._meta_data['allowed_lazy_attributes'] = [Traffic_Group]
4239
self._meta_data['attribute_registry'] =\
4340
{'tm:cm:traffic-group:traffic-groupstate': Traffic_Group}

f5/bigip/tm/cm/trust.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
from f5.bigip.mixins import CommandExecutionMixin
1919
from f5.bigip.mixins import ExclusiveAttributesMixin
2020
from f5.bigip.mixins import UnnamedResourceMixin
21-
from f5.bigip.resource import Resource
21+
from f5.bigip.resource import PathElement
2222

2323

24-
class Add_To_Trust(UnnamedResourceMixin, ExclusiveAttributesMixin,
25-
CommandExecutionMixin, Resource):
24+
class Add_To_Trust(PathElement, UnnamedResourceMixin, ExclusiveAttributesMixin,
25+
CommandExecutionMixin):
2626
"""BIG-IP® Add-To-Trust resource
2727
2828
Use this object to set or overwrite device trust
@@ -33,15 +33,16 @@ def __init__(self, cm):
3333
super(Add_To_Trust, self).__init__(cm)
3434
self._meta_data['exclusive_attributes'].append(
3535
('caDevice', 'nonCaDevice'))
36-
self._meta_data['required_creation_parameters'].update(
37-
('device', 'deviceName', 'username', 'password'))
36+
self._meta_data['required_command_parameters'].update(
37+
('name', 'device', 'deviceName', 'username', 'password'))
3838
self._meta_data['required_json_kind'] = \
3939
'tm:cm:add-to-trust:runstate'
4040
self._meta_data['allowed_commands'].append('run')
41-
self._meta_data['supported_versions'].discard('11.6.0')
41+
self._meta_data['minimum_version'] = '12.0.0'
4242

4343

44-
class Remove_From_Trust(UnnamedResourceMixin, CommandExecutionMixin, Resource):
44+
class Remove_From_Trust(PathElement, UnnamedResourceMixin,
45+
CommandExecutionMixin):
4546
"""BIG-IP®« Remove-From-Trust resource
4647
4748
Use this object to remove device trust
@@ -55,9 +56,9 @@ class Remove_From_Trust(UnnamedResourceMixin, CommandExecutionMixin, Resource):
5556

5657
def __init__(self, cm):
5758
super(Remove_From_Trust, self).__init__(cm)
58-
self._meta_data['required_creation_parameters'].update(
59-
('deviceName',))
59+
self._meta_data['required_command_parameters'].update(
60+
('deviceName', 'name'))
6061
self._meta_data['required_json_kind'] = \
6162
'tm:cm:remove-from-trust:runstate'
6263
self._meta_data['allowed_commands'].append('run')
63-
self._meta_data['supported_versions'].discard('11.6.0')
64+
self._meta_data['minimum_version'] = '12.0.0'

f5/bigip/tm/cm/trust_domain.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ class Trust_Domains(Collection):
3636

3737
def __init__(self, cm):
3838
super(Trust_Domains, self).__init__(cm)
39-
endpoint = 'trust-domain'
40-
self._meta_data['uri'] =\
41-
self._meta_data['container']._meta_data['uri'] + endpoint + '/'
4239
self._meta_data['allowed_lazy_attributes'] = [Trust_Domain]
4340
self._meta_data['attribute_registry'] = \
4441
{'tm:cm:trust-domain:trust-domainstate': Trust_Domain}

0 commit comments

Comments
 (0)