Coverage for plugins/modules/purefb_connect.py: 19%
177 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-30 17:06 -0400
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-30 17:06 -0400
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
4# (c) 2020, Simon Dodsley (simon@everpuredata.com)
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7from __future__ import absolute_import, division, print_function
9__metaclass__ = type
11ANSIBLE_METADATA = {
12 "metadata_version": "1.1",
13 "status": ["preview"],
14 "supported_by": "community",
15}
17DOCUMENTATION = r"""
18---
19module: purefb_connect
20version_added: '1.0.0'
21short_description: Manage replication connections between two FlashBlades
22description:
23- Manage replication connections to specified remote FlashBlade system
24author:
25- Everpure Ansible Team (@sdodsley) <pure-ansible-team@everpuredata.com>
26options:
27 state:
28 description:
29 - Create or delete replication connection
30 default: present
31 type: str
32 choices: [ absent, present ]
33 encrypted:
34 description:
35 - Define if replication connection is encrypted
36 type: bool
37 default: false
38 target_url:
39 description:
40 - Management IP address of target FlashBlade system
41 type: str
42 required: true
43 target_api:
44 description:
45 - API token for target FlashBlade system
46 type: str
47 target_repl:
48 description:
49 - Replication IP address of target FlashBlade system
50 - If not set at time of connection creation, will default to
51 all the replication addresses available on the target array
52 at the time of connection creation.
53 type: list
54 elements: str
55 version_added: "1.9.0"
56 default_limit:
57 description:
58 - Default maximum bandwidth threshold for outbound traffic in bytes.
59 - B, K, M, or G units. See examples.
60 - Must be 0 or between 5MB and 28GB
61 - Once exceeded, bandwidth throttling occurs
62 type: str
63 version_added: "1.9.0"
64 window_limit:
65 description:
66 - Maximum bandwidth threshold for outbound traffic during the specified
67 time range in bytes.
68 - B, K, M, or G units. See examples.
69 - Must be 0 or between 5MB and 28GB
70 - Once exceeded, bandwidth throttling occurs
71 type: str
72 version_added: "1.9.0"
73 window_start:
74 description:
75 - The window start time.
76 - The time must be set to the hour.
77 type: str
78 version_added: "1.9.0"
79 window_end:
80 description:
81 - The window end time.
82 - The time must be set to the hour.
83 type: str
84 version_added: "1.9.0"
85 context:
86 description:
87 - Name of fleet member on which to perform the operation.
88 - This requires the array receiving the request is a member of a fleet
89 and the context name to be a member of the same fleet.
90 type: str
91 default: ""
92 version_added: "1.22.0"
93extends_documentation_fragment:
94- everpure.flashblade.everpure.fb
95"""
97EXAMPLES = r"""
98- name: Create a connection to remote FlashBlade system
99 everpure.flashblade.purefb_connect:
100 target_url: 10.10.10.20
101 target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
102 fb_url: 10.10.10.2
103 api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
104- name: Create a connection to remote FlashBlade system with bandwidth limits
105 everpure.flashblade.purefb_connect:
106 target_url: 10.10.10.20
107 target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
108 window_limit: 28G
109 window_start: 1AM
110 window_end: 7AM
111 default_limit: 5M
112 fb_url: 10.10.10.2
113 api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
114- name: Delete connection to target FlashBlade system
115 everpure.flashblade.purefb_connect:
116 state: absent
117 target_url: 10.10.10.20
118 target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
119 fb_url: 10.10.10.2
120 api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
121"""
123RETURN = r"""
124"""
126HAS_PYPURECLIENT = True
127try:
128 from pypureclient.flashblade import (
129 Client,
130 ArrayConnectionPost,
131 TimeWindow,
132 Throttle,
133 )
134except ImportError:
135 HAS_PYPURECLIENT = False
137from ansible.module_utils.basic import AnsibleModule, human_to_bytes
138from ansible_collections.everpure.flashblade.plugins.module_utils.purefb import (
139 get_system,
140 purefb_argument_spec,
141)
142from ansible_collections.everpure.flashblade.plugins.module_utils.time_utils import (
143 time_to_milliseconds,
144)
145from ansible_collections.everpure.flashblade.plugins.module_utils.version import (
146 LooseVersion,
147)
148from ansible_collections.everpure.flashblade.plugins.module_utils.common import (
149 get_error_message,
150 get_rest_api_version,
151)
153CONTEXT_API_VERSION = "2.17"
154FAN_IN_MAXIMUM = 5
155FAN_OUT_MAXIMUM = 5
158def _check_connected(module, blade):
159 api_version = get_rest_api_version(blade)
160 if (
161 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
162 and module.params["context"]
163 ):
164 connected_blades = list(
165 blade.get_array_connections(context_names=[module.params["context"]]).items
166 )
167 else:
168 connected_blades = list(blade.get_array_connections().items)
169 for target in connected_blades:
170 if target.management_address is None:
171 remote_system = Client(
172 target=module.params["target_url"],
173 api_token=module.params["target_api"],
174 )
175 res = remote_system.get_arrays()
176 if res.status_code != 200:
177 module.fail_json(
178 msg="Failed to connect to remote array {0}.".format(
179 module.params["target_url"]
180 )
181 )
182 remote_array = list(res.items)[0].name
183 if target.remote.name == remote_array:
184 return target
185 if target.management_address == module.params[
186 "target_url"
187 ] and target.status in [
188 "connected",
189 "connecting",
190 "partially_connected",
191 ]:
192 return target
193 return None
196def break_connection(module, blade, target_blade):
197 """Break connection between arrays"""
198 api_version = get_rest_api_version(blade)
199 changed = True
200 if not module.check_mode:
201 if (
202 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
203 and module.params["context"]
204 ):
205 source_blade = list(
206 blade.get_arrays(context_names=[module.params["context"]]).items
207 )[0].name
208 else:
209 source_blade = list(blade.get_arrays().items)[0].name
210 if target_blade.management_address is None:
211 module.fail_json(
212 msg="Disconnect can only happen from the array that formed the connection"
213 )
214 if (
215 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
216 and module.params["context"]
217 ):
218 res = blade.delete_array_connections(
219 remote_names=[target_blade.remote.name],
220 context_names=[module.params["context"]],
221 )
222 else:
223 res = blade.delete_array_connections(
224 remote_names=[target_blade.remote.name]
225 )
226 if res.status_code != 200:
227 module.fail_json(
228 msg="Failed to disconnect {0} from {1}. Error: {2}".format(
229 target_blade.remote.name, source_blade, get_error_message(res)
230 )
231 )
232 module.exit_json(changed=changed)
235def create_connection(module, blade):
236 """Create connection between REST 2 capable arrays"""
237 api_version = get_rest_api_version(blade)
238 changed = True
239 if (
240 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
241 and module.params["context"]
242 ):
243 res = blade.get_array_connections(context_names=[module.params["context"]])
244 else:
245 res = blade.get_array_connections()
246 if res.total_item_count >= FAN_OUT_MAXIMUM:
247 module.fail_json(
248 msg="FlashBlade fan-out maximum of {0} already reached".format(
249 FAN_OUT_MAXIMUM
250 )
251 )
252 try:
253 remote_system = Client(
254 target=module.params["target_url"], api_token=module.params["target_api"]
255 )
256 except Exception:
257 module.fail_json(
258 msg="Failed to connect to remote array {0}.".format(
259 module.params["target_url"]
260 )
261 )
262 remote_array = list(remote_system.get_arrays().items)[0].name
263 remote_conn_cnt = remote_system.get_array_connections().total_item_count
264 if remote_conn_cnt >= FAN_IN_MAXIMUM:
265 module.fail_json(
266 msg="Remote array {0} already connected to {1} other array. Fan-In not supported".format(
267 remote_array, remote_conn_cnt
268 )
269 )
270 connection_key = list(remote_system.post_array_connections_connection_key().items)[
271 0
272 ].connection_key
274 if module.params["default_limit"] or module.params["window_limit"]:
275 if module.params["window_limit"]:
276 if not module.params["window_start"]:
277 module.params["window_start"] = "12AM"
278 if not module.params["window_end"]:
279 module.params["window_end"] = "12AM"
280 window = TimeWindow(
281 start=time_to_milliseconds(module.params["window_start"]),
282 end=time_to_milliseconds(module.params["window_end"]),
283 )
284 if module.params["window_limit"] and module.params["default_limit"]:
285 throttle = Throttle(
286 default_limit=human_to_bytes(module.params["default_limit"]),
287 window_limit=human_to_bytes(module.params["window_limit"]),
288 window=window,
289 )
290 elif module.params["window_limit"] and not module.params["default_limit"]:
291 throttle = Throttle(
292 window_limit=human_to_bytes(module.params["window_limit"]),
293 window=window,
294 )
295 else:
296 throttle = Throttle(
297 default_limit=human_to_bytes(module.params["default_limit"]),
298 )
299 connection_info = ArrayConnectionPost(
300 management_address=module.params["target_url"],
301 replication_addresses=module.params["target_repl"],
302 encrypted=module.params["encrypted"],
303 connection_key=connection_key,
304 throttle=throttle,
305 )
306 else:
307 connection_info = ArrayConnectionPost(
308 management_address=module.params["target_url"],
309 replication_addresses=module.params["target_repl"],
310 encrypted=module.params["encrypted"],
311 connection_key=connection_key,
312 )
313 if not module.check_mode:
314 if (
315 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
316 and module.params["context"]
317 ):
318 res = blade.post_array_connections(
319 array_connection=connection_info,
320 context_names=[module.params["context"]],
321 )
322 else:
323 res = blade.post_array_connections(array_connection=connection_info)
324 if res.status_code != 200:
325 module.fail_json(
326 msg="Failed to connect to remote array {0}. Error: {1}".format(
327 remote_array, get_error_message(res)
328 )
329 )
330 module.exit_json(changed=changed)
333def update_connection(module, blade):
334 """Update REST 2 based array connection"""
335 changed = False
336 api_version = get_rest_api_version(blade)
337 remote_blade = Client(
338 target=module.params["target_url"], api_token=module.params["target_api"]
339 )
340 remote_name = list(remote_blade.get_arrays().items)[0].name
341 remote_connection = list(
342 blade.get_array_connections(filter="remote.name='" + remote_name + "'").items
343 )[0]
344 if remote_connection.management_address is None:
345 module.fail_json(
346 msg="Update can only happen from the array that formed the connection"
347 )
348 if module.params["encrypted"] != remote_connection.encrypted:
349 if (
350 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
351 and module.params["context"]
352 ):
353 res = blade.get_file_system_replica_links(
354 context_names=[module.params["context"]]
355 )
356 else:
357 res = blade.get_file_system_replica_links()
358 if module.params["encrypted"] and res.total_item_count != 0:
359 module.fail_json(
360 msg="Cannot turn array connection encryption on if file system replica links exist"
361 )
362 current_connection = {
363 "encrypted": remote_connection.encrypted,
364 "replication_addresses": sorted(remote_connection.replication_addresses),
365 "throttle": [],
366 }
367 if (
368 not remote_connection.throttle.default_limit
369 and not remote_connection.throttle.window_limit
370 ):
371 if (
372 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
373 and module.params["context"]
374 ):
375 res = blade.get_bucket_replica_links(
376 context_names=[module.params["context"]]
377 )
378 else:
379 res = blade.get_bucket_replica_links()
380 if (
381 module.params["default_limit"] or module.params["window_limit"]
382 ) and res.total_item_count != 0:
383 module.fail_json(
384 msg="Cannot set throttle when bucket replica links already exist"
385 )
386 current_connection["throttle"] = {
387 "default_limit": remote_connection.throttle.default_limit,
388 "window_limit": remote_connection.throttle.window_limit,
389 "start": remote_connection.throttle.window.start,
390 "end": remote_connection.throttle.window.end,
391 }
392 if module.params["encrypted"]:
393 encryption = module.params["encrypted"]
394 else:
395 encryption = remote_connection.encrypted
396 if module.params["target_repl"]:
397 target_repl = sorted(module.params["target_repl"])
398 else:
399 target_repl = remote_connection.replication_addresses
400 if module.params["default_limit"]:
401 default_limit = human_to_bytes(module.params["default_limit"])
402 if default_limit == 0:
403 default_limit = None
404 else:
405 default_limit = remote_connection.throttle.default_limit
406 if module.params["window_limit"]:
407 window_limit = human_to_bytes(module.params["window_limit"])
408 else:
409 window_limit = remote_connection.throttle.window_limit
410 if module.params["window_start"]:
411 start = time_to_milliseconds(module.params["window_start"])
412 else:
413 start = remote_connection.throttle.window.start
414 if module.params["window_end"]:
415 end = time_to_milliseconds(module.params["window_end"])
416 else:
417 end = remote_connection.throttle.window.end
419 new_connection = {
420 "encrypted": encryption,
421 "replication_addresses": target_repl,
422 "throttle": [],
423 }
424 new_connection["throttle"] = {
425 "default_limit": default_limit,
426 "window_limit": window_limit,
427 "start": start,
428 "end": end,
429 }
430 if new_connection != current_connection:
431 changed = True
432 if not module.check_mode:
433 window = TimeWindow(
434 start=new_connection["throttle"]["start"],
435 end=new_connection["throttle"]["end"],
436 )
437 throttle = Throttle(
438 default_limit=new_connection["throttle"]["default_limit"],
439 window_limit=new_connection["throttle"]["window_limit"],
440 window=window,
441 )
442 connection_info = ArrayConnectionPost(
443 replication_addresses=new_connection["replication_addresses"],
444 encrypted=new_connection["encrypted"],
445 throttle=throttle,
446 )
447 if (
448 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
449 and module.params["context"]
450 ):
451 res = blade.patch_array_connections(
452 remote_names=[remote_name],
453 array_connection=connection_info,
454 context_names=[module.params["context"]],
455 )
456 else:
457 res = blade.patch_array_connections(
458 remote_names=[remote_name], array_connection=connection_info
459 )
460 if res.status_code != 200:
461 module.fail_json(
462 msg="Failed to update connection to remote array {0}. Error: {1}".format(
463 remote_name, get_error_message(res)
464 )
465 )
466 module.exit_json(changed=changed)
469def main():
470 argument_spec = purefb_argument_spec()
471 argument_spec.update(
472 dict(
473 state=dict(type="str", default="present", choices=["absent", "present"]),
474 encrypted=dict(type="bool", default=False),
475 target_url=dict(type="str", required=True),
476 target_api=dict(type="str", no_log=True),
477 target_repl=dict(type="list", elements="str"),
478 default_limit=dict(type="str"),
479 window_limit=dict(type="str"),
480 window_start=dict(type="str"),
481 window_end=dict(type="str"),
482 context=dict(type="str", default=""),
483 )
484 )
486 required_if = [("state", "present", ["target_api"])]
488 module = AnsibleModule(
489 argument_spec, required_if=required_if, supports_check_mode=True
490 )
492 if not HAS_PYPURECLIENT:
493 module.fail_json(msg="py-pure-client sdk is required for this module")
495 state = module.params["state"]
496 blade = get_system(module)
497 api_version = get_rest_api_version(blade)
498 if (
499 LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version)
500 and not module.params["context"]
501 ):
502 # If no context is provided set the context to the local array name
503 fleet_res = blade.get_fleets()
504 if fleet_res.status_code == 200 and list(fleet_res.items):
505 module.params["context"] = list(blade.get_arrays().items)[0].name
507 if module.params["default_limit"]:
508 default_limit = human_to_bytes(module.params["default_limit"])
509 if default_limit != 0 and not (5242880 <= default_limit <= 30064771072):
510 module.fail_json(msg="Default Bandwidth must be between 5MB and 28GB")
511 if module.params["window_limit"]:
512 window_limit = human_to_bytes(module.params["window_limit"])
513 if window_limit != 0 and not (5242880 <= window_limit <= 30064771072):
514 module.fail_json(msg="Window Bandwidth must be between 5MB and 28GB")
516 target_blade = _check_connected(module, blade)
517 if state == "present" and not target_blade:
518 create_connection(module, blade)
519 elif state == "present" and target_blade:
520 update_connection(module, blade)
521 elif state == "absent" and target_blade:
522 break_connection(module, blade, target_blade)
524 module.exit_json(changed=False)
527if __name__ == "__main__":
528 main()