Skip to content

Commit a03cb7c

Browse files
committed
Merge pull request #472 from pjbreaux/feature.active_standby_cluster
Feature.active standby cluster
2 parents aab307a + a84e898 commit a03cb7c

8 files changed

Lines changed: 199 additions & 99 deletions

File tree

f5/multi_device/cluster/__init__.py

Lines changed: 12 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,14 +148,15 @@ 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
'''
151155

152156
if hasattr(self, 'cluster'):
153157
msg = 'The ClusterManager is already managing a cluster.'
154158
raise AlreadyManagingCluster(msg)
159+
self._check_device_number(kwargs['devices'])
155160
print('Adding trusted peers to root BigIP...')
156161
self.trust_domain.create(
157162
devices=kwargs['devices'],
@@ -163,7 +168,7 @@ def create(self, **kwargs):
163168
self.cluster = Cluster(**kwargs)
164169

165170
def teardown(self):
166-
'''Teardown the cluster of BigIP devices.'''
171+
'''Teardown the cluster of BIG-IP® devices.'''
167172

168173
print('Tearing down the cluster...')
169174
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

0 commit comments

Comments
 (0)