1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# (c) 2020, Simon Dodsley (simon@purestorage.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- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.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 47extends_documentation_fragment: 48- purestorage.flashblade.purestorage.fb 49""" 50 51EXAMPLES = r""" 52- name: Create a connection to remote FlashBlade system 53 purefb_connect: 54 target_url: 10.10.10.20 55 target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6 56 fb_url: 10.10.10.2 57 api_token: e31060a7-21fc-e277-6240-25983c6c4592 58- name: Delete connection to target FlashBlade system 59 purefb_connect: 60 state: absent 61 target_url: 10.10.10.20 62 target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6 63 fb_url: 10.10.10.2 64 api_token: e31060a7-21fc-e277-6240-25983c6c4592 65""" 66 67RETURN = r""" 68""" 69 70HAS_PURITYFB = True 71try: 72 from purity_fb import PurityFb, ArrayConnection, ArrayConnectionPost 73except ImportError: 74 HAS_PURITYFB = False 75 76from ansible.module_utils.basic import AnsibleModule 77from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import ( 78 get_blade, 79 purefb_argument_spec, 80) 81 82 83MIN_REQUIRED_API_VERSION = "1.9" 84 85 86def _check_connected(module, blade): 87 connected_blades = blade.array_connections.list_array_connections() 88 for target in range(0, len(connected_blades.items)): 89 if connected_blades.items[target].management_address is None: 90 try: 91 remote_system = PurityFb(module.params["target_url"]) 92 remote_system.login(module.params["target_api"]) 93 remote_array = remote_system.arrays.list_arrays().items[0].name 94 if connected_blades.items[target].remote.name == remote_array: 95 return connected_blades.items[target] 96 except Exception: 97 module.fail_json( 98 msg="Failed to connect to remote array {0}.".format( 99 module.params["target_url"] 100 ) 101 ) 102 if connected_blades.items[target].management_address == module.params[ 103 "target_url" 104 ] and connected_blades.items[target].status in [ 105 "connected", 106 "connecting", 107 "partially_connected", 108 ]: 109 return connected_blades.items[target] 110 return None 111 112 113def break_connection(module, blade, target_blade): 114 """Break connection between arrays""" 115 changed = True 116 if not module.check_mode: 117 source_blade = blade.arrays.list_arrays().items[0].name 118 try: 119 if target_blade.management_address is None: 120 module.fail_json( 121 msg="Disconnect can only happen from the array that formed the connection" 122 ) 123 blade.array_connections.delete_array_connections( 124 remote_names=[target_blade.remote.name] 125 ) 126 except Exception: 127 module.fail_json( 128 msg="Failed to disconnect {0} from {1}.".format( 129 target_blade.remote.name, source_blade 130 ) 131 ) 132 module.exit_json(changed=changed) 133 134 135def create_connection(module, blade): 136 """Create connection between arrays""" 137 changed = True 138 if not module.check_mode: 139 remote_array = module.params["target_url"] 140 try: 141 remote_system = PurityFb(module.params["target_url"]) 142 remote_system.login(module.params["target_api"]) 143 remote_array = remote_system.arrays.list_arrays().items[0].name 144 remote_conn_cnt = ( 145 remote_system.array_connections.list_array_connections().pagination_info.total_item_count 146 ) 147 # TODO: SD - Update with new max when fan-in/fan-out is enabled for FB 148 if remote_conn_cnt == 1: 149 module.fail_json( 150 msg="Remote array {0} already connected to another array. Fan-In not supported".format( 151 remote_array 152 ) 153 ) 154 connection_key = ( 155 remote_system.array_connections.create_array_connections_connection_keys() 156 .items[0] 157 .connection_key 158 ) 159 remote_array = remote_system.arrays.list_arrays().items[0].name 160 connection_info = ArrayConnectionPost( 161 management_address=module.params["target_url"], 162 encrypted=module.params["encrypted"], 163 connection_key=connection_key, 164 ) 165 blade.array_connections.create_array_connections( 166 array_connection=connection_info 167 ) 168 except Exception: 169 module.fail_json( 170 msg="Failed to connect to remote array {0}.".format(remote_array) 171 ) 172 module.exit_json(changed=changed) 173 174 175def update_connection(module, blade, target_blade): 176 """Update array connection - only encryption currently""" 177 changed = True 178 if not module.check_mode: 179 if target_blade.management_address is None: 180 module.fail_json( 181 msg="Update can only happen from the array that formed the connection" 182 ) 183 if module.params["encrypted"] != target_blade.encrypted: 184 if ( 185 module.params["encrypted"] 186 and blade.file_system_replica_links.list_file_system_replica_links().pagination_info.total_item_count 187 != 0 188 ): 189 module.fail_json( 190 msg="Cannot turn array connection encryption on if file system replica links exist" 191 ) 192 new_attr = ArrayConnection(encrypted=module.params["encrypted"]) 193 changed = True 194 if not module.check_mode: 195 try: 196 blade.array_connections.update_array_connections( 197 remote_names=[target_blade.remote.name], 198 array_connection=new_attr, 199 ) 200 except Exception: 201 module.fail_json( 202 msg="Failed to change encryption setting for array connection." 203 ) 204 else: 205 changed = False 206 module.exit_json(changed=changed) 207 208 209def main(): 210 argument_spec = purefb_argument_spec() 211 argument_spec.update( 212 dict( 213 state=dict(type="str", default="present", choices=["absent", "present"]), 214 encrypted=dict(type="bool", default=False), 215 target_url=dict(type="str", required=True), 216 target_api=dict(type="str", no_log=True), 217 ) 218 ) 219 220 required_if = [("state", "present", ["target_api"])] 221 222 module = AnsibleModule( 223 argument_spec, required_if=required_if, supports_check_mode=True 224 ) 225 226 if not HAS_PURITYFB: 227 module.fail_json(msg="purity_fb sdk is required for this module") 228 229 state = module.params["state"] 230 blade = get_blade(module) 231 versions = blade.api_version.list_versions().versions 232 233 if MIN_REQUIRED_API_VERSION not in versions: 234 module.fail_json( 235 msg="Minimum FlashBlade REST version required: {0}".format( 236 MIN_REQUIRED_API_VERSION 237 ) 238 ) 239 240 target_blade = _check_connected(module, blade) 241 if state == "present" and not target_blade: 242 # TODO: SD - Update with new max when fan-out is supported 243 if ( 244 blade.array_connections.list_array_connections().pagination_info.total_item_count 245 == 1 246 ): 247 module.fail_json( 248 msg="Source FlashBlade already connected to another array. Fan-Out not supported" 249 ) 250 create_connection(module, blade) 251 elif state == "present" and target_blade: 252 update_connection(module, blade, target_blade) 253 elif state == "absent" and target_blade: 254 break_connection(module, blade, target_blade) 255 256 module.exit_json(changed=False) 257 258 259if __name__ == "__main__": 260 main() 261