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

1#!/usr/bin/python 

2# -*- coding: utf-8 -*- 

3 

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) 

6 

7from __future__ import absolute_import, division, print_function 

8 

9__metaclass__ = type 

10 

11ANSIBLE_METADATA = { 

12 "metadata_version": "1.1", 

13 "status": ["preview"], 

14 "supported_by": "community", 

15} 

16 

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""" 

96 

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""" 

122 

123RETURN = r""" 

124""" 

125 

126HAS_PYPURECLIENT = True 

127try: 

128 from pypureclient.flashblade import ( 

129 Client, 

130 ArrayConnectionPost, 

131 TimeWindow, 

132 Throttle, 

133 ) 

134except ImportError: 

135 HAS_PYPURECLIENT = False 

136 

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) 

152 

153CONTEXT_API_VERSION = "2.17" 

154FAN_IN_MAXIMUM = 5 

155FAN_OUT_MAXIMUM = 5 

156 

157 

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 

194 

195 

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) 

233 

234 

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 

273 

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) 

331 

332 

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 

418 

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) 

467 

468 

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 ) 

485 

486 required_if = [("state", "present", ["target_api"])] 

487 

488 module = AnsibleModule( 

489 argument_spec, required_if=required_if, supports_check_mode=True 

490 ) 

491 

492 if not HAS_PYPURECLIENT: 

493 module.fail_json(msg="py-pure-client sdk is required for this module") 

494 

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 

506 

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") 

515 

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) 

523 

524 module.exit_json(changed=False) 

525 

526 

527if __name__ == "__main__": 

528 main()