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
11
12ANSIBLE_METADATA = {
13    "metadata_version": "1.1",
14    "status": ["preview"],
15    "supported_by": "community",
16}
17
18
19DOCUMENTATION = """
20---
21module: purefa_network
22short_description:  Manage network interfaces in a Pure Storage FlashArray
23version_added: '1.0.0'
24description:
25    - This module manages the physical and virtual network interfaces on a Pure Storage FlashArray.
26    - To manage VLAN interfaces use the I(purefa_vlan) module.
27    - To manage network subnets use the I(purefa_subnet) module.
28    - To remove an IP address from a non-management port use 0.0.0.0/0
29author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
30options:
31  name:
32    description:
33      - Interface name (physical or virtual).
34    required: true
35    type: str
36  state:
37    description:
38      - State of existing interface (on/off).
39    required: false
40    default: present
41    choices: [ "present", "absent" ]
42    type: str
43  address:
44    description:
45      - IPv4 or IPv6 address of interface in CIDR notation.
46      - To remove an IP address from a non-management port use 0.0.0.0/0
47    required: false
48    type: str
49  gateway:
50    description:
51      - IPv4 or IPv6 address of interface gateway.
52    required: false
53    type: str
54  mtu:
55    description:
56      - MTU size of the interface. Range is 1280 to 9216.
57    required: false
58    default: 1500
59    type: int
60extends_documentation_fragment:
61    - purestorage.flasharray.purestorage.fa
62"""
63
64EXAMPLES = """
65- name: Configure and enable network interface ct0.eth8
66  purefa_network:
67    name: ct0.eth8
68    gateway: 10.21.200.1
69    address: "10.21.200.18/24"
70    mtu: 9000
71    state: present
72    fa_url: 10.10.10.2
73    api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
74
75- name: Disable physical interface ct1.eth2
76  purefa_network:
77    name: ct1.eth2
78    state: absent
79    fa_url: 10.10.10.2
80    api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
81
82- name: Enable virtual network interface vir0
83  purefa_network:
84    name: vir0
85    state: present
86    fa_url: 10.10.10.2
87    api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
88
89- name: Remove an IP address from iSCSI interface ct0.eth4
90  purefa_network:
91    name: ct0.eth4
92    address: 0.0.0.0/0
93    gateway: 0.0.0.0
94    fa_url: 10.10.10.2
95    api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
96"""
97
98RETURN = """
99"""
100
101try:
102    from netaddr import IPAddress, IPNetwork
103
104    HAS_NETADDR = True
105except ImportError:
106    HAS_NETADDR = False
107
108try:
109    from pypureclient.flasharray import NetworkInterfacePatch
110
111    HAS_PYPURECLIENT = True
112except ImportError:
113    HAS_PYPURECLIENT = False
114
115from ansible.module_utils.basic import AnsibleModule
116from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
117    get_system,
118    get_array,
119    purefa_argument_spec,
120)
121
122FC_ENABLE_API = "2.4"
123
124
125def _get_fc_interface(module, array):
126    """Return FC Interface or None"""
127    interface = {}
128    interface_list = array.get_network_interfaces(names=[module.params["name"]])
129    if interface_list.status_code == 200:
130        interface = list(interface_list.items)[0]
131        return interface
132    else:
133        return None
134
135
136def _get_interface(module, array):
137    """Return Network Interface or None"""
138    interface = {}
139    if module.params["name"][0] == "v":
140        try:
141            interface = array.get_network_interface(module.params["name"])
142        except Exception:
143            return None
144    else:
145        try:
146            interfaces = array.list_network_interfaces()
147        except Exception:
148            return None
149        for ints in range(0, len(interfaces)):
150            if interfaces[ints]["name"] == module.params["name"]:
151                interface = interfaces[ints]
152                break
153    return interface
154
155
156def update_fc_interface(module, array, interface):
157    """Modify FC Interface settings"""
158    changed = False
159    if not interface.enabled and module.params["state"] == "present":
160        changed = True
161        if not module.check_mode:
162            network = NetworkInterfacePatch(enabled=True, override_npiv_check=True)
163            res = array.patch_network_interfaces(
164                names=[module.params["name"]], network=network
165            )
166            if res.status_code != 200:
167                module.fail_json(
168                    msg="Failed to enable interface {0}.".format(module.params["name"])
169                )
170    if interface.enabled and module.params["state"] == "absent":
171        changed = True
172        if not module.check_mode:
173            network = NetworkInterfacePatch(enabled=False, override_npiv_check=True)
174            res = array.patch_network_interfaces(
175                names=[module.params["name"]], network=network
176            )
177            if res.status_code != 200:
178                module.fail_json(
179                    msg="Failed to disable interface {0}.".format(module.params["name"])
180                )
181    module.exit_json(changed=changed)
182
183
184def update_interface(module, array, interface):
185    """Modify Interface settings"""
186    changed = False
187    current_state = {
188        "mtu": interface["mtu"],
189        "gateway": interface["gateway"],
190        "address": interface["address"],
191        "netmask": interface["netmask"],
192    }
193    if not module.params["address"]:
194        address = interface["address"]
195    else:
196        if module.params["gateway"]:
197            if module.params["gateway"] and module.params["gateway"] not in IPNetwork(
198                module.params["address"]
199            ):
200                module.fail_json(msg="Gateway and subnet are not compatible.")
201            elif not module.params["gateway"] and interface["gateway"] not in [
202                None,
203                IPNetwork(module.params["address"]),
204            ]:
205                module.fail_json(msg="Gateway and subnet are not compatible.")
206        address = str(module.params["address"].split("/", 1)[0])
207    if not module.params["mtu"]:
208        mtu = interface["mtu"]
209    else:
210        if not 1280 <= module.params["mtu"] <= 9216:
211            module.fail_json(
212                msg="MTU {0} is out of range (1280 to 9216)".format(
213                    module.params["mtu"]
214                )
215            )
216        else:
217            mtu = module.params["mtu"]
218    if module.params["address"]:
219        netmask = str(IPNetwork(module.params["address"]).netmask)
220    else:
221        netmask = interface["netmask"]
222    if not module.params["gateway"]:
223        gateway = interface["gateway"]
224    else:
225        cidr = str(IPAddress(netmask).netmask_bits())
226        full_addr = address + "/" + cidr
227        if module.params["gateway"] not in IPNetwork(full_addr):
228            module.fail_json(msg="Gateway and subnet are not compatible.")
229        gateway = module.params["gateway"]
230    new_state = {
231        "address": address,
232        "mtu": mtu,
233        "gateway": gateway,
234        "netmask": netmask,
235    }
236    if new_state != current_state:
237        changed = True
238        if (
239            "management" in interface["services"]
240            or "app" in interface["services"]
241            and address == "0.0.0.0/0"
242        ):
243            module.fail_json(
244                msg="Removing IP address from a management or app port is not supported"
245            )
246        if not module.check_mode:
247            try:
248                if new_state["gateway"] is not None:
249                    array.set_network_interface(
250                        interface["name"],
251                        address=new_state["address"],
252                        mtu=new_state["mtu"],
253                        netmask=new_state["netmask"],
254                        gateway=new_state["gateway"],
255                    )
256                else:
257                    array.set_network_interface(
258                        interface["name"],
259                        address=new_state["address"],
260                        mtu=new_state["mtu"],
261                        netmask=new_state["netmask"],
262                    )
263            except Exception:
264                module.fail_json(
265                    msg="Failed to change settings for interface {0}.".format(
266                        interface["name"]
267                    )
268                )
269    if not interface["enabled"] and module.params["state"] == "present":
270        changed = True
271        if not module.check_mode:
272            try:
273                array.enable_network_interface(interface["name"])
274            except Exception:
275                module.fail_json(
276                    msg="Failed to enable interface {0}.".format(interface["name"])
277                )
278    if interface["enabled"] and module.params["state"] == "absent":
279        changed = True
280        if not module.check_mode:
281            try:
282                array.disable_network_interface(interface["name"])
283            except Exception:
284                module.fail_json(
285                    msg="Failed to disable interface {0}.".format(interface["name"])
286                )
287
288    module.exit_json(changed=changed)
289
290
291def main():
292    argument_spec = purefa_argument_spec()
293    argument_spec.update(
294        dict(
295            name=dict(type="str", required=True),
296            state=dict(type="str", default="present", choices=["present", "absent"]),
297            address=dict(type="str"),
298            gateway=dict(type="str"),
299            mtu=dict(type="int", default=1500),
300        )
301    )
302
303    module = AnsibleModule(argument_spec, supports_check_mode=True)
304
305    if not HAS_NETADDR:
306        module.fail_json(msg="netaddr module is required")
307
308    array = get_system(module)
309    api_version = array._list_available_rest_versions()
310    if module.params["name"].split(".")[1][0].lower() == "f":
311        if FC_ENABLE_API in api_version:
312            if not HAS_PYPURECLIENT:
313                module.fail_json(msg="pypureclient module is required")
314            array = get_array(module)
315            interface = _get_fc_interface(module, array)
316            if not interface:
317                module.fail_json(msg="Invalid network interface specified.")
318            else:
319                update_fc_interface(module, array, interface)
320        else:
321            module.warn("Purity version does not support enabling/disabling FC ports")
322    else:
323        interface = _get_interface(module, array)
324        if not interface:
325            module.fail_json(msg="Invalid network interface specified.")
326        else:
327            update_interface(module, array, interface)
328
329    module.exit_json(changed=False)
330
331
332if __name__ == "__main__":
333    main()
334