Skip to content

Commit 9431a64

Browse files
author
Paul Breaux
committed
Support two member cluster only in first clustering release
Issues: Fixes #471 Problem: In order to minimize feature creep and the ability for users to shoot themselves in the foot, I would like to limit clustering support to handle only two devices. This will be reflected in the code with tests and documentation. Analysis: Modified the code to only allow up to four devices to be clustered, yet the functional tests are there for up to two devices. Four devices is not yet supported. This will be made clear in the customer-facing documentation. It is already clear in the code documentation. Tests: All clustering tests pass against 11.6 and 12.0 VE images
1 parent f269294 commit 9431a64

8 files changed

Lines changed: 155 additions & 73 deletions

File tree

f5/multi_device/cluster/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# coding=utf-8
12
# Copyright 2016 F5 Networks Inc.
23

34
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,9 +19,9 @@
1819
1920
Definitions:
2021
Cluster: The manager of the TrustDomain and DeviceGroup objects.
21-
TrustDomain: a group of BIG-IP devices that have exchanged certificates
22+
TrustDomain: a group of BIG-IP® devices that have exchanged certificates
2223
and trust one another
23-
DeviceGroup: a group of BIG-IP device that sync configuration data and
24+
DeviceGroup: a group of BIG-IP® device that sync configuration data and
2425
failover connections.
2526
2627
Clustering is broken down into three component parts: a cluster manager, a
@@ -33,6 +34,9 @@
3334
to the group. After this step, a cluster exists.
3435
3536
Currently the only supported type of cluster is a 'sync-failover' cluster.
37+
The number of devices supported officially is currently two, for an
38+
active-standby cluster, but the code below can accommodate a four-member
39+
cluster.
3640
3741
Methods:
3842
@@ -85,7 +89,7 @@
8589

8690

8791
class ClusterManager(object):
88-
'''Manage a cluster of BigIPs.
92+
'''Manage a cluster of BIG-IP® devices.
8993
9094
This is accomplished with REST URI calls only, but some operations are
9195
only permitted via tmsh commands (such as adding cm/trust-domain peers).
@@ -120,12 +124,12 @@ def __getattr__(self, name):
120124
raise AttributeError(name)
121125

122126
def _check_device_number(self, devices):
123-
'''Check if number of devices is < 2 or > 8.
127+
'''Check if number of devices is between 2 and 4
124128
125129
:param kwargs: dict -- keyword args in dict
126130
'''
127131

128-
if len(devices) < 2 or len(devices) > 8:
132+
if len(devices) < 2 or len(devices) > 4:
129133
msg = 'The number of devices to cluster is not supported.'
130134
raise ClusterNotSupported(msg)
131135

@@ -144,7 +148,7 @@ def manage_extant(self, **kwargs):
144148
self.cluster = Cluster(**kwargs)
145149

146150
def create(self, **kwargs):
147-
'''Create a cluster of BigIP devices.
151+
'''Create a cluster of BIG-IP® devices.
148152
149153
:param kwargs: dict -- keyword arguments for cluster manager
150154
'''
@@ -163,7 +167,7 @@ def create(self, **kwargs):
163167
self.cluster = Cluster(**kwargs)
164168

165169
def teardown(self):
166-
'''Teardown the cluster of BigIP devices.'''
170+
'''Teardown the cluster of BIG-IP® devices.'''
167171

168172
print('Tearing down the cluster...')
169173
self.device_group.teardown()

f5/multi_device/device_group.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,14 @@ class DeviceGroup(object):
7979
above and also may take some other action to enforce the expected state,
8080
such as syncing config.
8181
82+
The pollster is used heavliy here for 'check' and 'get' methods, since we
83+
are often waiting for the device or devices to respond to some action.
84+
8285
Example:
8386
87+
* dg = self._get_device_group()
88+
* self._check_all_devices_in_sync()
89+
* self.ensure_all_devices_in_sync()
8490
8591
'''
8692

@@ -189,7 +195,8 @@ def _get_device_names_in_group(self):
189195
dg = pollster(self._get_device_group)(self.devices[0])
190196
members = dg.devices_s.get_collection()
191197
for member in members:
192-
device_names.append(member.name)
198+
member_name = member.name.replace('/%s/' % self.partition, '')
199+
device_names.append(member_name)
193200
return device_names
194201

195202
def _get_device_group(self, device):
@@ -222,9 +229,9 @@ def _add_device_to_device_group(self, device):
222229

223230
device_name = get_device_info(device).name
224231
dg = pollster(self._get_device_group)(device)
232+
print('Adding following device to group: ' + device_name)
225233
dg.devices_s.devices.create(name=device_name, partition=self.partition)
226234
pollster(self._check_device_exists_in_device_group)(device_name)
227-
print('added following device to group: ' + device_name)
228235

229236
def _check_device_exists_in_device_group(self, device_name):
230237
'''Check whether a device exists in the device group
@@ -235,25 +242,14 @@ def _check_device_exists_in_device_group(self, device_name):
235242
dg = self._get_device_group(self.devices[0])
236243
dg.devices_s.devices.load(name=device_name, partition=self.partition)
237244

238-
def _delete_all_devices_from_device_group(self, device):
239-
'''Remove all devices from device service cluster group.
240-
241-
:param device: ManagementRoot object -- device from which to remove
242-
'''
243-
244-
dg = pollster(self._get_device_group)(device)
245-
dg_devices = dg.devices_s.get_collection()
246-
for device in dg_devices:
247-
device.delete()
248-
return dg
249-
250245
def _delete_device_from_device_group(self, device):
251246
'''Remove device from device service cluster group.
252247
253248
:param device: ManagementRoot object -- device to delete from group
254249
'''
255250

256251
device_name = get_device_info(device).name
252+
print('Deleting following device from group: %s ' % device_name)
257253
dg = pollster(self._get_device_group)(device)
258254
device_to_remove = dg.devices_s.devices.load(
259255
name=device_name, partition=self.partition

f5/multi_device/test/test_device_group.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from f5.multi_device.exceptions import DeviceGroupNotSupported
1818
from f5.multi_device.exceptions import DeviceGroupOperationNotSupported
1919
from f5.multi_device.exceptions import MissingRequiredDeviceGroupParameter
20+
from f5.multi_device.exceptions import UnexpectedDeviceGroupDevices
2021
from f5.multi_device.exceptions import UnexpectedDeviceGroupState
2122
from f5.multi_device.exceptions import UnexpectedDeviceGroupType
2223

@@ -28,6 +29,7 @@ class MockDeviceInfo(object):
2829
def __init__(self, name):
2930
self.name = name
3031
self.selfDevice = 'true'
32+
self.type = 'sync-failover'
3133

3234

3335
class FakeActDevice(object):
@@ -162,3 +164,40 @@ def test__check_all_devices_in_sync_unexpected(DeviceGroupCreateNew):
162164
dg._check_all_devices_in_sync()
163165
assert "Expected all devices in group to have 'In Sync' status." == \
164166
ex.value.message
167+
168+
169+
@mock.patch(
170+
'f5.multi_device.device_group.DeviceGroup._get_device_group',
171+
return_value=MockDeviceInfo('test')
172+
)
173+
@mock.patch(
174+
'f5.multi_device.device_group.DeviceGroup._get_device_names_in_group',
175+
return_value=['test', 'test', 'test', 'test']
176+
)
177+
@mock.patch(
178+
'f5.multi_device.device_group.DeviceGroup.ensure_all_devices_in_sync'
179+
)
180+
def test_validate(mock_get_dg, mock_dev_map, mock_in_sync, BigIPs):
181+
dg = DeviceGroup(
182+
devices=BigIPs, device_group_name='test',
183+
device_group_type='sync-failover', device_group_partition='Common')
184+
assert dg.devices == BigIPs
185+
assert dg.name == 'test'
186+
assert dg.type == 'sync-failover'
187+
assert dg.partition == 'Common'
188+
assert dg.ensure_all_devices_in_sync.call_args == mock.call()
189+
190+
191+
@mock.patch(
192+
'f5.multi_device.device_group.DeviceGroup._get_device_group',
193+
return_value=MockDeviceInfo('test')
194+
)
195+
@mock.patch(
196+
'f5.multi_device.device_group.DeviceGroup._get_device_names_in_group',
197+
)
198+
def test_validate_given_devices_dont_match(mock_get_dg, mock_dev_map, BigIPs):
199+
with pytest.raises(UnexpectedDeviceGroupDevices) as ex:
200+
DeviceGroup(
201+
devices=BigIPs, device_group_name='device_not_found',
202+
device_group_type='sync-failover', device_group_partition='Common')
203+
assert 'Given devices does not match queried devices.' in ex.value.message

f5/multi_device/test/test_trust_domain.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#
1515

1616
from f5.multi_device.cluster import TrustDomain
17+
from f5.multi_device.exceptions import DeviceAlreadyInTrustDomain
1718
from f5.multi_device.exceptions import DeviceNotTrusted
1819

1920
import mock
@@ -54,3 +55,77 @@ def test_validate_device_not_trusted(TrustDomainCreateNew):
5455
td.validate()
5556
assert "'test' is not trusted by 'test', which trusts: []" in \
5657
ex.value.message
58+
59+
60+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._set_attributes')
61+
@mock.patch('f5.multi_device.trust_domain.TrustDomain.validate')
62+
def test___init__(mock_set_attr, mock_validate, BigIPs):
63+
mock_bigips = BigIPs
64+
td = TrustDomain(devices=mock_bigips)
65+
assert td._set_attributes.call_args == mock.call(devices=mock_bigips)
66+
67+
68+
def test__set_attributes(BigIPs):
69+
mock_bigips = BigIPs
70+
td = TrustDomain()
71+
td._set_attributes(devices=mock_bigips, partition='test')
72+
assert td.devices == mock_bigips
73+
assert td.partition == 'test'
74+
assert td.device_group_name == 'device_trust_group'
75+
assert td.device_group_type == 'sync-only'
76+
77+
78+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._add_trustee')
79+
@mock.patch('f5.multi_device.trust_domain.pollster')
80+
def test_create(mock_add_trustee, mock_pollster, TrustDomainCreateNew):
81+
td, mock_bigips = TrustDomainCreateNew
82+
td.create(devices=mock_bigips, partition='test')
83+
assert td.devices == mock_bigips
84+
assert td.partition == 'test'
85+
assert td._add_trustee.call_args_list == \
86+
[
87+
mock.call(mock_bigips[1]),
88+
mock.call(mock_bigips[2]),
89+
mock.call(mock_bigips[3])
90+
]
91+
92+
93+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._add_trustee')
94+
@mock.patch('f5.multi_device.trust_domain.pollster')
95+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._remove_trustee')
96+
def test_teardown(
97+
mock_add_trustee, mock_pollster, mock_rem_trustee, TrustDomainCreateNew
98+
):
99+
td, mock_bigips = TrustDomainCreateNew
100+
td.create(devices=mock_bigips, partition='test')
101+
td.teardown()
102+
assert td.domain == {}
103+
assert td._remove_trustee.call_args_list == \
104+
[
105+
mock.call(mock_bigips[0]),
106+
mock.call(mock_bigips[1]),
107+
mock.call(mock_bigips[2]),
108+
mock.call(mock_bigips[3])
109+
]
110+
111+
112+
@mock.patch('f5.multi_device.trust_domain.get_device_info')
113+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._modify_trust')
114+
def test__add_trustee(mock_dev_info, mock_mod_trust, TrustDomainCreateNew):
115+
td, mock_bigips = TrustDomainCreateNew
116+
td._set_attributes(devices=mock_bigips, partition='test')
117+
td._add_trustee(mock_bigips[1])
118+
assert td._modify_trust.call_args == \
119+
mock.call(mock_bigips[0], td._get_add_trustee_cmd, mock_bigips[1])
120+
121+
122+
@mock.patch('f5.multi_device.trust_domain.TrustDomain._modify_trust')
123+
def test__add_trustee_already_in_domain(
124+
mock_mod_trust, TrustDomainCreateNew
125+
):
126+
td, mock_bigips = TrustDomainCreateNew
127+
td._set_attributes(devices=mock_bigips, partition='test')
128+
td.domain = {'test': 'device'}
129+
with pytest.raises(DeviceAlreadyInTrustDomain) as ex:
130+
td._add_trustee(mock_bigips[1])
131+
assert "Device: 'test' is already in this trust domain" in ex.value.message

f5/multi_device/trust_domain.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555

5656
class TrustDomain(object):
57-
'''Manages the trusted peers of a BigIP device.'''
57+
'''Manages the trust domain of a BIG-IP® device.'''
5858

5959
iapp_actions = {'definition': {'implementation': None, 'presentation': ''}}
6060

@@ -234,6 +234,9 @@ def _deploy_iapp(self, iapp_name, actions, deploying_device):
234234
tmpl = deploying_device.tm.sys.applications.templates.template
235235
serv = deploying_device.tm.sys.applications.services.service
236236
tmpl.create(name=iapp_name, partition=self.partition, actions=actions)
237+
pollster(deploying_device.tm.sys.applications.templates.template.load)(
238+
name=iapp_name, partition=self.partition
239+
)
237240
serv.create(
238241
name=iapp_name,
239242
partition=self.partition,

requirements.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
-e .
33

44
# Test Requirements
5+
git+https://github.com/F5Networks/pytest-symbols.git
56
hacking==0.10.2
67
mock==1.3.0
78
pytest==2.9.1

test/functional/cluster/test_cluster.py

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,12 @@ def BigIPSetup():
4545
symbols.bigip2['netloc'],
4646
symbols.bigip2['username'],
4747
symbols.bigip2['password'])
48-
c = ManagementRoot(
49-
symbols.bigip3['netloc'],
50-
symbols.bigip3['username'],
51-
symbols.bigip3['password'])
52-
d = ManagementRoot(
53-
symbols.bigip4['netloc'],
54-
symbols.bigip4['username'],
55-
symbols.bigip4['password']
56-
)
57-
return a, b, c, d
48+
return a, b
5849

5950

6051
@pytest.fixture
6152
def TwoBigIPTeardownSyncFailover(request, BigIPSetup):
62-
a, b, c, d = BigIPSetup
53+
a, b = BigIPSetup
6354
bigip_list = [a, b]
6455

6556
def teardown_cluster():
@@ -74,8 +65,8 @@ def teardown_cluster():
7465

7566
@pytest.fixture
7667
def ThreeBigIPTeardownSyncFailover(request, BigIPSetup):
77-
a, b, c, d = BigIPSetup
78-
bigip_list = [a, b, c]
68+
a, b = BigIPSetup
69+
bigip_list = [a, b]
7970

8071
def teardown_cluster():
8172
cm = ClusterManager(
@@ -94,44 +85,20 @@ def teardown_cluster():
9485
"'run_cluster_tests: True'")
9586
class TestCluster(object):
9687
def test_new_failover_cluster_two_member(self, BigIPSetup):
97-
a, b, c, d = BigIPSetup
88+
a, b = BigIPSetup
9889
bigip_list = [a, b]
99-
cm = ClusterManager()
100-
101-
cm.create(
102-
devices=bigip_list,
103-
device_group_name=DEVICE_GROUP_NAME,
104-
device_group_partition=PARTITION,
105-
device_group_type='sync-failover')
106-
cm.teardown()
107-
108-
def test_new_failover_cluster_three_member(self, BigIPSetup):
109-
a, b, c, d = BigIPSetup
110-
bigip_list = [a, b, c]
111-
cm = ClusterManager()
112-
113-
cm.create(
114-
devices=bigip_list,
115-
device_group_name=DEVICE_GROUP_NAME,
116-
device_group_partition=PARTITION,
117-
device_group_type='sync-failover')
118-
cm.teardown()
90+
for x in range(5):
91+
cm = ClusterManager()
11992

120-
def test_new_failover_cluster_four_member(self, BigIPSetup):
121-
a, b, c, d = BigIPSetup
122-
bigip_list = [a, b, c, d]
123-
cm = ClusterManager()
124-
125-
cm.create(
126-
devices=bigip_list,
127-
device_group_name=DEVICE_GROUP_NAME,
128-
device_group_partition=PARTITION,
129-
device_group_type='sync-failover'
130-
)
131-
cm.teardown()
93+
cm.create(
94+
devices=bigip_list,
95+
device_group_name=DEVICE_GROUP_NAME,
96+
device_group_partition=PARTITION,
97+
device_group_type='sync-failover')
98+
cm.teardown()
13299

133100
def test_existing_failover_cluster(self, BigIPSetup):
134-
a, b, c, d = BigIPSetup
101+
a, b = BigIPSetup
135102
bigip_list = [a, b]
136103
cm = ClusterManager()
137104
kwargs = {'devices': bigip_list,

0 commit comments

Comments
 (0)