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