1"""
2Module for gathering and managing network information
3"""
4
5import datetime
6import hashlib
7import logging
8import os
9import re
10import socket
11import time
12from multiprocessing.pool import ThreadPool
13
14import salt.utils.decorators.path
15import salt.utils.functools
16import salt.utils.network
17import salt.utils.platform
18import salt.utils.validate.net
19from salt._compat import ipaddress
20from salt.exceptions import CommandExecutionError
21
22log = logging.getLogger(__name__)
23
24
25def __virtual__():
26    """
27    Only work on POSIX-like systems
28    """
29    # Disable on Windows, a specific file module exists:
30    if salt.utils.platform.is_windows():
31        return (
32            False,
33            "The network execution module cannot be loaded on Windows: use win_network"
34            " instead.",
35        )
36    return True
37
38
39def wol(mac, bcast="255.255.255.255", destport=9):
40    """
41    Send Wake On Lan packet to a host
42
43    CLI Example:
44
45    .. code-block:: bash
46
47        salt '*' network.wol 08-00-27-13-69-77
48        salt '*' network.wol 080027136977 255.255.255.255 7
49        salt '*' network.wol 08:00:27:13:69:77 255.255.255.255 7
50    """
51    dest = __utils__["network.mac_str_to_bytes"](mac)
52    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
53    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
54    sock.sendto(b"\xff" * 6 + dest * 16, (bcast, int(destport)))
55    return True
56
57
58def ping(host, timeout=False, return_boolean=False):
59    """
60    Performs an ICMP ping to a host
61
62    .. versionchanged:: 2015.8.0
63        Added support for SunOS
64
65    CLI Example:
66
67    .. code-block:: bash
68
69        salt '*' network.ping archlinux.org
70
71    .. versionadded:: 2015.5.0
72
73    Return a True or False instead of ping output.
74
75    .. code-block:: bash
76
77        salt '*' network.ping archlinux.org return_boolean=True
78
79    Set the time to wait for a response in seconds.
80
81    .. code-block:: bash
82
83        salt '*' network.ping archlinux.org timeout=3
84    """
85    if timeout:
86        if __grains__["kernel"] == "SunOS":
87            cmd = "ping -c 4 {} {}".format(
88                __utils__["network.sanitize_host"](host), timeout
89            )
90        else:
91            cmd = "ping -W {} -c 4 {}".format(
92                timeout, __utils__["network.sanitize_host"](host)
93            )
94    else:
95        cmd = "ping -c 4 {}".format(__utils__["network.sanitize_host"](host))
96    if return_boolean:
97        ret = __salt__["cmd.run_all"](cmd)
98        if ret["retcode"] != 0:
99            return False
100        else:
101            return True
102    else:
103        return __salt__["cmd.run"](cmd)
104
105
106# FIXME: Does not work with: netstat 1.42 (2001-04-15) from net-tools
107# 1.6.0 (Ubuntu 10.10)
108def _netstat_linux():
109    """
110    Return netstat information for Linux distros
111    """
112    ret = []
113    cmd = "netstat -tulpnea"
114    out = __salt__["cmd.run"](cmd)
115    for line in out.splitlines():
116        comps = line.split()
117        if line.startswith("tcp"):
118            ret.append(
119                {
120                    "proto": comps[0],
121                    "recv-q": comps[1],
122                    "send-q": comps[2],
123                    "local-address": comps[3],
124                    "remote-address": comps[4],
125                    "state": comps[5],
126                    "user": comps[6],
127                    "inode": comps[7],
128                    "program": comps[8],
129                }
130            )
131        if line.startswith("udp"):
132            ret.append(
133                {
134                    "proto": comps[0],
135                    "recv-q": comps[1],
136                    "send-q": comps[2],
137                    "local-address": comps[3],
138                    "remote-address": comps[4],
139                    "user": comps[5],
140                    "inode": comps[6],
141                    "program": comps[7],
142                }
143            )
144    return ret
145
146
147def _ss_linux():
148    """
149    Return ss information for Linux distros
150    (netstat is deprecated and may not be available)
151    """
152    ret = []
153    cmd = "ss -tulpnea"
154    out = __salt__["cmd.run"](cmd)
155    for line in out.splitlines():
156        comps = line.split()
157        ss_user = 0
158        ss_inode = 0
159        ss_program = ""
160        length = len(comps)
161        if line.startswith("tcp") or line.startswith("udp"):
162            i = 6
163            while i < (length - 1):
164                fields = comps[i].split(":")
165                if fields[0] == "users":
166                    users = fields[1].split(",")
167                    ss_program = users[0].split('"')[1]
168
169                if fields[0] == "uid":
170                    ss_user = fields[1]
171
172                if fields[0] == "ino":
173                    ss_inode = fields[1]
174
175                i += 1
176
177        if line.startswith("tcp"):
178            ss_state = comps[1]
179            if ss_state == "ESTAB":
180                ss_state = "ESTABLISHED"
181            ret.append(
182                {
183                    "proto": comps[0],
184                    "recv-q": comps[2],
185                    "send-q": comps[3],
186                    "local-address": comps[4],
187                    "remote-address": comps[5],
188                    "state": ss_state,
189                    "user": ss_user,
190                    "inode": ss_inode,
191                    "program": ss_program,
192                }
193            )
194        if line.startswith("udp"):
195            ret.append(
196                {
197                    "proto": comps[0],
198                    "recv-q": comps[2],
199                    "send-q": comps[3],
200                    "local-address": comps[4],
201                    "remote-address": comps[5],
202                    "user": ss_user,
203                    "inode": ss_inode,
204                    "program": ss_program,
205                }
206            )
207    return ret
208
209
210def _netinfo_openbsd():
211    """
212    Get process information for network connections using fstat
213    """
214    ret = {}
215    _fstat_re = re.compile(
216        r"internet(6)? (?:stream tcp 0x\S+ (\S+)|dgram udp (\S+))"
217        r"(?: [<>=-]+ (\S+))?$"
218    )
219    out = __salt__["cmd.run"]("fstat")
220    for line in out.splitlines():
221        try:
222            user, cmd, pid, _, details = line.split(None, 4)
223            ipv6, tcp, udp, remote_addr = _fstat_re.match(details).groups()
224        except (ValueError, AttributeError):
225            # Line either doesn't have the right number of columns, or the
226            # regex which looks for address information did not match. Either
227            # way, ignore this line and continue on to the next one.
228            continue
229        if tcp:
230            local_addr = tcp
231            proto = "tcp{}".format("" if ipv6 is None else ipv6)
232        else:
233            local_addr = udp
234            proto = "udp{}".format("" if ipv6 is None else ipv6)
235        if ipv6:
236            # IPv6 addresses have the address part enclosed in brackets (if the
237            # address part is not a wildcard) to distinguish the address from
238            # the port number. Remove them.
239            local_addr = "".join(x for x in local_addr if x not in "[]")
240
241        # Normalize to match netstat output
242        local_addr = ".".join(local_addr.rsplit(":", 1))
243        if remote_addr is None:
244            remote_addr = "*.*"
245        else:
246            remote_addr = ".".join(remote_addr.rsplit(":", 1))
247
248        ret.setdefault(local_addr, {}).setdefault(remote_addr, {}).setdefault(
249            proto, {}
250        ).setdefault(pid, {})["user"] = user
251        ret[local_addr][remote_addr][proto][pid]["cmd"] = cmd
252    return ret
253
254
255def _netinfo_freebsd_netbsd():
256    """
257    Get process information for network connections using sockstat
258    """
259    ret = {}
260    # NetBSD requires '-n' to disable port-to-service resolution
261    out = __salt__["cmd.run"](
262        "sockstat -46 {} | tail -n+2".format(
263            "-n" if __grains__["kernel"] == "NetBSD" else ""
264        ),
265        python_shell=True,
266    )
267    for line in out.splitlines():
268        user, cmd, pid, _, proto, local_addr, remote_addr = line.split()
269        local_addr = ".".join(local_addr.rsplit(":", 1))
270        remote_addr = ".".join(remote_addr.rsplit(":", 1))
271        ret.setdefault(local_addr, {}).setdefault(remote_addr, {}).setdefault(
272            proto, {}
273        ).setdefault(pid, {})["user"] = user
274        ret[local_addr][remote_addr][proto][pid]["cmd"] = cmd
275    return ret
276
277
278def _ppid():
279    """
280    Return a dict of pid to ppid mappings
281    """
282    ret = {}
283    if __grains__["kernel"] == "SunOS":
284        cmd = "ps -a -o pid,ppid | tail +2"
285    else:
286        cmd = "ps -ax -o pid,ppid | tail -n+2"
287    out = __salt__["cmd.run"](cmd, python_shell=True)
288    for line in out.splitlines():
289        pid, ppid = line.split()
290        ret[pid] = ppid
291    return ret
292
293
294def _netstat_bsd():
295    """
296    Return netstat information for BSD flavors
297    """
298    ret = []
299    if __grains__["kernel"] == "NetBSD":
300        for addr_family in ("inet", "inet6"):
301            cmd = "netstat -f {} -an | tail -n+3".format(addr_family)
302            out = __salt__["cmd.run"](cmd, python_shell=True)
303            for line in out.splitlines():
304                comps = line.split()
305                entry = {
306                    "proto": comps[0],
307                    "recv-q": comps[1],
308                    "send-q": comps[2],
309                    "local-address": comps[3],
310                    "remote-address": comps[4],
311                }
312                if entry["proto"].startswith("tcp"):
313                    entry["state"] = comps[5]
314                ret.append(entry)
315    else:
316        # Lookup TCP connections
317        cmd = "netstat -p tcp -an | tail -n+3"
318        out = __salt__["cmd.run"](cmd, python_shell=True)
319        for line in out.splitlines():
320            comps = line.split()
321            ret.append(
322                {
323                    "proto": comps[0],
324                    "recv-q": comps[1],
325                    "send-q": comps[2],
326                    "local-address": comps[3],
327                    "remote-address": comps[4],
328                    "state": comps[5],
329                }
330            )
331        # Lookup UDP connections
332        cmd = "netstat -p udp -an | tail -n+3"
333        out = __salt__["cmd.run"](cmd, python_shell=True)
334        for line in out.splitlines():
335            comps = line.split()
336            ret.append(
337                {
338                    "proto": comps[0],
339                    "recv-q": comps[1],
340                    "send-q": comps[2],
341                    "local-address": comps[3],
342                    "remote-address": comps[4],
343                }
344            )
345
346    # Add in user and program info
347    ppid = _ppid()
348    if __grains__["kernel"] == "OpenBSD":
349        netinfo = _netinfo_openbsd()
350    elif __grains__["kernel"] in ("FreeBSD", "NetBSD"):
351        netinfo = _netinfo_freebsd_netbsd()
352    for idx, _ in enumerate(ret):
353        local = ret[idx]["local-address"]
354        remote = ret[idx]["remote-address"]
355        proto = ret[idx]["proto"]
356        try:
357            # Make a pointer to the info for this connection for easier
358            # reference below
359            ptr = netinfo[local][remote][proto]
360        except KeyError:
361            continue
362        # Get the pid-to-ppid mappings for this connection
363        conn_ppid = {x: y for x, y in ppid.items() if x in ptr}
364        try:
365            # Master pid for this connection will be the pid whose ppid isn't
366            # in the subset dict we created above
367            master_pid = next(iter(x for x, y in conn_ppid.items() if y not in ptr))
368        except StopIteration:
369            continue
370        ret[idx]["user"] = ptr[master_pid]["user"]
371        ret[idx]["program"] = "/".join((master_pid, ptr[master_pid]["cmd"]))
372    return ret
373
374
375def _netstat_sunos():
376    """
377    Return netstat information for SunOS flavors
378    """
379    log.warning("User and program not (yet) supported on SunOS")
380
381    ret = []
382    for addr_family in ("inet", "inet6"):
383        # Lookup TCP connections
384        cmd = "netstat -f {} -P tcp -an | tail +5".format(addr_family)
385        out = __salt__["cmd.run"](cmd, python_shell=True)
386        for line in out.splitlines():
387            comps = line.split()
388            ret.append(
389                {
390                    "proto": "tcp6" if addr_family == "inet6" else "tcp",
391                    "recv-q": comps[5],
392                    "send-q": comps[4],
393                    "local-address": comps[0],
394                    "remote-address": comps[1],
395                    "state": comps[6],
396                }
397            )
398        # Lookup UDP connections
399        cmd = "netstat -f {} -P udp -an | tail +5".format(addr_family)
400        out = __salt__["cmd.run"](cmd, python_shell=True)
401        for line in out.splitlines():
402            comps = line.split()
403            ret.append(
404                {
405                    "proto": "udp6" if addr_family == "inet6" else "udp",
406                    "local-address": comps[0],
407                    "remote-address": comps[1] if len(comps) > 2 else "",
408                }
409            )
410
411    return ret
412
413
414def _netstat_aix():
415    """
416    Return netstat information for SunOS flavors
417    """
418    ret = []
419    ## AIX 6.1 - 7.2, appears to ignore addr_family field contents
420    ## for addr_family in ('inet', 'inet6'):
421    for addr_family in ("inet",):
422        # Lookup connections
423        cmd = "netstat -n -a -f {} | tail -n +3".format(addr_family)
424        out = __salt__["cmd.run"](cmd, python_shell=True)
425        for line in out.splitlines():
426            comps = line.split()
427            if len(comps) < 5:
428                continue
429
430            proto_seen = None
431            tcp_flag = True
432            if "tcp" == comps[0] or "tcp4" == comps[0]:
433                proto_seen = "tcp"
434            elif "tcp6" == comps[0]:
435                proto_seen = "tcp6"
436            elif "udp" == comps[0] or "udp4" == comps[0]:
437                proto_seen = "udp"
438                tcp_flag = False
439            elif "udp6" == comps[0]:
440                proto_seen = "udp6"
441                tcp_flag = False
442
443            if tcp_flag:
444                if len(comps) >= 6:
445                    ret.append(
446                        {
447                            "proto": proto_seen,
448                            "recv-q": comps[1],
449                            "send-q": comps[2],
450                            "local-address": comps[3],
451                            "remote-address": comps[4],
452                            "state": comps[5],
453                        }
454                    )
455            else:
456                if len(comps) >= 5:
457                    ret.append(
458                        {
459                            "proto": proto_seen,
460                            "local-address": comps[3],
461                            "remote-address": comps[4],
462                        }
463                    )
464    return ret
465
466
467def _netstat_route_linux():
468    """
469    Return netstat routing information for Linux distros
470    """
471    ret = []
472    cmd = "netstat -A inet -rn | tail -n+3"
473    out = __salt__["cmd.run"](cmd, python_shell=True)
474    for line in out.splitlines():
475        comps = line.split()
476        ret.append(
477            {
478                "addr_family": "inet",
479                "destination": comps[0],
480                "gateway": comps[1],
481                "netmask": comps[2],
482                "flags": comps[3],
483                "interface": comps[7],
484            }
485        )
486    cmd = "netstat -A inet6 -rn | tail -n+3"
487    out = __salt__["cmd.run"](cmd, python_shell=True)
488    for line in out.splitlines():
489        comps = line.split()
490        if len(comps) == 6:
491            ret.append(
492                {
493                    "addr_family": "inet6",
494                    "destination": comps[0],
495                    "gateway": comps[1],
496                    "netmask": "",
497                    "flags": comps[2],
498                    "interface": comps[5],
499                }
500            )
501        elif len(comps) == 7:
502            ret.append(
503                {
504                    "addr_family": "inet6",
505                    "destination": comps[0],
506                    "gateway": comps[1],
507                    "netmask": "",
508                    "flags": comps[2],
509                    "interface": comps[6],
510                }
511            )
512        else:
513            continue
514    return ret
515
516
517def _ip_route_linux():
518    """
519    Return ip routing information for Linux distros
520    (netstat is deprecated and may not be available)
521    """
522    # table main closest to old netstat inet output
523    ret = []
524    cmd = "ip -4 route show table main"
525    out = __salt__["cmd.run"](cmd, python_shell=True)
526    for line in out.splitlines():
527        comps = line.split()
528
529        # need to fake similar output to that provided by netstat
530        # to maintain output format
531        if comps[0] == "unreachable":
532            continue
533
534        if comps[0] == "default":
535            ip_interface = ""
536            if comps[3] == "dev":
537                ip_interface = comps[4]
538
539            ret.append(
540                {
541                    "addr_family": "inet",
542                    "destination": "0.0.0.0",
543                    "gateway": comps[2],
544                    "netmask": "0.0.0.0",
545                    "flags": "UG",
546                    "interface": ip_interface,
547                }
548            )
549        else:
550            address_mask = convert_cidr(comps[0])
551            ip_interface = ""
552            if comps[1] == "dev":
553                ip_interface = comps[2]
554
555            ret.append(
556                {
557                    "addr_family": "inet",
558                    "destination": address_mask["network"],
559                    "gateway": "0.0.0.0",
560                    "netmask": address_mask["netmask"],
561                    "flags": "U",
562                    "interface": ip_interface,
563                }
564            )
565
566    # table all closest to old netstat inet6 output
567    cmd = "ip -6 route show table all"
568    out = __salt__["cmd.run"](cmd, python_shell=True)
569    for line in out.splitlines():
570        comps = line.split()
571
572        # need to fake similar output to that provided by netstat
573        # to maintain output format
574        if comps[0] in (
575            "unicast",
576            "broadcast",
577            "throw",
578            "unreachable",
579            "prohibit",
580            "blackhole",
581            "nat",
582            "anycast",
583            "multicast",
584        ):
585            continue
586
587        if comps[0] == "default":
588            ip_interface = ""
589            if comps[3] == "dev":
590                ip_interface = comps[4]
591
592            ret.append(
593                {
594                    "addr_family": "inet6",
595                    "destination": "::/0",
596                    "gateway": comps[2],
597                    "netmask": "",
598                    "flags": "UG",
599                    "interface": ip_interface,
600                }
601            )
602
603        elif comps[0] == "local":
604            ip_interface = ""
605            if comps[2] == "dev":
606                ip_interface = comps[3]
607
608            local_address = comps[1] + "/128"
609            ret.append(
610                {
611                    "addr_family": "inet6",
612                    "destination": local_address,
613                    "gateway": "::",
614                    "netmask": "",
615                    "flags": "U",
616                    "interface": ip_interface,
617                }
618            )
619        else:
620            address_mask = convert_cidr(comps[0])
621            ip_interface = ""
622            if comps[1] == "dev":
623                ip_interface = comps[2]
624
625            ret.append(
626                {
627                    "addr_family": "inet6",
628                    "destination": comps[0],
629                    "gateway": "::",
630                    "netmask": "",
631                    "flags": "U",
632                    "interface": ip_interface,
633                }
634            )
635    return ret
636
637
638def _netstat_route_freebsd():
639    """
640    Return netstat routing information for FreeBSD and macOS
641    """
642    ret = []
643    cmd = "netstat -f inet -rn | tail -n+5"
644    out = __salt__["cmd.run"](cmd, python_shell=True)
645    for line in out.splitlines():
646        comps = line.split()
647        if (
648            __grains__["os"] == "FreeBSD"
649            and int(__grains__.get("osmajorrelease", 0)) < 10
650        ):
651            ret.append(
652                {
653                    "addr_family": "inet",
654                    "destination": comps[0],
655                    "gateway": comps[1],
656                    "netmask": comps[2],
657                    "flags": comps[3],
658                    "interface": comps[5],
659                }
660            )
661        else:
662            ret.append(
663                {
664                    "addr_family": "inet",
665                    "destination": comps[0],
666                    "gateway": comps[1],
667                    "netmask": "",
668                    "flags": comps[2],
669                    "interface": comps[3],
670                }
671            )
672    cmd = "netstat -f inet6 -rn | tail -n+5"
673    out = __salt__["cmd.run"](cmd, python_shell=True)
674    for line in out.splitlines():
675        comps = line.split()
676        ret.append(
677            {
678                "addr_family": "inet6",
679                "destination": comps[0],
680                "gateway": comps[1],
681                "netmask": "",
682                "flags": comps[2],
683                "interface": comps[3],
684            }
685        )
686    return ret
687
688
689def _netstat_route_netbsd():
690    """
691    Return netstat routing information for NetBSD
692    """
693    ret = []
694    cmd = "netstat -f inet -rn | tail -n+5"
695    out = __salt__["cmd.run"](cmd, python_shell=True)
696    for line in out.splitlines():
697        comps = line.split()
698        ret.append(
699            {
700                "addr_family": "inet",
701                "destination": comps[0],
702                "gateway": comps[1],
703                "netmask": "",
704                "flags": comps[3],
705                "interface": comps[6],
706            }
707        )
708    cmd = "netstat -f inet6 -rn | tail -n+5"
709    out = __salt__["cmd.run"](cmd, python_shell=True)
710    for line in out.splitlines():
711        comps = line.split()
712        ret.append(
713            {
714                "addr_family": "inet6",
715                "destination": comps[0],
716                "gateway": comps[1],
717                "netmask": "",
718                "flags": comps[3],
719                "interface": comps[6],
720            }
721        )
722    return ret
723
724
725def _netstat_route_openbsd():
726    """
727    Return netstat routing information for OpenBSD
728    """
729    ret = []
730    cmd = "netstat -f inet -rn | tail -n+5"
731    out = __salt__["cmd.run"](cmd, python_shell=True)
732    for line in out.splitlines():
733        comps = line.split()
734        ret.append(
735            {
736                "addr_family": "inet",
737                "destination": comps[0],
738                "gateway": comps[1],
739                "netmask": "",
740                "flags": comps[2],
741                "interface": comps[7],
742            }
743        )
744    cmd = "netstat -f inet6 -rn | tail -n+5"
745    out = __salt__["cmd.run"](cmd, python_shell=True)
746    for line in out.splitlines():
747        comps = line.split()
748        ret.append(
749            {
750                "addr_family": "inet6",
751                "destination": comps[0],
752                "gateway": comps[1],
753                "netmask": "",
754                "flags": comps[2],
755                "interface": comps[7],
756            }
757        )
758    return ret
759
760
761def _netstat_route_sunos():
762    """
763    Return netstat routing information for SunOS
764    """
765    ret = []
766    cmd = "netstat -f inet -rn | tail +5"
767    out = __salt__["cmd.run"](cmd, python_shell=True)
768    for line in out.splitlines():
769        comps = line.split()
770        ret.append(
771            {
772                "addr_family": "inet",
773                "destination": comps[0],
774                "gateway": comps[1],
775                "netmask": "",
776                "flags": comps[2],
777                "interface": comps[5] if len(comps) >= 6 else "",
778            }
779        )
780    cmd = "netstat -f inet6 -rn | tail +5"
781    out = __salt__["cmd.run"](cmd, python_shell=True)
782    for line in out.splitlines():
783        comps = line.split()
784        ret.append(
785            {
786                "addr_family": "inet6",
787                "destination": comps[0],
788                "gateway": comps[1],
789                "netmask": "",
790                "flags": comps[2],
791                "interface": comps[5] if len(comps) >= 6 else "",
792            }
793        )
794    return ret
795
796
797def _netstat_route_aix():
798    """
799    Return netstat routing information for AIX
800    """
801    ret = []
802    cmd = "netstat -f inet -rn | tail -n +5"
803    out = __salt__["cmd.run"](cmd, python_shell=True)
804    for line in out.splitlines():
805        comps = line.split()
806        ret.append(
807            {
808                "addr_family": "inet",
809                "destination": comps[0],
810                "gateway": comps[1],
811                "netmask": "",
812                "flags": comps[2],
813                "interface": comps[5] if len(comps) >= 6 else "",
814            }
815        )
816    cmd = "netstat -f inet6 -rn | tail -n +5"
817    out = __salt__["cmd.run"](cmd, python_shell=True)
818    for line in out.splitlines():
819        comps = line.split()
820        ret.append(
821            {
822                "addr_family": "inet6",
823                "destination": comps[0],
824                "gateway": comps[1],
825                "netmask": "",
826                "flags": comps[2],
827                "interface": comps[5] if len(comps) >= 6 else "",
828            }
829        )
830    return ret
831
832
833def netstat():
834    """
835    Return information on open ports and states
836
837    .. note::
838        On BSD minions, the output contains PID info (where available) for each
839        netstat entry, fetched from sockstat/fstat output.
840
841    .. versionchanged:: 2014.1.4
842        Added support for OpenBSD, FreeBSD, and NetBSD
843
844    .. versionchanged:: 2015.8.0
845        Added support for SunOS
846
847    .. versionchanged:: 2016.11.4
848        Added support for AIX
849
850    CLI Example:
851
852    .. code-block:: bash
853
854        salt '*' network.netstat
855    """
856    if __grains__["kernel"] == "Linux":
857        if not __utils__["path.which"]("netstat"):
858            return _ss_linux()
859        else:
860            return _netstat_linux()
861    elif __grains__["kernel"] in ("OpenBSD", "FreeBSD", "NetBSD"):
862        return _netstat_bsd()
863    elif __grains__["kernel"] == "SunOS":
864        return _netstat_sunos()
865    elif __grains__["kernel"] == "AIX":
866        return _netstat_aix()
867    raise CommandExecutionError("Not yet supported on this platform")
868
869
870def active_tcp():
871    """
872    Return a dict containing information on all of the running TCP connections (currently linux and solaris only)
873
874    .. versionchanged:: 2015.8.4
875
876        Added support for SunOS
877
878    CLI Example:
879
880    .. code-block:: bash
881
882        salt '*' network.active_tcp
883    """
884    if __grains__["kernel"] == "Linux":
885        return __utils__["network.active_tcp"]()
886    elif __grains__["kernel"] == "SunOS":
887        # lets use netstat to mimic linux as close as possible
888        ret = {}
889        for connection in _netstat_sunos():
890            if not connection["proto"].startswith("tcp"):
891                continue
892            if connection["state"] != "ESTABLISHED":
893                continue
894            ret[len(ret) + 1] = {
895                "local_addr": ".".join(connection["local-address"].split(".")[:-1]),
896                "local_port": ".".join(connection["local-address"].split(".")[-1:]),
897                "remote_addr": ".".join(connection["remote-address"].split(".")[:-1]),
898                "remote_port": ".".join(connection["remote-address"].split(".")[-1:]),
899            }
900        return ret
901    elif __grains__["kernel"] == "AIX":
902        # lets use netstat to mimic linux as close as possible
903        ret = {}
904        for connection in _netstat_aix():
905            if not connection["proto"].startswith("tcp"):
906                continue
907            if connection["state"] != "ESTABLISHED":
908                continue
909            ret[len(ret) + 1] = {
910                "local_addr": ".".join(connection["local-address"].split(".")[:-1]),
911                "local_port": ".".join(connection["local-address"].split(".")[-1:]),
912                "remote_addr": ".".join(connection["remote-address"].split(".")[:-1]),
913                "remote_port": ".".join(connection["remote-address"].split(".")[-1:]),
914            }
915        return ret
916    else:
917        return {}
918
919
920@salt.utils.decorators.path.which("traceroute")
921def traceroute(host):
922    """
923    Performs a traceroute to a 3rd party host
924
925    .. versionchanged:: 2015.8.0
926        Added support for SunOS
927
928    .. versionchanged:: 2016.11.4
929        Added support for AIX
930
931    CLI Example:
932
933    .. code-block:: bash
934
935        salt '*' network.traceroute archlinux.org
936    """
937    ret = []
938    cmd = "traceroute {}".format(__utils__["network.sanitize_host"](host))
939    out = __salt__["cmd.run"](cmd)
940
941    # Parse version of traceroute
942    if __utils__["platform.is_sunos"]() or __utils__["platform.is_aix"]():
943        traceroute_version = [0, 0, 0]
944    else:
945        version_out = __salt__["cmd.run"]("traceroute --version")
946        try:
947            # Linux traceroute version looks like:
948            #   Modern traceroute for Linux, version 2.0.19, Dec 10 2012
949            # Darwin and FreeBSD traceroute version looks like: Version 1.4a12+[FreeBSD|Darwin]
950
951            version_raw = re.findall(
952                r".*[Vv]ersion (\d+)\.([\w\+]+)\.*(\w*)", version_out
953            )[0]
954            log.debug("traceroute_version_raw: %s", version_raw)
955            traceroute_version = []
956            for t in version_raw:
957                try:
958                    traceroute_version.append(int(t))
959                except ValueError:
960                    traceroute_version.append(t)
961
962            if len(traceroute_version) < 3:
963                traceroute_version.append(0)
964
965            log.debug("traceroute_version: %s", traceroute_version)
966
967        except IndexError:
968            traceroute_version = [0, 0, 0]
969
970    for line in out.splitlines():
971        # Pre requirements for line parsing
972        skip_line = False
973        if " " not in line:
974            skip_line = True
975        if line.startswith("traceroute"):
976            skip_line = True
977        if __utils__["platform.is_aix"]():
978            if line.startswith("trying to get source for"):
979                skip_line = True
980            if line.startswith("source should be"):
981                skip_line = True
982            if line.startswith("outgoing MTU"):
983                skip_line = True
984            if line.startswith("fragmentation required"):
985                skip_line = True
986        if skip_line:
987            log.debug("Skipping traceroute output line: %s", line)
988            continue
989
990        # Parse output from unix variants
991        if (
992            "Darwin" in str(traceroute_version[1])
993            or "FreeBSD" in str(traceroute_version[1])
994            or __grains__["kernel"] in ("SunOS", "AIX")
995        ):
996            try:
997                traceline = re.findall(r"\s*(\d*)\s+(.*)\s+\((.*)\)\s+(.*)$", line)[0]
998            except IndexError:
999                traceline = re.findall(r"\s*(\d*)\s+(\*\s+\*\s+\*)", line)[0]
1000
1001            log.debug("traceline: %s", traceline)
1002            delays = re.findall(r"(\d+\.\d+)\s*ms", str(traceline))
1003
1004            try:
1005                if traceline[1] == "* * *":
1006                    result = {"count": traceline[0], "hostname": "*"}
1007                else:
1008                    result = {
1009                        "count": traceline[0],
1010                        "hostname": traceline[1],
1011                        "ip": traceline[2],
1012                    }
1013                    for idx, delay in enumerate(delays):
1014                        result["ms{}".format(idx + 1)] = delay
1015            except IndexError:
1016                result = {}
1017
1018        # Parse output from specific version ranges
1019        elif (
1020            traceroute_version[0] >= 2
1021            and traceroute_version[2] >= 14
1022            or traceroute_version[0] >= 2
1023            and traceroute_version[1] > 0
1024        ):
1025            comps = line.split("  ")
1026            if len(comps) >= 2 and comps[1] == "* * *":
1027                result = {"count": int(comps[0]), "hostname": "*"}
1028            elif len(comps) >= 5:
1029                result = {
1030                    "count": int(comps[0]),
1031                    "hostname": comps[1].split()[0],
1032                    "ip": comps[1].split()[1].strip("()"),
1033                    "ms1": float(comps[2].split()[0]),
1034                    "ms2": float(comps[3].split()[0]),
1035                    "ms3": float(comps[4].split()[0]),
1036                }
1037            else:
1038                result = {}
1039
1040        # Parse anything else
1041        else:
1042            comps = line.split()
1043            if len(comps) >= 8:
1044                result = {
1045                    "count": comps[0],
1046                    "hostname": comps[1],
1047                    "ip": comps[2],
1048                    "ms1": comps[4],
1049                    "ms2": comps[6],
1050                    "ms3": comps[8],
1051                    "ping1": comps[3],
1052                    "ping2": comps[5],
1053                    "ping3": comps[7],
1054                }
1055            else:
1056                result = {}
1057
1058        ret.append(result)
1059        if not result:
1060            log.warning("Cannot parse traceroute output line: %s", line)
1061    return ret
1062
1063
1064@salt.utils.decorators.path.which("dig")
1065def dig(host):
1066    """
1067    Performs a DNS lookup with dig
1068
1069    CLI Example:
1070
1071    .. code-block:: bash
1072
1073        salt '*' network.dig archlinux.org
1074    """
1075    cmd = "dig {}".format(__utils__["network.sanitize_host"](host))
1076    return __salt__["cmd.run"](cmd)
1077
1078
1079@salt.utils.decorators.path.which("arp")
1080def arp():
1081    """
1082    Return the arp table from the minion
1083
1084    .. versionchanged:: 2015.8.0
1085        Added support for SunOS
1086
1087    CLI Example:
1088
1089    .. code-block:: bash
1090
1091        salt '*' network.arp
1092    """
1093    ret = {}
1094    out = __salt__["cmd.run"]("arp -an")
1095    for line in out.splitlines():
1096        comps = line.split()
1097        if len(comps) < 4:
1098            continue
1099        if __grains__["kernel"] == "SunOS":
1100            if ":" not in comps[-1]:
1101                continue
1102            ret[comps[-1]] = comps[1]
1103        elif __grains__["kernel"] == "OpenBSD":
1104            if comps[0] == "Host" or comps[1] == "(incomplete)":
1105                continue
1106            ret[comps[1]] = comps[0]
1107        elif __grains__["kernel"] == "AIX":
1108            if comps[0] in ("bucket", "There"):
1109                continue
1110            ret[comps[3]] = comps[1].strip("(").strip(")")
1111        else:
1112            ret[comps[3]] = comps[1].strip("(").strip(")")
1113
1114    return ret
1115
1116
1117def interfaces():
1118    """
1119    Return a dictionary of information about all the interfaces on the minion
1120
1121    CLI Example:
1122
1123    .. code-block:: bash
1124
1125        salt '*' network.interfaces
1126    """
1127    return __utils__["network.interfaces"]()
1128
1129
1130def hw_addr(iface):
1131    """
1132    Return the hardware address (a.k.a. MAC address) for a given interface
1133
1134    CLI Example:
1135
1136    .. code-block:: bash
1137
1138        salt '*' network.hw_addr eth0
1139    """
1140    return __utils__["network.hw_addr"](iface)
1141
1142
1143# Alias hwaddr to preserve backward compat
1144hwaddr = salt.utils.functools.alias_function(hw_addr, "hwaddr")
1145
1146
1147def interface(iface):
1148    """
1149    Return the inet address for a given interface
1150
1151    .. versionadded:: 2014.7.0
1152
1153    CLI Example:
1154
1155    .. code-block:: bash
1156
1157        salt '*' network.interface eth0
1158    """
1159    return __utils__["network.interface"](iface)
1160
1161
1162def interface_ip(iface):
1163    """
1164    Return the inet address for a given interface
1165
1166    .. versionadded:: 2014.7.0
1167
1168    CLI Example:
1169
1170    .. code-block:: bash
1171
1172        salt '*' network.interface_ip eth0
1173    """
1174    return __utils__["network.interface_ip"](iface)
1175
1176
1177def subnets(interfaces=None):
1178    """
1179    Returns a list of IPv4 subnets to which the host belongs
1180
1181    CLI Example:
1182
1183    .. code-block:: bash
1184
1185        salt '*' network.subnets
1186        salt '*' network.subnets interfaces=eth1
1187    """
1188    return __utils__["network.subnets"](interfaces)
1189
1190
1191def subnets6():
1192    """
1193    Returns a list of IPv6 subnets to which the host belongs
1194
1195    CLI Example:
1196
1197    .. code-block:: bash
1198
1199        salt '*' network.subnets
1200    """
1201    return __utils__["network.subnets6"]()
1202
1203
1204def in_subnet(cidr):
1205    """
1206    Returns True if host is within specified subnet, otherwise False.
1207
1208    CLI Example:
1209
1210    .. code-block:: bash
1211
1212        salt '*' network.in_subnet 10.0.0.0/16
1213    """
1214    return __utils__["network.in_subnet"](cidr)
1215
1216
1217def ip_in_subnet(ip_addr, cidr):
1218    """
1219    Returns True if given IP is within specified subnet, otherwise False.
1220
1221    CLI Example:
1222
1223    .. code-block:: bash
1224
1225        salt '*' network.ip_in_subnet 172.17.0.4 172.16.0.0/12
1226    """
1227    return __utils__["network.in_subnet"](cidr, ip_addr)
1228
1229
1230def convert_cidr(cidr):
1231    """
1232    returns the network address, subnet mask and broadcast address of a cidr address
1233
1234    .. versionadded:: 2016.3.0
1235
1236    CLI Example:
1237
1238    .. code-block:: bash
1239
1240        salt '*' network.convert_cidr 172.31.0.0/16
1241    """
1242    ret = {"network": None, "netmask": None, "broadcast": None}
1243    cidr = calc_net(cidr)
1244    network_info = ipaddress.ip_network(cidr)
1245    ret["network"] = str(network_info.network_address)
1246    ret["netmask"] = str(network_info.netmask)
1247    ret["broadcast"] = str(network_info.broadcast_address)
1248    return ret
1249
1250
1251def calc_net(ip_addr, netmask=None):
1252    """
1253    Returns the CIDR of a subnet based on
1254    an IP address (CIDR notation supported)
1255    and optional netmask.
1256
1257    CLI Example:
1258
1259    .. code-block:: bash
1260
1261        salt '*' network.calc_net 172.17.0.5 255.255.255.240
1262        salt '*' network.calc_net 2a02:f6e:a000:80:84d8:8332:7866:4e07/64
1263
1264    .. versionadded:: 2015.8.0
1265    """
1266    return __utils__["network.calc_net"](ip_addr, netmask)
1267
1268
1269def ip_addrs(interface=None, include_loopback=False, cidr=None, type=None):
1270    """
1271    Returns a list of IPv4 addresses assigned to the host. 127.0.0.1 is
1272    ignored, unless 'include_loopback=True' is indicated. If 'interface' is
1273    provided, then only IP addresses from that interface will be returned.
1274    Providing a CIDR via 'cidr="10.0.0.0/8"' will return only the addresses
1275    which are within that subnet. If 'type' is 'public', then only public
1276    addresses will be returned. Ditto for 'type'='private'.
1277
1278    .. versionchanged:: 3001
1279        ``interface`` can now be a single interface name or a list of
1280        interfaces. Globbing is also supported.
1281
1282    CLI Example:
1283
1284    .. code-block:: bash
1285
1286        salt '*' network.ip_addrs
1287    """
1288    addrs = __utils__["network.ip_addrs"](
1289        interface=interface, include_loopback=include_loopback
1290    )
1291    if cidr:
1292        return [i for i in addrs if __utils__["network.in_subnet"](cidr, [i])]
1293    else:
1294        if type == "public":
1295            return [i for i in addrs if not is_private(i)]
1296        elif type == "private":
1297            return [i for i in addrs if is_private(i)]
1298        else:
1299            return addrs
1300
1301
1302ipaddrs = salt.utils.functools.alias_function(ip_addrs, "ipaddrs")
1303
1304
1305def ip_addrs6(interface=None, include_loopback=False, cidr=None):
1306    """
1307    Returns a list of IPv6 addresses assigned to the host. ::1 is ignored,
1308    unless 'include_loopback=True' is indicated. If 'interface' is provided,
1309    then only IP addresses from that interface will be returned.
1310    Providing a CIDR via 'cidr="2000::/3"' will return only the addresses
1311    which are within that subnet.
1312
1313    .. versionchanged:: 3001
1314        ``interface`` can now be a single interface name or a list of
1315        interfaces. Globbing is also supported.
1316
1317    CLI Example:
1318
1319    .. code-block:: bash
1320
1321        salt '*' network.ip_addrs6
1322    """
1323    addrs = __utils__["network.ip_addrs6"](
1324        interface=interface, include_loopback=include_loopback
1325    )
1326    if cidr:
1327        return [i for i in addrs if __utils__["network.in_subnet"](cidr, [i])]
1328    else:
1329        return addrs
1330
1331
1332ipaddrs6 = salt.utils.functools.alias_function(ip_addrs6, "ipaddrs6")
1333
1334
1335def get_hostname():
1336    """
1337    Get hostname
1338
1339    CLI Example:
1340
1341    .. code-block:: bash
1342
1343        salt '*' network.get_hostname
1344    """
1345
1346    return socket.gethostname()
1347
1348
1349def get_fqdn():
1350    """
1351    Get fully qualified domain name
1352
1353    CLI Example:
1354
1355    .. code-block:: bash
1356
1357        salt '*' network.get_fqdn
1358    """
1359
1360    return socket.getfqdn()
1361
1362
1363def mod_hostname(hostname):
1364    """
1365    Modify hostname
1366
1367    .. versionchanged:: 2015.8.0
1368        Added support for SunOS (Solaris 10, Illumos, SmartOS)
1369
1370    CLI Example:
1371
1372    .. code-block:: bash
1373
1374        salt '*' network.mod_hostname master.saltstack.com
1375    """
1376    #
1377    # SunOS tested on SmartOS and OmniOS (Solaris 10 compatible)
1378    # Oracle Solaris 11 uses smf, currently not supported
1379    #
1380    # /etc/nodename is the hostname only, not fqdn
1381    # /etc/defaultdomain is the domain
1382    # /etc/hosts should have both fqdn and hostname entries
1383    #
1384
1385    if hostname is None:
1386        return False
1387
1388    hostname_cmd = __utils__["path.which"]("hostnamectl") or __utils__["path.which"](
1389        "hostname"
1390    )
1391    if __utils__["platform.is_sunos"]():
1392        uname_cmd = (
1393            "/usr/bin/uname"
1394            if __utils__["platform.is_smartos"]()
1395            else __utils__["path.which"]("uname")
1396        )
1397        check_hostname_cmd = __utils__["path.which"]("check-hostname")
1398
1399    # Grab the old hostname so we know which hostname to change and then
1400    # change the hostname using the hostname command
1401    if hostname_cmd.endswith("hostnamectl"):
1402        result = __salt__["cmd.run_all"]("{} status".format(hostname_cmd))
1403        if 0 == result["retcode"]:
1404            out = result["stdout"]
1405            for line in out.splitlines():
1406                line = line.split(":")
1407                if "Static hostname" in line[0]:
1408                    o_hostname = line[1].strip()
1409        else:
1410            log.debug("%s was unable to get hostname", hostname_cmd)
1411            o_hostname = __salt__["network.get_hostname"]()
1412    elif not __utils__["platform.is_sunos"]():
1413        # don't run hostname -f because -f is not supported on all platforms
1414        o_hostname = socket.getfqdn()
1415    else:
1416        # output: Hostname core OK: fully qualified as core.acheron.be
1417        o_hostname = __salt__["cmd.run"](check_hostname_cmd).split(" ")[-1]
1418
1419    if hostname_cmd.endswith("hostnamectl"):
1420        result = __salt__["cmd.run_all"](
1421            "{} set-hostname {}".format(
1422                hostname_cmd,
1423                hostname,
1424            )
1425        )
1426        if result["retcode"] != 0:
1427            log.debug(
1428                "%s was unable to set hostname. Error: %s",
1429                hostname_cmd,
1430                result["stderr"],
1431            )
1432            return False
1433    elif not __utils__["platform.is_sunos"]():
1434        __salt__["cmd.run"]("{} {}".format(hostname_cmd, hostname))
1435    else:
1436        __salt__["cmd.run"]("{} -S {}".format(uname_cmd, hostname.split(".")[0]))
1437
1438    # Modify the /etc/hosts file to replace the old hostname with the
1439    # new hostname
1440    with __utils__["files.fopen"]("/etc/hosts", "r") as fp_:
1441        host_c = [__utils__["stringutils.to_unicode"](_l) for _l in fp_.readlines()]
1442
1443    with __utils__["files.fopen"]("/etc/hosts", "w") as fh_:
1444        for host in host_c:
1445            host = host.split()
1446
1447            try:
1448                host[host.index(o_hostname)] = hostname
1449                if __utils__["platform.is_sunos"]():
1450                    # also set a copy of the hostname
1451                    host[host.index(o_hostname.split(".")[0])] = hostname.split(".")[0]
1452            except ValueError:
1453                pass
1454
1455            fh_.write(__utils__["stringutils.to_str"]("\t".join(host) + "\n"))
1456
1457    # Modify the /etc/sysconfig/network configuration file to set the
1458    # new hostname
1459    if __grains__["os_family"] == "RedHat":
1460        with __utils__["files.fopen"]("/etc/sysconfig/network", "r") as fp_:
1461            network_c = [
1462                __utils__["stringutils.to_unicode"](_l) for _l in fp_.readlines()
1463            ]
1464
1465        with __utils__["files.fopen"]("/etc/sysconfig/network", "w") as fh_:
1466            for net in network_c:
1467                if net.startswith("HOSTNAME"):
1468                    old_hostname = net.split("=", 1)[1].rstrip()
1469                    quote_type = __utils__["stringutils.is_quoted"](old_hostname)
1470                    fh_.write(
1471                        __utils__["stringutils.to_str"](
1472                            "HOSTNAME={1}{0}{1}\n".format(
1473                                __utils__["stringutils.dequote"](hostname), quote_type
1474                            )
1475                        )
1476                    )
1477                else:
1478                    fh_.write(__utils__["stringutils.to_str"](net))
1479    elif __grains__["os_family"] in ("Debian", "NILinuxRT"):
1480        with __utils__["files.fopen"]("/etc/hostname", "w") as fh_:
1481            fh_.write(__utils__["stringutils.to_str"](hostname + "\n"))
1482        if __grains__["lsb_distrib_id"] == "nilrt":
1483            str_hostname = __utils__["stringutils.to_str"](hostname)
1484            nirtcfg_cmd = "/usr/local/natinst/bin/nirtcfg"
1485            nirtcfg_cmd += (
1486                " --set section=SystemSettings,token='Host_Name',value='{}'".format(
1487                    str_hostname
1488                )
1489            )
1490            if __salt__["cmd.run_all"](nirtcfg_cmd)["retcode"] != 0:
1491                raise CommandExecutionError(
1492                    "Couldn't set hostname to: {}\n".format(str_hostname)
1493                )
1494    elif __grains__["os_family"] == "OpenBSD":
1495        with __utils__["files.fopen"]("/etc/myname", "w") as fh_:
1496            fh_.write(__utils__["stringutils.to_str"](hostname + "\n"))
1497
1498    # Update /etc/nodename and /etc/defaultdomain on SunOS
1499    if __utils__["platform.is_sunos"]():
1500        with __utils__["files.fopen"]("/etc/nodename", "w") as fh_:
1501            fh_.write(__utils__["stringutils.to_str"](hostname.split(".")[0] + "\n"))
1502        with __utils__["files.fopen"]("/etc/defaultdomain", "w") as fh_:
1503            fh_.write(
1504                __utils__["stringutils.to_str"](
1505                    ".".join(hostname.split(".")[1:]) + "\n"
1506                )
1507            )
1508
1509    return True
1510
1511
1512def connect(host, port=None, **kwargs):
1513    """
1514    Test connectivity to a host using a particular
1515    port from the minion.
1516
1517    .. versionadded:: 2014.7.0
1518
1519    CLI Example:
1520
1521    .. code-block:: bash
1522
1523        salt '*' network.connect archlinux.org 80
1524
1525        salt '*' network.connect archlinux.org 80 timeout=3
1526
1527        salt '*' network.connect archlinux.org 80 timeout=3 family=ipv4
1528
1529        salt '*' network.connect google-public-dns-a.google.com port=53 proto=udp timeout=3
1530    """
1531
1532    ret = {"result": None, "comment": ""}
1533
1534    if not host:
1535        ret["result"] = False
1536        ret["comment"] = "Required argument, host, is missing."
1537        return ret
1538
1539    if not port:
1540        ret["result"] = False
1541        ret["comment"] = "Required argument, port, is missing."
1542        return ret
1543
1544    proto = kwargs.get("proto", "tcp")
1545    timeout = kwargs.get("timeout", 5)
1546    family = kwargs.get("family", None)
1547
1548    if salt.utils.validate.net.ipv4_addr(host) or salt.utils.validate.net.ipv6_addr(
1549        host
1550    ):
1551        address = host
1552    else:
1553        address = "{}".format(__utils__["network.sanitize_host"](host))
1554
1555    try:
1556        if proto == "udp":
1557            __proto = socket.SOL_UDP
1558        else:
1559            __proto = socket.SOL_TCP
1560            proto = "tcp"
1561
1562        if family:
1563            if family == "ipv4":
1564                __family = socket.AF_INET
1565            elif family == "ipv6":
1566                __family = socket.AF_INET6
1567            else:
1568                __family = 0
1569        else:
1570            __family = 0
1571
1572        (family, socktype, _proto, garbage, _address) = socket.getaddrinfo(
1573            address, port, __family, 0, __proto
1574        )[0]
1575    except socket.gaierror:
1576        ret["result"] = False
1577        ret["comment"] = "Unable to resolve host {} on {} port {}".format(
1578            host, proto, port
1579        )
1580        return ret
1581
1582    try:
1583        skt = socket.socket(family, socktype, _proto)
1584        skt.settimeout(timeout)
1585
1586        if proto == "udp":
1587            # Generate a random string of a
1588            # decent size to test UDP connection
1589            md5h = hashlib.md5()
1590            md5h.update(datetime.datetime.now().strftime("%s"))
1591            msg = md5h.hexdigest()
1592            skt.sendto(msg, _address)
1593            recv, svr = skt.recvfrom(255)
1594            skt.close()
1595        else:
1596            skt.connect(_address)
1597            skt.shutdown(2)
1598    except Exception as exc:  # pylint: disable=broad-except
1599        ret["result"] = False
1600        ret["comment"] = "Unable to connect to {} ({}) on {} port {}".format(
1601            host, _address[0], proto, port
1602        )
1603        return ret
1604
1605    ret["result"] = True
1606    ret["comment"] = "Successfully connected to {} ({}) on {} port {}".format(
1607        host, _address[0], proto, port
1608    )
1609    return ret
1610
1611
1612def is_private(ip_addr):
1613    """
1614    Check if the given IP address is a private address
1615
1616    .. versionadded:: 2014.7.0
1617    .. versionchanged:: 2015.8.0
1618        IPv6 support
1619
1620    CLI Example:
1621
1622    .. code-block:: bash
1623
1624        salt '*' network.is_private 10.0.0.3
1625    """
1626    return ipaddress.ip_address(ip_addr).is_private
1627
1628
1629def is_loopback(ip_addr):
1630    """
1631    Check if the given IP address is a loopback address
1632
1633    .. versionadded:: 2014.7.0
1634    .. versionchanged:: 2015.8.0
1635        IPv6 support
1636
1637    CLI Example:
1638
1639    .. code-block:: bash
1640
1641        salt '*' network.is_loopback 127.0.0.1
1642    """
1643    return ipaddress.ip_address(ip_addr).is_loopback
1644
1645
1646def reverse_ip(ip_addr):
1647    """
1648    Returns the reversed IP address
1649
1650    .. versionchanged:: 2015.8.0
1651        IPv6 support
1652
1653    CLI Example:
1654
1655    .. code-block:: bash
1656
1657        salt '*' network.reverse_ip 172.17.0.4
1658    """
1659    return ipaddress.ip_address(ip_addr).reverse_pointer
1660
1661
1662def _get_bufsize_linux(iface):
1663    """
1664    Return network interface buffer information using ethtool
1665    """
1666    ret = {"result": False}
1667
1668    cmd = "/sbin/ethtool -g {}".format(iface)
1669    out = __salt__["cmd.run"](cmd)
1670    pat = re.compile(r"^(.+):\s+(\d+)$")
1671    suffix = "max-"
1672    for line in out.splitlines():
1673        res = pat.match(line)
1674        if res:
1675            ret[res.group(1).lower().replace(" ", "-") + suffix] = int(res.group(2))
1676            ret["result"] = True
1677        elif line.endswith("maximums:"):
1678            suffix = "-max"
1679        elif line.endswith("settings:"):
1680            suffix = ""
1681    if not ret["result"]:
1682        parts = out.split()
1683        # remove shell cmd prefix from msg
1684        if parts[0].endswith("sh:"):
1685            out = " ".join(parts[1:])
1686        ret["comment"] = out
1687    return ret
1688
1689
1690def get_bufsize(iface):
1691    """
1692    Return network buffer sizes as a dict (currently linux only)
1693
1694    CLI Example:
1695
1696    .. code-block:: bash
1697
1698        salt '*' network.get_bufsize eth0
1699    """
1700    if __grains__["kernel"] == "Linux":
1701        if os.path.exists("/sbin/ethtool"):
1702            return _get_bufsize_linux(iface)
1703
1704    return {}
1705
1706
1707def _mod_bufsize_linux(iface, *args, **kwargs):
1708    """
1709    Modify network interface buffer sizes using ethtool
1710    """
1711    ret = {
1712        "result": False,
1713        "comment": "Requires rx=<val> tx==<val> rx-mini=<val> and/or rx-jumbo=<val>",
1714    }
1715    cmd = "/sbin/ethtool -G " + iface
1716    if not kwargs:
1717        return ret
1718    if args:
1719        ret["comment"] = "Unknown arguments: " + " ".join([str(item) for item in args])
1720        return ret
1721    eargs = ""
1722    for kw in ["rx", "tx", "rx-mini", "rx-jumbo"]:
1723        value = kwargs.get(kw)
1724        if value is not None:
1725            eargs += " " + kw + " " + str(value)
1726    if not eargs:
1727        return ret
1728    cmd += eargs
1729    out = __salt__["cmd.run"](cmd)
1730    if out:
1731        ret["comment"] = out
1732    else:
1733        ret["comment"] = eargs.strip()
1734        ret["result"] = True
1735    return ret
1736
1737
1738def mod_bufsize(iface, *args, **kwargs):
1739    """
1740    Modify network interface buffers (currently linux only)
1741
1742    CLI Example:
1743
1744    .. code-block:: bash
1745
1746        salt '*' network.mod_bufsize tx=<val> rx=<val> rx-mini=<val> rx-jumbo=<val>
1747    """
1748    if __grains__["kernel"] == "Linux":
1749        if os.path.exists("/sbin/ethtool"):
1750            return _mod_bufsize_linux(iface, *args, **kwargs)
1751
1752    return False
1753
1754
1755def routes(family=None):
1756    """
1757    Return currently configured routes from routing table
1758
1759    .. versionchanged:: 2015.8.0
1760        Added support for SunOS (Solaris 10, Illumos, SmartOS)
1761
1762    .. versionchanged:: 2016.11.4
1763        Added support for AIX
1764
1765    CLI Example:
1766
1767    .. code-block:: bash
1768
1769        salt '*' network.routes
1770    """
1771    if family != "inet" and family != "inet6" and family is not None:
1772        raise CommandExecutionError("Invalid address family {}".format(family))
1773
1774    if __grains__["kernel"] == "Linux":
1775        if not __utils__["path.which"]("netstat"):
1776            routes_ = _ip_route_linux()
1777        else:
1778            routes_ = _netstat_route_linux()
1779    elif __grains__["kernel"] == "SunOS":
1780        routes_ = _netstat_route_sunos()
1781    elif __grains__["os"] in ["FreeBSD", "MacOS", "Darwin"]:
1782        routes_ = _netstat_route_freebsd()
1783    elif __grains__["os"] in ["NetBSD"]:
1784        routes_ = _netstat_route_netbsd()
1785    elif __grains__["os"] in ["OpenBSD"]:
1786        routes_ = _netstat_route_openbsd()
1787    elif __grains__["os"] in ["AIX"]:
1788        routes_ = _netstat_route_aix()
1789    else:
1790        raise CommandExecutionError("Not yet supported on this platform")
1791
1792    if not family:
1793        return routes_
1794    else:
1795        ret = [route for route in routes_ if route["addr_family"] == family]
1796        return ret
1797
1798
1799def default_route(family=None):
1800    """
1801    Return default route(s) from routing table
1802
1803    .. versionchanged:: 2015.8.0
1804        Added support for SunOS (Solaris 10, Illumos, SmartOS)
1805
1806    .. versionchanged:: 2016.11.4
1807        Added support for AIX
1808
1809    CLI Example:
1810
1811    .. code-block:: bash
1812
1813        salt '*' network.default_route
1814    """
1815    if family != "inet" and family != "inet6" and family is not None:
1816        raise CommandExecutionError("Invalid address family {}".format(family))
1817
1818    _routes = routes(family)
1819
1820    default_route = {}
1821    if __grains__["kernel"] == "Linux":
1822        default_route["inet"] = ["0.0.0.0", "default"]
1823        default_route["inet6"] = ["::/0", "default"]
1824    elif (
1825        __grains__["os"]
1826        in [
1827            "FreeBSD",
1828            "NetBSD",
1829            "OpenBSD",
1830            "MacOS",
1831            "Darwin",
1832        ]
1833        or __grains__["kernel"] in ("SunOS", "AIX")
1834    ):
1835        default_route["inet"] = ["default"]
1836        default_route["inet6"] = ["default"]
1837    else:
1838        raise CommandExecutionError("Not yet supported on this platform")
1839
1840    ret = []
1841    for route in _routes:
1842        if family:
1843            if route["destination"] in default_route[family]:
1844                if __grains__["kernel"] == "SunOS" and route["addr_family"] != family:
1845                    continue
1846                ret.append(route)
1847        else:
1848            if (
1849                route["destination"] in default_route["inet"]
1850                or route["destination"] in default_route["inet6"]
1851            ):
1852                ret.append(route)
1853
1854    return ret
1855
1856
1857def get_route(ip):
1858    """
1859    Return routing information for given destination ip
1860
1861    .. versionadded:: 2015.5.3
1862
1863    .. versionchanged:: 2015.8.0
1864        Added support for SunOS (Solaris 10, Illumos, SmartOS)
1865        Added support for OpenBSD
1866
1867    .. versionchanged:: 2016.11.4
1868        Added support for AIX
1869
1870    CLI Example:
1871
1872    .. code-block:: bash
1873
1874        salt '*' network.get_route 10.10.10.10
1875    """
1876
1877    if __grains__["kernel"] == "Linux":
1878        cmd = "ip route get {}".format(ip)
1879        out = __salt__["cmd.run"](cmd, python_shell=True)
1880        regexp = re.compile(
1881            r"(via\s+(?P<gateway>[\w\.:]+))?\s+dev\s+(?P<interface>[\w\.\:\-]+)\s+.*src\s+(?P<source>[\w\.:]+)"
1882        )
1883        m = regexp.search(out.splitlines()[0])
1884        ret = {
1885            "destination": ip,
1886            "gateway": m.group("gateway"),
1887            "interface": m.group("interface"),
1888            "source": m.group("source"),
1889        }
1890
1891        return ret
1892
1893    if __grains__["kernel"] == "SunOS":
1894        # [root@nacl ~]# route -n get 172.16.10.123
1895        #   route to: 172.16.10.123
1896        # destination: 172.16.10.0
1897        #       mask: 255.255.255.0
1898        #  interface: net0
1899        #      flags: <UP,DONE,KERNEL>
1900        # recvpipe  sendpipe  ssthresh    rtt,ms rttvar,ms  hopcount      mtu     expire
1901        #       0         0         0         0         0         0      1500         0
1902        cmd = "/usr/sbin/route -n get {}".format(ip)
1903        out = __salt__["cmd.run"](cmd, python_shell=False)
1904
1905        ret = {"destination": ip, "gateway": None, "interface": None, "source": None}
1906
1907        for line in out.splitlines():
1908            line = line.split(":")
1909            if "route to" in line[0]:
1910                ret["destination"] = line[1].strip()
1911            if "gateway" in line[0]:
1912                ret["gateway"] = line[1].strip()
1913            if "interface" in line[0]:
1914                ret["interface"] = line[1].strip()
1915                ret["source"] = __utils__["network.interface_ip"](line[1].strip())
1916
1917        return ret
1918
1919    if __grains__["kernel"] == "OpenBSD":
1920        # [root@exosphere] route -n get blackdot.be
1921        #   route to: 5.135.127.100
1922        # destination: default
1923        #       mask: default
1924        #    gateway: 192.168.0.1
1925        #  interface: vio0
1926        # if address: 192.168.0.2
1927        #   priority: 8 (static)
1928        #      flags: <UP,GATEWAY,DONE,STATIC>
1929        #     use       mtu    expire
1930        # 8352657         0         0
1931        cmd = "route -n get {}".format(ip)
1932        out = __salt__["cmd.run"](cmd, python_shell=False)
1933
1934        ret = {"destination": ip, "gateway": None, "interface": None, "source": None}
1935
1936        for line in out.splitlines():
1937            line = line.split(":")
1938            if "route to" in line[0]:
1939                ret["destination"] = line[1].strip()
1940            if "gateway" in line[0]:
1941                ret["gateway"] = line[1].strip()
1942            if "interface" in line[0]:
1943                ret["interface"] = line[1].strip()
1944            if "if address" in line[0]:
1945                ret["source"] = line[1].strip()
1946
1947        return ret
1948
1949    if __grains__["kernel"] == "AIX":
1950        # root@la68pp002_pub:~# route -n get 172.29.149.95
1951        #   route to: 172.29.149.95
1952        # destination: 172.29.149.95
1953        #    gateway: 127.0.0.1
1954        #  interface: lo0
1955        # interf addr: 127.0.0.1
1956        #     flags: <UP,GATEWAY,HOST,DONE,STATIC>
1957        # recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
1958        #      0         0         0         0         0         0         0    -68642
1959        cmd = "route -n get {}".format(ip)
1960        out = __salt__["cmd.run"](cmd, python_shell=False)
1961
1962        ret = {"destination": ip, "gateway": None, "interface": None, "source": None}
1963
1964        for line in out.splitlines():
1965            line = line.split(":")
1966            if "route to" in line[0]:
1967                ret["destination"] = line[1].strip()
1968            if "gateway" in line[0]:
1969                ret["gateway"] = line[1].strip()
1970            if "interface" in line[0]:
1971                ret["interface"] = line[1].strip()
1972            if "interf addr" in line[0]:
1973                ret["source"] = line[1].strip()
1974
1975        return ret
1976
1977    else:
1978        raise CommandExecutionError("Not yet supported on this platform")
1979
1980
1981def ifacestartswith(cidr):
1982    """
1983    Retrieve the interface name from a specific CIDR
1984
1985    .. versionadded:: 2016.11.0
1986
1987    CLI Example:
1988
1989    .. code-block:: bash
1990
1991        salt '*' network.ifacestartswith 10.0
1992    """
1993    net_list = interfaces()
1994    intfnames = []
1995    pattern = str(cidr)
1996    size = len(pattern)
1997    for ifname, ifval in net_list.items():
1998        if "inet" in ifval:
1999            for inet in ifval["inet"]:
2000                if inet["address"][0:size] == pattern:
2001                    if "label" in inet:
2002                        intfnames.append(inet["label"])
2003                    else:
2004                        intfnames.append(ifname)
2005    return intfnames
2006
2007
2008def iphexval(ip):
2009    """
2010    Retrieve the hexadecimal representation of an IP address
2011
2012    .. versionadded:: 2016.11.0
2013
2014    CLI Example:
2015
2016    .. code-block:: bash
2017
2018        salt '*' network.iphexval 10.0.0.1
2019    """
2020    a = ip.split(".")
2021    hexval = ["%02X" % int(x) for x in a]  # pylint: disable=E1321
2022    return "".join(hexval)
2023
2024
2025def ip_networks(interface=None, include_loopback=False, verbose=False):
2026    """
2027    .. versionadded:: 3001
2028
2029    Returns a list of IPv4 networks to which the minion belongs.
2030
2031    interface
2032        Restrict results to the specified interface(s). This value can be
2033        either a single interface name or a list of interfaces. Globbing is
2034        also supported.
2035
2036    CLI Example:
2037
2038    .. code-block:: bash
2039
2040        salt '*' network.list_networks
2041        salt '*' network.list_networks interface=docker0
2042        salt '*' network.list_networks interface=docker0,enp*
2043        salt '*' network.list_networks interface=eth*
2044    """
2045    return __utils__["network.ip_networks"](
2046        interface=interface, include_loopback=include_loopback, verbose=verbose
2047    )
2048
2049
2050def ip_networks6(interface=None, include_loopback=False, verbose=False):
2051    """
2052    .. versionadded:: 3001
2053
2054    Returns a list of IPv6 networks to which the minion belongs.
2055
2056    interface
2057        Restrict results to the specified interface(s). This value can be
2058        either a single interface name or a list of interfaces. Globbing is
2059        also supported.
2060
2061    CLI Example:
2062
2063    .. code-block:: bash
2064
2065        salt '*' network.list_networks6
2066        salt '*' network.list_networks6 interface=docker0
2067        salt '*' network.list_networks6 interface=docker0,enp*
2068        salt '*' network.list_networks6 interface=eth*
2069    """
2070    return __utils__["network.ip_networks6"](
2071        interface=interface, include_loopback=include_loopback, verbose=verbose
2072    )
2073
2074
2075def fqdns():
2076    """
2077    Return all known FQDNs for the system by enumerating all interfaces and
2078    then trying to reverse resolve them (excluding 'lo' interface).
2079    """
2080    # Provides:
2081    # fqdns
2082
2083    # Possible value for h_errno defined in netdb.h
2084    HOST_NOT_FOUND = 1
2085    NO_DATA = 4
2086
2087    grains = {}
2088    fqdns = set()
2089
2090    def _lookup_fqdn(ip):
2091        try:
2092            return [socket.getfqdn(socket.gethostbyaddr(ip)[0])]
2093        except socket.herror as err:
2094            if err.errno in (0, HOST_NOT_FOUND, NO_DATA):
2095                # No FQDN for this IP address, so we don't need to know this all the time.
2096                log.debug("Unable to resolve address %s: %s", ip, err)
2097            else:
2098                log.error(err_message, err)
2099        except (OSError, socket.gaierror, socket.timeout) as err:
2100            log.error(err_message, err)
2101
2102    start = time.time()
2103
2104    addresses = salt.utils.network.ip_addrs(
2105        include_loopback=False, interface_data=salt.utils.network._get_interfaces()
2106    )
2107    addresses.extend(
2108        salt.utils.network.ip_addrs6(
2109            include_loopback=False, interface_data=salt.utils.network._get_interfaces()
2110        )
2111    )
2112    err_message = "Exception during resolving address: %s"
2113
2114    # Create a ThreadPool to process the underlying calls to 'socket.gethostbyaddr' in parallel.
2115    # This avoid blocking the execution when the "fqdn" is not defined for certains IP addresses, which was causing
2116    # that "socket.timeout" was reached multiple times secuencially, blocking execution for several seconds.
2117
2118    results = []
2119    try:
2120        pool = ThreadPool(8)
2121        results = pool.map(_lookup_fqdn, addresses)
2122        pool.close()
2123        pool.join()
2124    except Exception as exc:  # pylint: disable=broad-except
2125        log.error("Exception while creating a ThreadPool for resolving FQDNs: %s", exc)
2126
2127    for item in results:
2128        if item:
2129            fqdns.update(item)
2130
2131    elapsed = time.time() - start
2132    log.debug("Elapsed time getting FQDNs: %s seconds", elapsed)
2133
2134    return {"fqdns": sorted(list(fqdns))}
2135