1"""
2The networking module for Windows based systems
3"""
4
5import logging
6import time
7
8import salt.utils.network
9import salt.utils.platform
10import salt.utils.validate.net
11from salt.exceptions import CommandExecutionError, SaltInvocationError
12
13# Set up logging
14log = logging.getLogger(__name__)
15
16# Define the module's virtual name
17__virtualname__ = "ip"
18
19
20def __virtual__():
21    """
22    Confine this module to Windows systems
23    """
24    if salt.utils.platform.is_windows():
25        return __virtualname__
26    return (False, "Module win_ip: module only works on Windows systems")
27
28
29def _interface_configs():
30    """
31    Return all interface configs
32    """
33    cmd = ["netsh", "interface", "ip", "show", "config"]
34    lines = __salt__["cmd.run"](cmd, python_shell=False).splitlines()
35    ret = {}
36    current_iface = None
37    current_ip_list = None
38
39    for line in lines:
40
41        line = line.strip()
42        if not line:
43            current_iface = None
44            current_ip_list = None
45            continue
46
47        if "Configuration for interface" in line:
48            _, iface = line.rstrip('"').split('"', 1)  # get iface name
49            current_iface = {}
50            ret[iface] = current_iface
51            continue
52
53        if ":" not in line:
54            if current_ip_list:
55                current_ip_list.append(line)
56            else:
57                log.warning('Cannot parse "%s"', line)
58            continue
59
60        key, val = line.split(":", 1)
61        key = key.strip()
62        val = val.strip()
63
64        lkey = key.lower()
65        if ("dns servers" in lkey) or ("wins servers" in lkey):
66            current_ip_list = []
67            current_iface[key] = current_ip_list
68            current_ip_list.append(val)
69
70        elif "ip address" in lkey:
71            current_iface.setdefault("ip_addrs", []).append({key: val})
72
73        elif "subnet prefix" in lkey:
74            subnet, _, netmask = val.split(" ", 2)
75            last_ip = current_iface["ip_addrs"][-1]
76            last_ip["Subnet"] = subnet.strip()
77            last_ip["Netmask"] = netmask.lstrip().rstrip(")")
78
79        else:
80            current_iface[key] = val
81
82    return ret
83
84
85def raw_interface_configs():
86    """
87    Return raw configs for all interfaces
88
89    CLI Example:
90
91    .. code-block:: bash
92
93        salt -G 'os_family:Windows' ip.raw_interface_configs
94    """
95    cmd = ["netsh", "interface", "ip", "show", "config"]
96    return __salt__["cmd.run"](cmd, python_shell=False)
97
98
99def get_all_interfaces():
100    """
101    Return configs for all interfaces
102
103    CLI Example:
104
105    .. code-block:: bash
106
107        salt -G 'os_family:Windows' ip.get_all_interfaces
108    """
109    return _interface_configs()
110
111
112def get_interface(iface):
113    """
114    Return the configuration of a network interface
115
116    CLI Example:
117
118    .. code-block:: bash
119
120        salt -G 'os_family:Windows' ip.get_interface 'Local Area Connection'
121    """
122    return _interface_configs().get(iface, {})
123
124
125def is_enabled(iface):
126    """
127    Returns ``True`` if interface is enabled, otherwise ``False``
128
129    CLI Example:
130
131    .. code-block:: bash
132
133        salt -G 'os_family:Windows' ip.is_enabled 'Local Area Connection #2'
134    """
135    cmd = ["netsh", "interface", "show", "interface", "name={}".format(iface)]
136    iface_found = False
137    for line in __salt__["cmd.run"](cmd, python_shell=False).splitlines():
138        if "Connect state:" in line:
139            iface_found = True
140            return line.split()[-1] == "Connected"
141    if not iface_found:
142        raise CommandExecutionError("Interface '{}' not found".format(iface))
143    return False
144
145
146def is_disabled(iface):
147    """
148    Returns ``True`` if interface is disabled, otherwise ``False``
149
150    CLI Example:
151
152    .. code-block:: bash
153
154        salt -G 'os_family:Windows' ip.is_disabled 'Local Area Connection #2'
155    """
156    return not is_enabled(iface)
157
158
159def enable(iface):
160    """
161    Enable an interface
162
163    CLI Example:
164
165    .. code-block:: bash
166
167        salt -G 'os_family:Windows' ip.enable 'Local Area Connection #2'
168    """
169    if is_enabled(iface):
170        return True
171    cmd = [
172        "netsh",
173        "interface",
174        "set",
175        "interface",
176        "name={}".format(iface),
177        "admin=ENABLED",
178    ]
179    __salt__["cmd.run"](cmd, python_shell=False)
180    return is_enabled(iface)
181
182
183def disable(iface):
184    """
185    Disable an interface
186
187    CLI Example:
188
189    .. code-block:: bash
190
191        salt -G 'os_family:Windows' ip.disable 'Local Area Connection #2'
192    """
193    if is_disabled(iface):
194        return True
195    cmd = [
196        "netsh",
197        "interface",
198        "set",
199        "interface",
200        "name={}".format(iface),
201        "admin=DISABLED",
202    ]
203    __salt__["cmd.run"](cmd, python_shell=False)
204    return is_disabled(iface)
205
206
207def get_subnet_length(mask):
208    """
209    Convenience function to convert the netmask to the CIDR subnet length
210
211    CLI Example:
212
213    .. code-block:: bash
214
215        salt -G 'os_family:Windows' ip.get_subnet_length 255.255.255.0
216    """
217    if not salt.utils.validate.net.netmask(mask):
218        raise SaltInvocationError("'{}' is not a valid netmask".format(mask))
219    return salt.utils.network.get_net_size(mask)
220
221
222def set_static_ip(iface, addr, gateway=None, append=False):
223    """
224    Set static IP configuration on a Windows NIC
225
226    iface
227        The name of the interface to manage
228
229    addr
230        IP address with subnet length (ex. ``10.1.2.3/24``). The
231        :mod:`ip.get_subnet_length <salt.modules.win_ip.get_subnet_length>`
232        function can be used to calculate the subnet length from a netmask.
233
234    gateway : None
235        If specified, the default gateway will be set to this value.
236
237    append : False
238        If ``True``, this IP address will be added to the interface. Default is
239        ``False``, which overrides any existing configuration for the interface
240        and sets ``addr`` as the only address on the interface.
241
242    CLI Example:
243
244    .. code-block:: bash
245
246        salt -G 'os_family:Windows' ip.set_static_ip 'Local Area Connection' 10.1.2.3/24 gateway=10.1.2.1
247        salt -G 'os_family:Windows' ip.set_static_ip 'Local Area Connection' 10.1.2.4/24 append=True
248    """
249
250    def _find_addr(iface, addr, timeout=1):
251        ip, cidr = addr.rsplit("/", 1)
252        netmask = salt.utils.network.cidr_to_ipv4_netmask(cidr)
253        for idx in range(timeout):
254            for addrinfo in get_interface(iface).get("ip_addrs", []):
255                if addrinfo["IP Address"] == ip and addrinfo["Netmask"] == netmask:
256                    return addrinfo
257            time.sleep(1)
258        return {}
259
260    if not salt.utils.validate.net.ipv4_addr(addr):
261        raise SaltInvocationError("Invalid address '{}'".format(addr))
262
263    if gateway and not salt.utils.validate.net.ipv4_addr(addr):
264        raise SaltInvocationError("Invalid default gateway '{}'".format(gateway))
265
266    if "/" not in addr:
267        addr += "/32"
268
269    if append and _find_addr(iface, addr):
270        raise CommandExecutionError(
271            "Address '{}' already exists on interface '{}'".format(addr, iface)
272        )
273
274    cmd = ["netsh", "interface", "ip"]
275    if append:
276        cmd.append("add")
277    else:
278        cmd.append("set")
279    cmd.extend(["address", "name={}".format(iface)])
280    if not append:
281        cmd.append("source=static")
282    cmd.append("address={}".format(addr))
283    if gateway:
284        cmd.append("gateway={}".format(gateway))
285
286    result = __salt__["cmd.run_all"](cmd, python_shell=False)
287    if result["retcode"] != 0:
288        raise CommandExecutionError(
289            "Unable to set IP address: {}".format(result["stderr"])
290        )
291
292    new_addr = _find_addr(iface, addr, timeout=10)
293    if not new_addr:
294        return {}
295
296    ret = {"Address Info": new_addr}
297    if gateway:
298        ret["Default Gateway"] = gateway
299    return ret
300
301
302def set_dhcp_ip(iface):
303    """
304    Set Windows NIC to get IP from DHCP
305
306    CLI Example:
307
308    .. code-block:: bash
309
310        salt -G 'os_family:Windows' ip.set_dhcp_ip 'Local Area Connection'
311    """
312    cmd = ["netsh", "interface", "ip", "set", "address", iface, "dhcp"]
313    __salt__["cmd.run"](cmd, python_shell=False)
314    return {"Interface": iface, "DHCP enabled": "Yes"}
315
316
317def set_static_dns(iface, *addrs):
318    """
319    Set static DNS configuration on a Windows NIC
320
321    Args:
322
323        iface (str): The name of the interface to set
324
325        addrs (*):
326            One or more DNS servers to be added. To clear the list of DNS
327            servers pass an empty list (``[]``). If undefined or ``None`` no
328            changes will be made.
329
330    Returns:
331        dict: A dictionary containing the new DNS settings
332
333    CLI Example:
334
335    .. code-block:: bash
336
337        salt -G 'os_family:Windows' ip.set_static_dns 'Local Area Connection' '192.168.1.1'
338        salt -G 'os_family:Windows' ip.set_static_dns 'Local Area Connection' '192.168.1.252' '192.168.1.253'
339    """
340    if not addrs or str(addrs[0]).lower() == "none":
341        return {"Interface": iface, "DNS Server": "No Changes"}
342    # Clear the list of DNS servers if [] is passed
343    if str(addrs[0]).lower() == "[]":
344        log.debug("Clearing list of DNS servers")
345        cmd = [
346            "netsh",
347            "interface",
348            "ip",
349            "set",
350            "dns",
351            "name={}".format(iface),
352            "source=static",
353            "address=none",
354        ]
355        __salt__["cmd.run"](cmd, python_shell=False)
356        return {"Interface": iface, "DNS Server": []}
357    addr_index = 1
358    for addr in addrs:
359        if addr_index == 1:
360            cmd = [
361                "netsh",
362                "interface",
363                "ip",
364                "set",
365                "dns",
366                "name={}".format(iface),
367                "source=static",
368                "address={}".format(addr),
369                "register=primary",
370            ]
371            __salt__["cmd.run"](cmd, python_shell=False)
372            addr_index = addr_index + 1
373        else:
374            cmd = [
375                "netsh",
376                "interface",
377                "ip",
378                "add",
379                "dns",
380                "name={}".format(iface),
381                "address={}".format(addr),
382                "index={}".format(addr_index),
383            ]
384            __salt__["cmd.run"](cmd, python_shell=False)
385            addr_index = addr_index + 1
386    return {"Interface": iface, "DNS Server": addrs}
387
388
389def set_dhcp_dns(iface):
390    """
391    Set DNS source to DHCP on Windows
392
393    CLI Example:
394
395    .. code-block:: bash
396
397        salt -G 'os_family:Windows' ip.set_dhcp_dns 'Local Area Connection'
398    """
399    cmd = ["netsh", "interface", "ip", "set", "dns", iface, "dhcp"]
400    __salt__["cmd.run"](cmd, python_shell=False)
401    return {"Interface": iface, "DNS Server": "DHCP"}
402
403
404def set_dhcp_all(iface):
405    """
406    Set both IP Address and DNS to DHCP
407
408    CLI Example:
409
410    .. code-block:: bash
411
412        salt -G 'os_family:Windows' ip.set_dhcp_all 'Local Area Connection'
413    """
414    set_dhcp_ip(iface)
415    set_dhcp_dns(iface)
416    return {"Interface": iface, "DNS Server": "DHCP", "DHCP enabled": "Yes"}
417
418
419def get_default_gateway():
420    """
421    Set DNS source to DHCP on Windows
422
423    CLI Example:
424
425    .. code-block:: bash
426
427        salt -G 'os_family:Windows' ip.get_default_gateway
428    """
429    try:
430        return next(
431            iter(
432                x.split()[-1]
433                for x in __salt__["cmd.run"](
434                    ["netsh", "interface", "ip", "show", "config"], python_shell=False
435                ).splitlines()
436                if "Default Gateway:" in x
437            )
438        )
439    except StopIteration:
440        raise CommandExecutionError("Unable to find default gateway")
441