Skip to content

Commit 8bfd2d7

Browse files
committed
Add GTM Datacenter support to the SDK
Issues: Fixes #497 Problem: The SDK does not support the GTM datacenter API Analysis: This change provides support for the GTM Datacenter API. It uses exclusive mixins to allow users to specify either the 'enabled' or 'disabled' attributes and handles their mutual exclusion appropriately Tests: * test/functional/tm/gtm/test_datacenter.py * f5/bigip/tm/gtm/test/test_datacenter.py
1 parent 5d4f71d commit 8bfd2d7

4 files changed

Lines changed: 369 additions & 0 deletions

File tree

f5/bigip/tm/gtm/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030

3131
from f5.bigip.resource import OrganizingCollection
32+
from f5.bigip.tm.gtm.datacenter import Datacenters
3233
from f5.bigip.tm.gtm.rule import Rules
3334

3435

@@ -37,5 +38,6 @@ class Gtm(OrganizingCollection):
3738
def __init__(self, tm):
3839
super(Gtm, self).__init__(tm)
3940
self._meta_data['allowed_lazy_attributes'] = [
41+
Datacenters,
4042
Rules
4143
]

f5/bigip/tm/gtm/datacenter.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# coding=utf-8
2+
#
3+
# Copyright 2014-2015 F5 Networks Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
"""BIG-IP® Global Traffic Manager (GTM) datacenter module.
19+
20+
REST URI
21+
``http://localhost/mgmt/tm/gtm/datacenter``
22+
23+
GUI Path
24+
``DNS --> GSLB : Data Centers``
25+
26+
REST Kind
27+
``tm:gtm:datacenter:*``
28+
"""
29+
30+
from f5.bigip.mixins import ExclusiveAttributesMixin
31+
from f5.bigip.resource import Collection
32+
from f5.bigip.resource import Resource
33+
34+
35+
class Datacenters(Collection):
36+
"""BIG-IP® GTM datacenter collection"""
37+
def __init__(self, gtm):
38+
super(Datacenters, self).__init__(gtm)
39+
self._meta_data['allowed_lazy_attributes'] = [Datacenter]
40+
self._meta_data['attribute_registry'] =\
41+
{'tm:gtm:datacenter:datacenterstate': Datacenter}
42+
43+
44+
class Datacenter(Resource, ExclusiveAttributesMixin):
45+
"""BIG-IP® GTM datacenter resource"""
46+
def __init__(self, dc_s):
47+
super(Datacenter, self).__init__(dc_s)
48+
self._meta_data['required_json_kind'] =\
49+
'tm:gtm:datacenter:datacenterstate'
50+
self._meta_data['exclusive_attributes'].append(('enabled', 'disabled'))
51+
52+
def _endis_able(self, config_dict):
53+
if 'enabled' in config_dict and not config_dict['enabled']:
54+
config_dict['disabled'] = True
55+
elif 'disabled' in config_dict and not config_dict['disabled']:
56+
config_dict['enabled'] = True
57+
return config_dict
58+
59+
def _endis_attrs(self):
60+
"""Manipulate return value to equal negation of set value
61+
62+
This function (uniquely?!) manipulates response values before the
63+
consumer has access to them. We think this is dangerous. It is likely
64+
this function will move once we figure out how to properly annotate
65+
this RISKY behavior!"
66+
67+
The BIG-IP REST API for this particular endpoint has two fields
68+
which are mutually exclusive; disabled and enabled. When using this
69+
SDK API, you may do the following
70+
71+
d = api.tm.gtm.datasources.datasource.load(name='foo')
72+
d.update(enabled=False)
73+
74+
You might expect that the behavior of the following...
75+
76+
if d.enabled:
77+
print("enabled")
78+
else:
79+
print("disabled")
80+
81+
...would result in "enabled" being printed, but that would not be
82+
the case; BIG-IP will specify that "enabled" is True and that the
83+
following is also now True
84+
85+
d.disabled == True
86+
87+
This behavior of setting a different variable instead of the one
88+
that you specified, may not be obvious to the casual user. Therefore,
89+
this method will set appropriate sister variables to be the negation
90+
of the variable you set.
91+
92+
Therefore
93+
94+
d.enabled = True
95+
96+
will also do the following automatically
97+
98+
d.disabled = False
99+
100+
This behavior will allow the SDK to behave according to most users
101+
expectations, shown below
102+
103+
d.update(enabled=False)
104+
if d.enabled:
105+
print("enabled")
106+
else:
107+
print("disabled")
108+
109+
which will print the following
110+
111+
"disabled"
112+
113+
Likewise, checking for d.disabled would return True.
114+
Returns:
115+
None
116+
"""
117+
if 'disabled' in self.__dict__:
118+
self.__dict__['enabled'] = not self.__dict__['disabled']
119+
if 'enabled' in self.__dict__:
120+
self.__dict__['disabled'] = not self.__dict__['enabled']
121+
return None
122+
123+
def create(self, **kwargs):
124+
kwargs = self._endis_able(kwargs)
125+
126+
if 'enabled' in kwargs and kwargs['enabled']:
127+
del kwargs['disabled']
128+
if 'disabled' in kwargs and kwargs['disabled']:
129+
del kwargs['enabled']
130+
131+
self._create(**kwargs)
132+
self._endis_attrs()
133+
return self
134+
135+
def load(self, **kwargs):
136+
kwargs = self._endis_able(kwargs)
137+
self._load(**kwargs)
138+
self._endis_attrs()
139+
return self
140+
141+
def refresh(self, **kwargs):
142+
kwargs = self._endis_able(kwargs)
143+
self._refresh(**kwargs)
144+
self._endis_attrs()
145+
return self
146+
147+
def update(self, **kwargs):
148+
if 'enabled' in self.__dict__ and 'enabled' not in kwargs:
149+
kwargs['enabled'] = self.__dict__.pop('enabled')
150+
elif 'disabled' in self.__dict__ and 'disabled' not in kwargs:
151+
kwargs['disabled'] = self.__dict__.pop('disabled')
152+
kwargs = self._endis_able(kwargs)
153+
self._update(**kwargs)
154+
self._endis_attrs()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2015 F5 Networks Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
import mock
17+
import pytest
18+
19+
from f5.bigip import ManagementRoot
20+
from f5.bigip.resource import MissingRequiredCreationParameter
21+
from f5.bigip.tm.gtm.datacenter import Datacenter
22+
23+
24+
@pytest.fixture
25+
def FakeDatacenter():
26+
fake_dc_s = mock.MagicMock()
27+
fake_dc = Datacenter(fake_dc_s)
28+
return fake_dc
29+
30+
31+
class TestCreate(object):
32+
def test_create_two(self, fakeicontrolsession):
33+
b = ManagementRoot('192.168.1.1', 'admin', 'admin')
34+
r1 = b.tm.gtm.datacenters.datacenter
35+
r2 = b.tm.gtm.datacenters.datacenter
36+
assert r1 is not r2
37+
38+
def test_create_no_args(self, FakeDatacenter):
39+
with pytest.raises(MissingRequiredCreationParameter):
40+
FakeDatacenter.create()
41+
42+
def test_create_partition(self, FakeDatacenter):
43+
with pytest.raises(MissingRequiredCreationParameter):
44+
FakeDatacenter.create(partition='Common')
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Copyright 2015 F5 Networks Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
import pytest
17+
18+
from f5.bigip.resource import MissingRequiredCreationParameter
19+
from f5.bigip.tm.gtm.datacenter import Datacenter
20+
from requests.exceptions import HTTPError
21+
22+
23+
def delete_dc(mgmt_root, name, partition):
24+
r = mgmt_root.tm.gtm.datacenters.datacenter
25+
try:
26+
r.load(name=name, partition=partition)
27+
except HTTPError as err:
28+
if err.response.status_code != 404:
29+
raise
30+
return
31+
r.delete()
32+
33+
34+
def setup_create_test(request, mgmt_root, name, partition):
35+
def teardown():
36+
delete_dc(mgmt_root, name, partition)
37+
request.addfinalizer(teardown)
38+
39+
40+
def setup_basic_test(request, mgmt_root, name, partition):
41+
def teardown():
42+
delete_dc(mgmt_root, name, partition)
43+
44+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
45+
dc1.create(name=name, partition=partition)
46+
request.addfinalizer(teardown)
47+
return dc1
48+
49+
50+
class TestCreate(object):
51+
def test_create_no_args(self, mgmt_root):
52+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
53+
with pytest.raises(MissingRequiredCreationParameter):
54+
dc1.create()
55+
56+
def test_create(self, request, mgmt_root):
57+
setup_create_test(request, mgmt_root, 'dc1', 'Common')
58+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
59+
dc1.create(name='dc1', partition='Common')
60+
assert dc1.name == 'dc1'
61+
assert dc1.partition == 'Common'
62+
assert dc1.generation and isinstance(dc1.generation, int)
63+
assert dc1.kind == 'tm:gtm:datacenter:datacenterstate'
64+
assert dc1.selfLink.startswith(
65+
'https://localhost/mgmt/tm/gtm/datacenter/~Common~dc1')
66+
67+
def test_create_optional_args(self, request, mgmt_root):
68+
setup_create_test(request, mgmt_root, 'dc1', 'Common')
69+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
70+
dc1.create(name='dc1',
71+
partition='Common',
72+
enabled=False,
73+
contact="admin@root.local",
74+
description="A datacenter is fine too",
75+
location="Between the earth and the moon")
76+
assert False == dc1.enabled
77+
assert "admin@root.local" == dc1.contact
78+
assert "A datacenter is fine too" == dc1.description
79+
assert "Between the earth and the moon" == dc1.location
80+
81+
def test_create_duplicate(self, request, mgmt_root):
82+
setup_create_test(request, mgmt_root, 'dc1', 'Common')
83+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
84+
dc1.create(name='dc1', partition='Common')
85+
dc2 = mgmt_root.tm.gtm.datacenters.datacenter
86+
with pytest.raises(HTTPError) as err:
87+
dc2.create(name='dc1', partition='Common')
88+
assert err.response.status_code == 400
89+
90+
91+
class TestRefresh(object):
92+
def test_refresh(self, request, mgmt_root):
93+
setup_basic_test(request, mgmt_root, 'dc1', 'Common')
94+
d1 = mgmt_root.tm.gtm.datacenters.datacenter.load(
95+
name='dc1', partition='Common')
96+
d2 = mgmt_root.tm.gtm.datacenters.datacenter.load(
97+
name='dc1', partition='Common')
98+
assert True == d1.enabled
99+
assert True == d2.enabled
100+
101+
d2.update(enabled=False)
102+
assert False == d2.enabled
103+
assert True == d1.enabled
104+
105+
d1.refresh()
106+
assert False == d1.enabled
107+
108+
109+
class TestLoad(object):
110+
def test_load_no_object(self, mgmt_root):
111+
with pytest.raises(HTTPError) as err:
112+
mgmt_root.tm.gtm.datacenters.datacenter.load(
113+
name='dc1', partition='Common')
114+
assert err.response.status_code == 404
115+
116+
def test_load(self, request, mgmt_root):
117+
setup_basic_test(request, mgmt_root, 'dc1', 'Common')
118+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter.load(
119+
name='dc1', partition='Common')
120+
assert True == dc1.enabled
121+
dc1.update(enabled=False)
122+
dc2 = mgmt_root.tm.gtm.datacenters.datacenter.load(
123+
name='dc1', partition='Common')
124+
assert False == dc1.enabled
125+
assert False == dc2.enabled
126+
127+
128+
class TestUpdate(object):
129+
def test_update(self, request, mgmt_root):
130+
dc1 = setup_basic_test(request, mgmt_root, 'dc1', 'Common')
131+
assert True == dc1.enabled
132+
assert False == dc1.disabled
133+
dc1.update(enabled=False)
134+
assert False == dc1.enabled
135+
assert True == dc1.disabled
136+
137+
def test_update_samevalue(self, request, mgmt_root):
138+
dc1 = setup_basic_test(request, mgmt_root, 'dc1', 'Common')
139+
dc1.update(enabled=True)
140+
assert False != dc1.enabled
141+
142+
143+
class TestDelete(object):
144+
def test_delete(self, request, mgmt_root):
145+
dc1 = setup_basic_test(request, mgmt_root, 'dc1', 'Common')
146+
dc1.delete()
147+
with pytest.raises(HTTPError) as err:
148+
mgmt_root.tm.gtm.datacenters.datacenter.load(
149+
name='dc1', partition='Common')
150+
assert err.response.status_code == 404
151+
152+
153+
class TestDatacenterCollection(object):
154+
def test_datacenter_collection(self, request, mgmt_root):
155+
setup_create_test(request, mgmt_root, 'dc1', 'Common')
156+
dc1 = mgmt_root.tm.gtm.datacenters.datacenter
157+
dc1.create(name='dc1', partition='Common')
158+
assert dc1.name == 'dc1'
159+
assert dc1.partition == 'Common'
160+
assert dc1.generation and isinstance(dc1.generation, int)
161+
assert dc1.fullPath == '/Common/dc1'
162+
assert dc1.kind == 'tm:gtm:datacenter:datacenterstate'
163+
assert dc1.selfLink.startswith(
164+
'https://localhost/mgmt/tm/gtm/datacenter/~Common~dc1')
165+
166+
rc = mgmt_root.tm.gtm.datacenters.get_collection()
167+
assert isinstance(rc, list)
168+
assert len(rc)
169+
assert isinstance(rc[0], Datacenter)

0 commit comments

Comments
 (0)