Mini Shell
# pylint: disable=invalid-name
"""
Define some generic socket functions for network modules
"""
import fnmatch
import itertools
import logging
import os
import platform
import random
import re
import shutil
import socket
import subprocess
import types
from collections.abc import Mapping, Sequence
from string import ascii_letters, digits
import salt.utils.args
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
from salt._compat import ipaddress
from salt.exceptions import SaltClientError, SaltSystemExit
from salt.utils.decorators.jinja import jinja_filter
from salt.utils.versions import Version
try:
import salt.utils.win_network
WIN_NETWORK_LOADED = True
except ImportError:
WIN_NETWORK_LOADED = False
log = logging.getLogger(__name__)
try:
import ctypes
import ctypes.util
LIBC = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
RES_INIT = LIBC.__res_init
except (ImportError, OSError, AttributeError, TypeError):
pass
class Interfaces:
__slots__ = ("interfaces",)
def __init__(self, interfaces=None):
if interfaces is None:
interfaces = {}
self.interfaces = interfaces
def __call__(self, *args, **kwargs):
if not self.interfaces:
self.interfaces = interfaces()
return self.interfaces
def clear(self):
self.interfaces = {}
_get_interfaces = Interfaces()
_clear_interfaces = _get_interfaces.clear
def sanitize_host(host):
"""
Sanitize host string.
https://tools.ietf.org/html/rfc1123#section-2.1
"""
RFC952_characters = ascii_letters + digits + ".-_"
return "".join([c for c in host[0:255] if c in RFC952_characters])
def isportopen(host, port):
"""
Return status of a port
"""
if not 1 <= int(port) <= 65535:
return False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
out = sock.connect_ex((sanitize_host(host), int(port)))
return out
def host_to_ips(host):
"""
Returns a list of IP addresses of a given hostname or None if not found.
"""
ips = []
try:
for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(
host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM
):
if family == socket.AF_INET:
ip, port = sockaddr
elif family == socket.AF_INET6:
ip, port, flow_info, scope_id = sockaddr
ips.append(ip)
if not ips:
ips = None
except Exception: # pylint: disable=broad-except
ips = None
return ips
def _generate_minion_id():
"""
Get list of possible host names and convention names.
:return:
"""
# There are three types of hostnames:
# 1. Network names. How host is accessed from the network.
# 2. Host aliases. They might be not available in all the network or only locally (/etc/hosts)
# 3. Convention names, an internal nodename.
class DistinctList(list):
"""
List, which allows one to append only distinct objects.
Needs to work on Python 2.6, because of collections.OrderedDict only since 2.7 version.
Override 'filter()' for custom filtering.
"""
localhost_matchers = [
r"localhost.*",
r"ip6-.*",
r"127[.]\d",
r"0\.0\.0\.0",
r"::1.*",
r"ipv6-.*",
r"fe00::.*",
r"fe02::.*",
r"1.0.0.*.ip6.arpa",
]
def append(self, p_object):
if p_object and p_object not in self and not self.filter(p_object):
super().append(p_object)
return self
def extend(self, iterable):
for obj in iterable:
self.append(obj)
return self
def filter(self, element):
"Returns True if element needs to be filtered"
for rgx in self.localhost_matchers:
if re.match(rgx, element):
return True
def first(self):
return self and self[0] or None
hostname = socket.gethostname()
hosts = (
DistinctList()
.append(
salt.utils.stringutils.to_unicode(
socket.getfqdn(salt.utils.stringutils.to_bytes(hostname))
)
)
.append(platform.node())
.append(hostname)
)
if not hosts:
try:
for a_nfo in socket.getaddrinfo(
hosts.first() or "localhost",
None,
socket.AF_INET,
socket.SOCK_RAW,
socket.IPPROTO_IP,
socket.AI_CANONNAME,
):
if len(a_nfo) > 3:
hosts.append(a_nfo[3])
except socket.gaierror:
log.warning(
"Cannot resolve address %s info via socket: %s",
hosts.first() or "localhost (N/A)",
socket.gaierror,
)
# Universal method for everywhere (Linux, Slowlaris, Windows etc)
for f_name in (
"/etc/hostname",
"/etc/nodename",
"/etc/hosts",
r"{win}\system32\drivers\etc\hosts".format(win=os.getenv("WINDIR")),
):
try:
with salt.utils.files.fopen(f_name) as f_hdl:
for line in f_hdl:
line = salt.utils.stringutils.to_unicode(line)
hst = line.strip().split("#")[0].strip().split()
if hst:
if hst[0][:4] in ("127.", "::1") or len(hst) == 1:
hosts.extend(hst)
except OSError:
pass
# include public and private ipaddresses
return hosts.extend(
[addr for addr in ip_addrs() if not ipaddress.ip_address(addr).is_loopback]
)
def generate_minion_id():
"""
Return only first element of the hostname from all possible list.
:return:
"""
try:
ret = salt.utils.stringutils.to_unicode(_generate_minion_id().first())
except TypeError:
ret = None
return ret or "localhost"
def get_socket(addr, type=socket.SOCK_STREAM, proto=0):
"""
Return a socket object for the addr
IP-version agnostic
"""
version = ipaddress.ip_address(addr).version
if version == 4:
family = socket.AF_INET
elif version == 6:
family = socket.AF_INET6
return socket.socket(family, type, proto)
def get_fqhostname():
"""
Returns the fully qualified hostname
"""
l = [socket.getfqdn()]
# try socket.getaddrinfo
try:
addrinfo = socket.getaddrinfo(
socket.gethostname(),
0,
socket.AF_UNSPEC,
socket.SOCK_STREAM,
socket.SOL_TCP,
socket.AI_CANONNAME,
)
for info in addrinfo:
# info struct [family, socktype, proto, canonname, sockaddr]
# On Windows `canonname` can be an empty string
# This can cause the function to return `None`
if len(info) >= 4 and info[3]:
l = [info[3]]
except socket.gaierror:
pass
return l and l[0] or None
def ip_to_host(ip):
"""
Returns the hostname of a given IP
"""
try:
hostname, aliaslist, ipaddrlist = socket.gethostbyaddr(ip)
except Exception as exc: # pylint: disable=broad-except
log.debug("salt.utils.network.ip_to_host(%r) failed: %s", ip, exc)
hostname = None
return hostname
def is_reachable_host(entity_name):
"""
Returns a bool telling if the entity name is a reachable host (IPv4/IPv6/FQDN/etc).
:param hostname:
:return:
"""
try:
assert type(socket.getaddrinfo(entity_name, 0, 0, 0, 0)) == list
ret = True
except socket.gaierror:
ret = False
return ret
def is_ip(ip_addr):
"""
Returns a bool telling if the passed IP is a valid IPv4 or IPv6 address.
"""
return is_ipv4(ip_addr) or is_ipv6(ip_addr)
def is_ipv4(ip_addr):
"""
Returns a bool telling if the value passed to it was a valid IPv4 address
"""
try:
return ipaddress.ip_address(ip_addr).version == 4
except ValueError:
return False
def is_ipv6(ip_addr):
"""
Returns a bool telling if the value passed to it was a valid IPv6 address
"""
try:
return ipaddress.ip_address(ip_addr).version == 6
except ValueError:
return False
def is_loopback(ip_addr):
"""
Returns a bool telling if the value passed to it is a loopback address
"""
try:
return ipaddress.ip_address(ip_addr).is_loopback
except ValueError:
return False
def is_subnet(cidr):
"""
Returns a bool telling if the passed string is an IPv4 or IPv6 subnet
"""
return is_ipv4_subnet(cidr) or is_ipv6_subnet(cidr)
def is_ipv4_subnet(cidr):
"""
Returns a bool telling if the passed string is an IPv4 subnet
"""
try:
return "/" in cidr and bool(ipaddress.IPv4Network(cidr))
except Exception: # pylint: disable=broad-except
return False
def is_ipv6_subnet(cidr):
"""
Returns a bool telling if the passed string is an IPv6 subnet
"""
try:
return "/" in cidr and bool(ipaddress.IPv6Network(cidr))
except Exception: # pylint: disable=broad-except
return False
@jinja_filter("is_ip")
def is_ip_filter(ip_addr, options=None):
"""
Returns a bool telling if the passed IP is a valid IPv4 or IPv6 address.
"""
return is_ipv4_filter(ip_addr, options=options) or is_ipv6_filter(
ip_addr, options=options
)
def _ip_options_global(ip_obj, version):
return not ip_obj.is_private
def _ip_options_multicast(ip_obj, version):
return ip_obj.is_multicast
def _ip_options_loopback(ip_obj, version):
return ip_obj.is_loopback
def _ip_options_link_local(ip_obj, version):
return ip_obj.is_link_local
def _ip_options_private(ip_obj, version):
return ip_obj.is_private
def _ip_options_reserved(ip_obj, version):
return ip_obj.is_reserved
def _ip_options_site_local(ip_obj, version):
if version == 6:
return ip_obj.is_site_local
return False
def _ip_options_unspecified(ip_obj, version):
return ip_obj.is_unspecified
def _ip_options(ip_obj, version, options=None):
# will process and IP options
options_fun_map = {
"global": _ip_options_global,
"link-local": _ip_options_link_local,
"linklocal": _ip_options_link_local,
"ll": _ip_options_link_local,
"link_local": _ip_options_link_local,
"loopback": _ip_options_loopback,
"lo": _ip_options_loopback,
"multicast": _ip_options_multicast,
"private": _ip_options_private,
"public": _ip_options_global,
"reserved": _ip_options_reserved,
"site-local": _ip_options_site_local,
"sl": _ip_options_site_local,
"site_local": _ip_options_site_local,
"unspecified": _ip_options_unspecified,
}
if not options:
return str(ip_obj) # IP version already checked
options_list = [option.strip() for option in options.split(",")]
for option, fun in options_fun_map.items():
if option in options_list:
fun_res = fun(ip_obj, version)
if not fun_res:
return None
# stop at first failed test
# else continue
return str(ip_obj)
def _is_ipv(ip_addr, version, options=None):
if not version:
version = 4
if version not in (4, 6):
return None
try:
ip_obj = ipaddress.ip_address(ip_addr)
except ValueError:
# maybe it is an IP network
try:
ip_obj = ipaddress.ip_interface(ip_addr)
except ValueError:
# nope, still not :(
return None
if not ip_obj.version == version:
return None
# has the right version, let's move on
return _ip_options(ip_obj, version, options=options)
@jinja_filter("is_ipv4")
def is_ipv4_filter(ip_addr, options=None):
"""
Returns a bool telling if the value passed to it was a valid IPv4 address.
ip
The IP address.
net: False
Consider IP addresses followed by netmask.
options
CSV of options regarding the nature of the IP address. E.g.: loopback, multicast, private etc.
"""
_is_ipv4 = _is_ipv(ip_addr, 4, options=options)
return isinstance(_is_ipv4, str)
@jinja_filter("is_ipv6")
def is_ipv6_filter(ip_addr, options=None):
"""
Returns a bool telling if the value passed to it was a valid IPv6 address.
ip
The IP address.
net: False
Consider IP addresses followed by netmask.
options
CSV of options regarding the nature of the IP address. E.g.: loopback, multicast, private etc.
"""
_is_ipv6 = _is_ipv(ip_addr, 6, options=options)
return isinstance(_is_ipv6, str)
def _ipv_filter(value, version, options=None):
if version not in (4, 6):
return
if isinstance(value, (str, bytes)):
return _is_ipv(
value, version, options=options
) # calls is_ipv4 or is_ipv6 for `value`
elif isinstance(value, (list, tuple, types.GeneratorType)):
# calls is_ipv4 or is_ipv6 for each element in the list
# os it filters and returns only those elements having the desired IP version
return [
_is_ipv(addr, version, options=options)
for addr in value
if _is_ipv(addr, version, options=options) is not None
]
return None
@jinja_filter("ipv4")
def ipv4(value, options=None):
"""
Filters a list and returns IPv4 values only.
"""
return _ipv_filter(value, 4, options=options)
@jinja_filter("ipv6")
def ipv6(value, options=None):
"""
Filters a list and returns IPv6 values only.
"""
return _ipv_filter(value, 6, options=options)
@jinja_filter("ipaddr")
def ipaddr(value, options=None):
"""
Filters and returns only valid IP objects.
"""
ipv4_obj = ipv4(value, options=options)
ipv6_obj = ipv6(value, options=options)
if ipv4_obj is None or ipv6_obj is None:
# an IP address can be either IPv4 either IPv6
# therefofe if the value passed as arg is not a list, at least one of the calls above will return None
# if one of them is none, means that we should return only one of them
return ipv4_obj or ipv6_obj # one of them
else:
return ipv4_obj + ipv6_obj # extend lists
def _filter_ipaddr(value, options, version=None):
ipaddr_filter_out = None
if version:
if version == 4:
ipaddr_filter_out = ipv4(value, options)
elif version == 6:
ipaddr_filter_out = ipv6(value, options)
else:
ipaddr_filter_out = ipaddr(value, options)
if not ipaddr_filter_out:
return
if not isinstance(ipaddr_filter_out, (list, tuple, types.GeneratorType)):
ipaddr_filter_out = [ipaddr_filter_out]
return ipaddr_filter_out
@jinja_filter("ip_host")
def ip_host(value, options=None, version=None):
"""
Returns the interfaces IP address, e.g.: 192.168.0.1/28.
"""
ipaddr_filter_out = _filter_ipaddr(value, options=options, version=version)
if not ipaddr_filter_out:
return
if not isinstance(value, (list, tuple, types.GeneratorType)):
return str(ipaddress.ip_interface(ipaddr_filter_out[0]))
return [str(ipaddress.ip_interface(ip_a)) for ip_a in ipaddr_filter_out]
def _network_hosts(ip_addr_entry):
return [
str(host) for host in ipaddress.ip_network(ip_addr_entry, strict=False).hosts()
]
@jinja_filter("network_hosts")
def network_hosts(value, options=None, version=None):
"""
Return the list of hosts within a network.
.. note::
When running this command with a large IPv6 network, the command will
take a long time to gather all of the hosts.
"""
ipaddr_filter_out = _filter_ipaddr(value, options=options, version=version)
if not ipaddr_filter_out:
return
if not isinstance(value, (list, tuple, types.GeneratorType)):
return _network_hosts(ipaddr_filter_out[0])
return [_network_hosts(ip_a) for ip_a in ipaddr_filter_out]
def _network_size(ip_addr_entry):
return ipaddress.ip_network(ip_addr_entry, strict=False).num_addresses
@jinja_filter("network_size")
def network_size(value, options=None, version=None):
"""
Get the size of a network.
"""
ipaddr_filter_out = _filter_ipaddr(value, options=options, version=version)
if not ipaddr_filter_out:
return
if not isinstance(value, (list, tuple, types.GeneratorType)):
return _network_size(ipaddr_filter_out[0])
return [_network_size(ip_a) for ip_a in ipaddr_filter_out]
def natural_ipv4_netmask(ip_addr, fmt="prefixlen"):
"""
Returns the "natural" mask of an IPv4 address
"""
bits = _ipv4_to_bits(ip_addr)
if bits.startswith("11"):
mask = "24"
elif bits.startswith("1"):
mask = "16"
else:
mask = "8"
if fmt == "netmask":
return cidr_to_ipv4_netmask(mask)
else:
return "/" + mask
def rpad_ipv4_network(ip_addr):
"""
Returns an IP network address padded with zeros.
Ex: '192.168.3' -> '192.168.3.0'
'10.209' -> '10.209.0.0'
"""
return ".".join(itertools.islice(itertools.chain(ip_addr.split("."), "0000"), 0, 4))
def cidr_to_ipv4_netmask(cidr_bits):
"""
Returns an IPv4 netmask
"""
try:
cidr_bits = int(cidr_bits)
if not 1 <= cidr_bits <= 32:
return ""
except ValueError:
return ""
netmask = ""
for idx in range(4):
if idx:
netmask += "."
if cidr_bits >= 8:
netmask += "255"
cidr_bits -= 8
else:
netmask += f"{256 - (2 ** (8 - cidr_bits)):d}"
cidr_bits = 0
return netmask
def _number_of_set_bits_to_ipv4_netmask(set_bits):
"""
Returns an IPv4 netmask from the integer representation of that mask.
Ex. 0xffffff00 -> '255.255.255.0'
0xffff6400 -> '255.255.100.0'
"""
# Note: previously used cidr but that is counting number of bits in set_bits
# and can lead to wrong netmaks values, for example:
# 0xFFFF6400 is 255.255.100.0, 0x64 is 100 decimal
# but if convert to cidr first, it gives 19 bits, get 255.255.224.0 - WRONG
# leveraging Python ip_address library for different method of conversion
return str(ipaddress.ip_address(set_bits))
def _number_of_set_bits(x):
"""
Returns the number of bits that are set in a 32bit int
"""
# Taken from http://stackoverflow.com/a/4912729. Many thanks!
x -= (x >> 1) & 0x55555555
x = ((x >> 2) & 0x33333333) + (x & 0x33333333)
x = ((x >> 4) + x) & 0x0F0F0F0F
x += x >> 8
x += x >> 16
return x & 0x0000003F
def _interfaces_ip(out):
"""
Uses ip to return a dictionary of interfaces with various information about
each (up/down state, ip address, netmask, and hwaddr)
"""
ret = dict()
def parse_network(value, cols):
"""
Return a tuple of ip, netmask, broadcast
based on the current set of cols
"""
brd = None
scope = None
if "/" in value: # we have a CIDR in this address
ip, cidr = value.split("/")
else:
ip = value
cidr = 32
if type_ == "inet":
mask = cidr_to_ipv4_netmask(int(cidr))
if "brd" in cols:
brd = cols[cols.index("brd") + 1]
elif type_ == "inet6":
mask = cidr
if "scope" in cols:
scope = cols[cols.index("scope") + 1]
return (ip, mask, brd, scope)
groups = re.compile("\r?\n\\d").split(out)
for group in groups:
iface = None
data = dict()
for line in group.splitlines():
if " " not in line:
continue
match = re.match(r"^\d*:\s+([\w.\-]+)(?:@)?([\w.\-]+)?:\s+<(.+)>", line)
if match:
iface, parent, attrs = match.groups()
if "UP" in attrs.split(","):
data["up"] = True
else:
data["up"] = False
if parent:
data["parent"] = parent
continue
cols = line.split()
if len(cols) >= 2:
type_, value = tuple(cols[0:2])
iflabel = cols[-1:][0]
if type_ in ("inet", "inet6"):
ipaddr, netmask, broadcast, scope = parse_network(value, cols)
addr_obj = dict()
if "secondary" not in cols:
if type_ == "inet":
if "inet" not in data:
data["inet"] = list()
addr_obj["address"] = ipaddr
addr_obj["netmask"] = netmask
addr_obj["broadcast"] = broadcast
addr_obj["label"] = iflabel
data["inet"].append(addr_obj)
elif type_ == "inet6":
if "inet6" not in data:
data["inet6"] = list()
addr_obj["address"] = ipaddr
addr_obj["prefixlen"] = netmask
addr_obj["scope"] = scope
data["inet6"].append(addr_obj)
else:
if type_ == "inet":
if "secondary" not in data:
data["secondary"] = list()
addr_obj["type"] = type_
addr_obj["address"] = ipaddr
addr_obj["netmask"] = netmask
addr_obj["broadcast"] = broadcast
addr_obj["label"] = iflabel
data["secondary"].append(addr_obj)
elif type_ == "inet6":
if "secondary" not in data:
data["secondary"] = list()
addr_obj["type"] = type_
addr_obj["address"] = ipaddr
addr_obj["prefixlen"] = netmask
addr_obj["scope"] = scope
data["secondary"].append(addr_obj)
elif type_.startswith("link"):
data["hwaddr"] = value
if iface:
ret[iface] = data
del iface, data
return ret
def _interfaces_ifconfig(out):
"""
Uses ifconfig to return a dictionary of interfaces with various information
about each (up/down state, ip address, netmask, and hwaddr)
"""
ret = dict()
piface = re.compile(r"^([^\s:]+)")
pmac = re.compile(".*?(?:HWaddr|ether|address:|lladdr) ([0-9a-fA-F:]+)")
if salt.utils.platform.is_sunos():
pip = re.compile(r".*?(?:inet\s+)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(.*)")
pip6 = re.compile(".*?(?:inet6 )([0-9a-fA-F:]+)")
pmask6 = re.compile(r".*?(?:inet6 [0-9a-fA-F:]+/(\d+)).*")
else:
pip = re.compile(r".*?(?:inet addr:|inet [^\d]*)(.*?)\s")
pip6 = re.compile(".*?(?:inet6 addr: (.*?)/|inet6 )([0-9a-fA-F:]+)")
pmask6 = re.compile(
r".*?(?:inet6 addr: [0-9a-fA-F:]+/(\d+)|prefixlen (\d+))(?:"
r" Scope:([a-zA-Z]+)| scopeid (0x[0-9a-fA-F]))?"
)
pmask = re.compile(r".*?(?:Mask:|netmask )(?:((?:0x)?[0-9a-fA-F]{8})|([\d\.]+))")
pupdown = re.compile("UP")
pbcast = re.compile(r".*?(?:Bcast:|broadcast )([\d\.]+)")
groups = re.compile("\r?\n(?=\\S)").split(out)
for group in groups:
data = dict()
iface = ""
updown = False
for line in group.splitlines():
miface = piface.match(line)
mmac = pmac.match(line)
mip = pip.match(line)
mip6 = pip6.match(line)
mupdown = pupdown.search(line)
if miface:
iface = miface.group(1)
if mmac:
data["hwaddr"] = mmac.group(1)
if salt.utils.platform.is_sunos():
expand_mac = []
for chunk in data["hwaddr"].split(":"):
expand_mac.append(f"0{chunk}" if len(chunk) < 2 else f"{chunk}")
data["hwaddr"] = ":".join(expand_mac)
if mip:
if "inet" not in data:
data["inet"] = list()
addr_obj = dict()
addr_obj["address"] = mip.group(1)
mmask = pmask.match(line)
if mmask:
if mmask.group(1):
mmask = _number_of_set_bits_to_ipv4_netmask(
int(mmask.group(1), 16)
)
else:
mmask = mmask.group(2)
addr_obj["netmask"] = mmask
mbcast = pbcast.match(line)
if mbcast:
addr_obj["broadcast"] = mbcast.group(1)
data["inet"].append(addr_obj)
if mupdown:
updown = True
if mip6:
if "inet6" not in data:
data["inet6"] = list()
addr_obj = dict()
addr_obj["address"] = mip6.group(1) or mip6.group(2)
mmask6 = pmask6.match(line)
if mmask6:
addr_obj["prefixlen"] = mmask6.group(1) or mmask6.group(2)
if not salt.utils.platform.is_sunos():
ipv6scope = mmask6.group(3) or mmask6.group(4)
addr_obj["scope"] = (
ipv6scope.lower() if ipv6scope is not None else ipv6scope
)
# SunOS sometimes has ::/0 as inet6 addr when using addrconf
if (
not salt.utils.platform.is_sunos()
or addr_obj["address"] != "::"
and addr_obj["prefixlen"] != 0
):
data["inet6"].append(addr_obj)
data["up"] = updown
if iface in ret:
# SunOS optimization, where interfaces occur twice in 'ifconfig -a'
# output with the same name: for ipv4 and then for ipv6 addr family.
# Every instance has its own 'UP' status and we assume that ipv4
# status determines global interface status.
#
# merge items with higher priority for older values
# after that merge the inet and inet6 sub items for both
ret[iface] = dict(list(data.items()) + list(ret[iface].items()))
if "inet" in data:
ret[iface]["inet"].extend(
x for x in data["inet"] if x not in ret[iface]["inet"]
)
if "inet6" in data:
ret[iface]["inet6"].extend(
x for x in data["inet6"] if x not in ret[iface]["inet6"]
)
else:
ret[iface] = data
del data
return ret
def linux_interfaces():
"""
Obtain interface information for *NIX/BSD variants
"""
ifaces = dict()
ip_path = salt.utils.path.which("ip")
ifconfig_path = None if ip_path else salt.utils.path.which("ifconfig")
if ip_path:
cmd1 = subprocess.Popen(
[ip_path, "link", "show"],
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
cmd2 = subprocess.Popen(
[ip_path, "addr", "show"],
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
ifaces = _interfaces_ip(
"{}\n{}".format(
salt.utils.stringutils.to_str(cmd1), salt.utils.stringutils.to_str(cmd2)
)
)
elif ifconfig_path:
cmd = subprocess.Popen(
[ifconfig_path, "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
ifaces = _interfaces_ifconfig(salt.utils.stringutils.to_str(cmd))
return ifaces
def _netbsd_interfaces_ifconfig(out):
"""
Uses ifconfig to return a dictionary of interfaces with various information
about each (up/down state, ip address, netmask, and hwaddr)
"""
ret = dict()
piface = re.compile(r"^([^\s:]+)")
pmac = re.compile(".*?address: ([0-9a-f:]+)")
pip = re.compile(r".*?inet [^\d]*(.*?)/([\d]*)\s")
pip6 = re.compile(r".*?inet6 ([0-9a-f:]+)%([a-zA-Z0-9]*)/([\d]*)\s")
pupdown = re.compile("UP")
pbcast = re.compile(r".*?broadcast ([\d\.]+)")
groups = re.compile("\r?\n(?=\\S)").split(out)
for group in groups:
data = dict()
iface = ""
updown = False
for line in group.splitlines():
miface = piface.match(line)
mmac = pmac.match(line)
mip = pip.match(line)
mip6 = pip6.match(line)
mupdown = pupdown.search(line)
if miface:
iface = miface.group(1)
if mmac:
data["hwaddr"] = mmac.group(1)
if mip:
if "inet" not in data:
data["inet"] = list()
addr_obj = dict()
addr_obj["address"] = mip.group(1)
mmask = mip.group(2)
if mip.group(2):
addr_obj["netmask"] = cidr_to_ipv4_netmask(mip.group(2))
mbcast = pbcast.match(line)
if mbcast:
addr_obj["broadcast"] = mbcast.group(1)
data["inet"].append(addr_obj)
if mupdown:
updown = True
if mip6:
if "inet6" not in data:
data["inet6"] = list()
addr_obj = dict()
addr_obj["address"] = mip6.group(1)
mmask6 = mip6.group(3)
addr_obj["scope"] = mip6.group(2)
addr_obj["prefixlen"] = mip6.group(3)
data["inet6"].append(addr_obj)
data["up"] = updown
ret[iface] = data
del data
return ret
def _junos_interfaces_ifconfig(out): # pragma: no cover
"""
Uses ifconfig to return a dictionary of interfaces with various information
about each (up/down state, ip address, netmask, and hwaddr)
"""
ret = dict()
piface = re.compile(r"^([^\s:]+)")
pmac = re.compile("curr media .*? ([0-9a-f:]+)")
pip = re.compile(
r".*?inet\s*(primary)*\s+mtu"
r" (\d+)\s+local=[^\d]*(.*?)\s{0,40}dest=[^\d]*(.*?)\/([\d]*)\s{0,40}bcast=((?:[0-9]{1,3}\.){3}[0-9]{1,3})"
)
pip6 = re.compile(
r".*?inet6 mtu [^\d]+\s{0,40}local=([0-9a-f:]+)%([a-zA-Z0-9]*)/([\d]*)\s"
)
pupdown = re.compile("UP")
pbcast = re.compile(r".*?broadcast ([\d\.]+)")
groups = re.compile("\r?\n(?=\\S)").split(out)
for group in groups:
data = dict()
iface = ""
updown = False
primary = False
for line in group.splitlines():
miface = piface.match(line)
mmac = pmac.match(line)
mip = pip.match(line)
mip6 = pip6.match(line)
mupdown = pupdown.search(line)
if miface:
iface = miface.group(1)
if mmac:
data["hwaddr"] = mmac.group(1)
if mip:
if "primary" in data:
primary = True
if "inet" not in data:
data["inet"] = list()
if mip.group(2):
data["mtu"] = int(mip.group(2))
addr_obj = dict()
addr_obj["address"] = mip.group(3)
mmask = mip.group(5)
if mip.group(5):
addr_obj["netmask"] = cidr_to_ipv4_netmask(mip.group(5))
mbcast = pbcast.match(line)
if mbcast:
addr_obj["broadcast"] = mbcast.group(1)
data["inet"].append(addr_obj)
if mupdown:
updown = True
if mip6:
if "inet6" not in data:
data["inet6"] = list()
addr_obj = dict()
addr_obj["address"] = mip6.group(1)
mmask6 = mip6.group(3)
addr_obj["scope"] = mip6.group(2)
addr_obj["prefixlen"] = mip6.group(3)
data["inet6"].append(addr_obj)
data["up"] = updown
ret[iface] = data
del data
return ret
def junos_interfaces(): # pragma: no cover
"""
Obtain interface information for Junos; ifconfig
output diverged from other BSD variants (Netmask is now part of the
address)
"""
ifconfig_path = salt.utils.path.which("ifconfig")
cmd = subprocess.Popen(
[ifconfig_path, "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
return _junos_interfaces_ifconfig(salt.utils.stringutils.to_str(cmd))
def netbsd_interfaces():
"""
Obtain interface information for NetBSD >= 8 where the ifconfig
output diverged from other BSD variants (Netmask is now part of the
address)
"""
# NetBSD versions prior to 8.0 can still use linux_interfaces()
if Version(os.uname()[2]) < Version("8.0"):
return linux_interfaces()
ifconfig_path = salt.utils.path.which("ifconfig")
cmd = subprocess.Popen(
[ifconfig_path, "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
return _netbsd_interfaces_ifconfig(salt.utils.stringutils.to_str(cmd))
def _interfaces_ipconfig(out):
"""
Returns a dictionary of interfaces with various information about each
(up/down state, ip address, netmask, and hwaddr)
NOTE: This is not used by any function and may be able to be removed in the
future.
"""
ifaces = dict()
iface = None
addr = None
adapter_iface_regex = re.compile(r"adapter (\S.+):$")
for line in out.splitlines():
if not line:
continue
# TODO what does Windows call Infiniband and 10/40gige adapters
if line.startswith("Ethernet"):
iface = ifaces[adapter_iface_regex.search(line).group(1)]
iface["up"] = True
addr = {}
continue
if iface:
key, val = line.split(",", 1)
key = key.strip(" .")
val = val.strip()
if addr and key == "Subnet Mask":
addr["netmask"] = val
elif key in ("IP Address", "IPv4 Address"):
if "inet" not in iface:
iface["inet"] = list()
addr = {
"address": val.rstrip("(Preferred)"),
"netmask": None,
"broadcast": None,
} # TODO find the broadcast
iface["inet"].append(addr)
elif "IPv6 Address" in key:
if "inet6" not in iface:
iface["inet"] = list()
# XXX What is the prefixlen!?
addr = {"address": val.rstrip("(Preferred)"), "prefixlen": None}
iface["inet6"].append(addr)
elif key == "Physical Address":
iface["hwaddr"] = val
elif key == "Media State":
# XXX seen used for tunnel adaptors
# might be useful
iface["up"] = val != "Media disconnected"
def win_interfaces():
"""
Obtain interface information for Windows systems
"""
if WIN_NETWORK_LOADED is False:
# Let's throw the ImportException again
import salt.utils.win_network as _
return salt.utils.win_network.get_interface_info()
def interfaces():
"""
Return a dictionary of information about all the interfaces on the minion
"""
if salt.utils.platform.is_windows():
return win_interfaces()
elif salt.utils.platform.is_junos():
return junos_interfaces()
elif salt.utils.platform.is_netbsd():
return netbsd_interfaces()
else:
return linux_interfaces()
def get_net_start(ipaddr, netmask):
"""
Return the address of the network
"""
net = ipaddress.ip_network(f"{ipaddr}/{netmask}", strict=False)
return str(net.network_address)
def get_net_size(mask):
"""
Turns an IPv4 netmask into its corresponding prefix length
(255.255.255.0 -> 24 as in 192.168.1.10/24).
"""
binary_str = ""
for octet in mask.split("."):
binary_str += bin(int(octet))[2:].zfill(8)
return len(binary_str.rstrip("0"))
def calc_net(ipaddr, netmask=None):
"""
Takes IP (CIDR notation supported) and optionally netmask
and returns the network in CIDR-notation.
(The IP can be any IP inside the subnet)
"""
if netmask is not None:
ipaddr = f"{ipaddr}/{netmask}"
return str(ipaddress.ip_network(ipaddr, strict=False))
def _ipv4_to_bits(ipaddr):
"""
Accepts an IPv4 dotted quad and returns a string representing its binary
counterpart
"""
return "".join([bin(int(x))[2:].rjust(8, "0") for x in ipaddr.split(".")])
def _get_iface_info(iface):
"""
If `iface` is available, return interface info and no error, otherwise
return no info and log and return an error
"""
iface_info = interfaces()
if iface in iface_info.keys():
return iface_info, False
else:
error_msg = 'Interface "{}" not in available interfaces: "{}"'.format(
iface, '", "'.join(iface_info.keys())
)
log.error(error_msg)
return None, error_msg
def _hw_addr_aix(iface): # pragma: no cover
"""
Return the hardware address (a.k.a. MAC address) for a given interface on AIX
MAC address not available in through interfaces
"""
cmd = subprocess.Popen(
["grep", "Hardware Address"],
stdin=subprocess.Popen(
["entstat", "-d", iface],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]
if cmd:
comps = cmd.split(" ")
if len(comps) == 3:
mac_addr = comps[2].strip("'").strip()
return mac_addr
error_msg = 'Interface "{}" either not available or does not contain a hardware address'.format(
iface
)
log.error(error_msg)
return error_msg
def hw_addr(iface):
"""
Return the hardware address (a.k.a. MAC address) for a given interface
.. versionchanged:: 2016.11.4
Added support for AIX
"""
if salt.utils.platform.is_aix():
return _hw_addr_aix(iface)
iface_info, error = _get_iface_info(iface)
if error is False:
return iface_info.get(iface, {}).get("hwaddr", "")
else:
return error
def interface(iface):
"""
Return the details of `iface` or an error if it does not exist
"""
iface_info, error = _get_iface_info(iface)
if error is False:
return iface_info.get(iface, {}).get("inet", "")
else:
return error
def interface_ip(iface):
"""
Return `iface` IPv4 addr or an error if `iface` does not exist
"""
iface_info, error = _get_iface_info(iface)
if error is False:
inet = iface_info.get(iface, {}).get("inet", None)
return inet[0].get("address", "") if inet else ""
else:
return error
def _subnets(proto="inet", interfaces_=None):
"""
Returns a list of subnets to which the host belongs
"""
if interfaces_ is None:
ifaces = interfaces()
elif isinstance(interfaces_, list):
ifaces = {}
for key, value in interfaces().items():
if key in interfaces_:
ifaces[key] = value
else:
ifaces = {interfaces_: interfaces().get(interfaces_, {})}
ret = set()
if proto == "inet":
subnet = "netmask"
dflt_cidr = 32
elif proto == "inet6":
subnet = "prefixlen"
dflt_cidr = 128
else:
log.error("Invalid proto %s calling subnets()", proto)
return
for ip_info in ifaces.values():
addrs = ip_info.get(proto, [])
addrs.extend(
[addr for addr in ip_info.get("secondary", []) if addr.get("type") == proto]
)
for intf in addrs:
if subnet in intf:
intf = ipaddress.ip_interface(
"{}/{}".format(intf["address"], intf[subnet])
)
else:
intf = ipaddress.ip_interface(
"{}/{}".format(intf["address"], dflt_cidr)
)
if not intf.is_loopback:
ret.add(intf.network)
return [str(net) for net in sorted(ret)]
def subnets(interfaces=None):
"""
Returns a list of IPv4 subnets to which the host belongs
"""
return _subnets("inet", interfaces_=interfaces)
def subnets6():
"""
Returns a list of IPv6 subnets to which the host belongs
"""
return _subnets("inet6")
def in_subnet(cidr, addr=None):
"""
Returns True if host or (any of) addrs is within specified subnet, otherwise False
"""
try:
cidr = ipaddress.ip_network(cidr)
except ValueError:
log.error("Invalid CIDR '%s'", cidr)
return False
if addr is None:
addr = ip_addrs()
addr.extend(ip_addrs6())
elif not isinstance(addr, (list, tuple)):
addr = (addr,)
return any(ipaddress.ip_address(item) in cidr for item in addr)
def _get_ips(ifaces, proto="inet"):
"""
Accepts a dict of interface data and returns a list of dictionaries
"""
ret = []
for ip_info in ifaces.values():
ret.extend(ip_info.get(proto, []))
ret.extend(
[addr for addr in ip_info.get("secondary", []) if addr.get("type") == proto]
)
return ret
def _filter_interfaces(interface=None, interface_data=None):
"""
Gather interface data if not passed in, and optionally filter by the
specified interface name.
"""
ifaces = interface_data if isinstance(interface_data, dict) else interfaces()
if interface is None:
ret = ifaces
else:
interface = salt.utils.args.split_input(interface)
# pylint: disable=not-an-iterable
ret = {
k: v
for k, v in ifaces.items()
if any(fnmatch.fnmatch(k, pat) for pat in interface)
}
# pylint: enable=not-an-iterable
return ret
def _ip_addrs(
interface=None, include_loopback=False, interface_data=None, proto="inet"
):
"""
Return the full list of IP adresses matching the criteria
proto = inet|inet6
"""
addrs = _get_ips(_filter_interfaces(interface, interface_data), proto=proto)
ret = set()
for addr in addrs:
addr = ipaddress.ip_address(addr.get("address"))
if not addr.is_loopback or include_loopback:
ret.add(addr)
return [str(addr) for addr in sorted(ret)]
def ip_addrs(interface=None, include_loopback=False, interface_data=None):
"""
Returns a list of IPv4 addresses assigned to the host. 127.0.0.1 is
ignored, unless 'include_loopback=True' is indicated. If 'interface' is
provided, then only IP addresses from that interface will be returned.
"""
return _ip_addrs(interface, include_loopback, interface_data, "inet")
def ip_addrs6(interface=None, include_loopback=False, interface_data=None):
"""
Returns a list of IPv6 addresses assigned to the host. ::1 is ignored,
unless 'include_loopback=True' is indicated. If 'interface' is provided,
then only IP addresses from that interface will be returned.
"""
return _ip_addrs(interface, include_loopback, interface_data, "inet6")
def _ip_networks(
interface=None,
include_loopback=False,
verbose=False,
interface_data=None,
proto="inet",
):
"""
Returns a list of networks to which the minion belongs. The results can be
restricted to a single interface using the ``interface`` argument.
"""
addrs = _get_ips(_filter_interfaces(interface, interface_data), proto=proto)
ret = set()
for addr in addrs:
_ip = addr.get("address")
_net = addr.get("netmask" if proto == "inet" else "prefixlen")
if _ip and _net:
try:
ip_net = ipaddress.ip_network(f"{_ip}/{_net}", strict=False)
except Exception: # pylint: disable=broad-except
continue
if not ip_net.is_loopback or include_loopback:
ret.add(ip_net)
if not verbose:
return [str(addr) for addr in sorted(ret)]
verbose_ret = {
str(x): {
"address": str(x.network_address),
"netmask": str(x.netmask),
"num_addresses": x.num_addresses,
"prefixlen": x.prefixlen,
}
for x in ret
}
return verbose_ret
def ip_networks(
interface=None, include_loopback=False, verbose=False, interface_data=None
):
"""
Returns the IPv4 networks to which the minion belongs. Networks will be
returned as a list of network/prefixlen. To get more information about a
each network, use verbose=True and a dictionary with more information will
be returned.
"""
return _ip_networks(
interface=interface,
include_loopback=include_loopback,
verbose=verbose,
interface_data=interface_data,
proto="inet",
)
def ip_networks6(
interface=None, include_loopback=False, verbose=False, interface_data=None
):
"""
Returns the IPv6 networks to which the minion belongs. Networks will be
returned as a list of network/prefixlen. To get more information about a
each network, use verbose=True and a dictionary with more information will
be returned.
"""
return _ip_networks(
interface=interface,
include_loopback=include_loopback,
verbose=verbose,
interface_data=interface_data,
proto="inet6",
)
def hex2ip(hex_ip, invert=False):
"""
Convert a hex string to an ip, if a failure occurs the original hex is
returned. If 'invert=True' assume that ip from /proc/net/<proto>
"""
if len(hex_ip) == 32: # ipv6
ip_addr = []
for i in range(0, 32, 8):
ip_part = hex_ip[i : i + 8]
ip_part = [ip_part[x : x + 2] for x in range(0, 8, 2)]
if invert:
ip_addr.append("{0[3]}{0[2]}:{0[1]}{0[0]}".format(ip_part))
else:
ip_addr.append("{0[0]}{0[1]}:{0[2]}{0[3]}".format(ip_part))
try:
address = ipaddress.IPv6Address(":".join(ip_addr))
if address.ipv4_mapped:
return str(address.ipv4_mapped)
else:
return address.compressed
except ipaddress.AddressValueError as ex:
log.error("hex2ip - ipv6 address error: %s", ex)
return hex_ip
try:
hip = int(hex_ip, 16)
except ValueError:
return hex_ip
if invert:
return "{3}.{2}.{1}.{0}".format(
hip >> 24 & 255, hip >> 16 & 255, hip >> 8 & 255, hip & 255
)
return "{}.{}.{}.{}".format(
hip >> 24 & 255, hip >> 16 & 255, hip >> 8 & 255, hip & 255
)
def mac2eui64(mac, prefix=None):
"""
Convert a MAC address to a EUI64 identifier
or, with prefix provided, a full IPv6 address
"""
# http://tools.ietf.org/html/rfc4291#section-2.5.1
eui64 = re.sub(r"[.:-]", "", mac).lower()
eui64 = eui64[0:6] + "fffe" + eui64[6:]
eui64 = hex(int(eui64[0:2], 16) | 2)[2:].zfill(2) + eui64[2:]
if prefix is None:
return ":".join(re.findall(r".{4}", eui64))
else:
try:
net = ipaddress.ip_network(prefix, strict=False)
euil = int(f"0x{eui64}", 16)
return f"{net[euil]}/{net.prefixlen}"
except Exception: # pylint: disable=broad-except
return
def active_tcp():
"""
Return a dict describing all active tcp connections as quickly as possible
"""
ret = {}
for statf in ["/proc/net/tcp", "/proc/net/tcp6"]:
if not os.path.isfile(statf):
continue
with salt.utils.files.fopen(statf, "rb") as fp_:
for line in fp_:
line = salt.utils.stringutils.to_unicode(line)
if line.strip().startswith("sl"):
continue
iret = _parse_tcp_line(line)
slot = next(iter(iret))
if iret[slot]["state"] == 1: # 1 is ESTABLISHED
del iret[slot]["state"]
ret[len(ret)] = iret[slot]
return ret
def local_port_tcp(port):
"""
Return a set of remote ip addrs attached to the specified local port
"""
ret = _remotes_on(port, "local_port")
return ret
def remote_port_tcp(port):
"""
Return a set of ip addrs the current host is connected to on given port
"""
ret = _remotes_on(port, "remote_port")
return ret
def _remotes_on(port, which_end):
"""
Return a set of ip addrs active tcp connections
"""
port = int(port)
ret = _netlink_tool_remote_on(port, which_end)
if ret is not None:
return ret
ret = set()
proc_available = False
for statf in ["/proc/net/tcp", "/proc/net/tcp6"]:
if not os.path.isfile(statf):
continue
proc_available = True
with salt.utils.files.fopen(statf, "r") as fp_:
for line in fp_:
line = salt.utils.stringutils.to_unicode(line)
if line.strip().startswith("sl"):
continue
iret = _parse_tcp_line(line)
slot = next(iter(iret))
if (
iret[slot][which_end] == port and iret[slot]["state"] == 1
): # 1 is ESTABLISHED
ret.add(iret[slot]["remote_addr"])
if not proc_available: # Fallback to use OS specific tools
if salt.utils.platform.is_sunos():
return _sunos_remotes_on(port, which_end)
if salt.utils.platform.is_freebsd():
return _freebsd_remotes_on(port, which_end)
if salt.utils.platform.is_netbsd():
return _netbsd_remotes_on(port, which_end)
if salt.utils.platform.is_openbsd():
return _openbsd_remotes_on(port, which_end)
if salt.utils.platform.is_windows():
return _windows_remotes_on(port, which_end)
if salt.utils.platform.is_aix():
return _aix_remotes_on(port, which_end)
return _linux_remotes_on(port, which_end)
return ret
def _parse_tcp_line(line):
"""
Parse a single line from the contents of /proc/net/tcp or /proc/net/tcp6
"""
ret = {}
comps = line.strip().split()
slot = comps[0].rstrip(":")
ret[slot] = {}
l_addr, l_port = comps[1].split(":")
r_addr, r_port = comps[2].split(":")
ret[slot]["local_addr"] = hex2ip(l_addr, True)
ret[slot]["local_port"] = int(l_port, 16)
ret[slot]["remote_addr"] = hex2ip(r_addr, True)
ret[slot]["remote_port"] = int(r_port, 16)
ret[slot]["state"] = int(comps[3], 16)
return ret
def _netlink_tool_remote_on(port, which_end):
"""
Returns set of IPv4/IPv6 host addresses of remote established connections
on local or remote tcp port.
Parses output of shell 'ss' to get connections
[root@salt-master ~]# ss -ant
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 *:80 *:*
LISTEN 0 128 *:22 *:*
ESTAB 0 0 127.0.0.1:56726 127.0.0.1:4505
ESTAB 0 0 [::ffff:127.0.0.1]:41323 [::ffff:127.0.0.1]:4505
"""
remotes = set()
valid = False
tcp_end = "dst" if which_end == "remote_port" else "src"
try:
data = subprocess.check_output(["ss", "-ant", tcp_end, f":{port}"])
except subprocess.CalledProcessError:
log.error("Failed ss")
raise
except OSError: # not command "No such file or directory"
return None
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
if "Address:Port" in line: # ss tools may not be valid
valid = True
continue
elif "ESTAB" not in line:
continue
chunks = line.split()
local_host, local_port = chunks[3].rsplit(":", 1)
remote_host, remote_port = chunks[4].rsplit(":", 1)
if which_end == "remote_port" and int(remote_port) != int(port):
continue
if which_end == "local_port" and int(local_port) != int(port):
continue
remotes.add(remote_host.strip("[]"))
if valid is False:
remotes = None
return remotes
def _sunos_remotes_on(port, which_end): # pragma: no cover
"""
SunOS specific helper function.
Returns set of ipv4 host addresses of remote established connections
on local or remote tcp port.
Parses output of shell 'netstat' to get connections
[root@salt-master ~]# netstat -f inet -n
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
-------------------- -------------------- ----- ------ ----- ------ -----------
10.0.0.101.4505 10.0.0.1.45329 1064800 0 1055864 0 ESTABLISHED
10.0.0.101.4505 10.0.0.100.50798 1064800 0 1055864 0 ESTABLISHED
"""
remotes = set()
try:
data = subprocess.check_output(["netstat", "-f", "inet", "-n"])
except subprocess.CalledProcessError:
log.error("Failed netstat")
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
if "ESTABLISHED" not in line:
continue
chunks = line.split()
local_host, local_port = chunks[0].rsplit(".", 1)
remote_host, remote_port = chunks[1].rsplit(".", 1)
if which_end == "remote_port" and int(remote_port) != port:
continue
if which_end == "local_port" and int(local_port) != port:
continue
remotes.add(remote_host)
return remotes
def _freebsd_remotes_on(port, which_end): # pragma: no cover
"""
Returns set of ipv4 host addresses of remote established connections
on local tcp port port.
Parses output of shell 'sockstat' (FreeBSD)
to get connections
$ sudo sockstat -4
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
root python2.7 1456 29 tcp4 *:4505 *:*
root python2.7 1445 17 tcp4 *:4506 *:*
root python2.7 1294 14 tcp4 127.0.0.1:11813 127.0.0.1:4505
root python2.7 1294 41 tcp4 127.0.0.1:61115 127.0.0.1:4506
$ sudo sockstat -4 -c -p 4506
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
root python2.7 1294 41 tcp4 127.0.0.1:61115 127.0.0.1:4506
"""
port = int(port)
remotes = set()
try:
cmd = salt.utils.args.shlex_split(f"sockstat -4 -c -p {port}")
data = subprocess.check_output(cmd)
except subprocess.CalledProcessError as ex:
log.error('Failed "sockstat" with returncode = %s', ex.returncode)
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
chunks = line.split()
if not chunks:
continue
# ['root', 'python2.7', '1456', '37', 'tcp4',
# '127.0.0.1:4505-', '127.0.0.1:55703']
# print chunks
if "COMMAND" in chunks[1]:
continue # ignore header
if len(chunks) < 2:
continue
# sockstat -4 -c -p 4506 does this with high PIDs:
# USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
# salt-master python2.781106 35 tcp4 192.168.12.34:4506 192.168.12.45:60143
local = chunks[-2]
remote = chunks[-1]
lhost, lport = local.split(":")
rhost, rport = remote.split(":")
if which_end == "local" and int(lport) != port: # ignore if local port not port
continue
if (
which_end == "remote" and int(rport) != port
): # ignore if remote port not port
continue
remotes.add(rhost)
return remotes
def _netbsd_remotes_on(port, which_end): # pragma: no cover
"""
Returns set of ipv4 host addresses of remote established connections
on local tcp port port.
Parses output of shell 'sockstat' (NetBSD)
to get connections
$ sudo sockstat -4 -n
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
root python2.7 1456 29 tcp *.4505 *.*
root python2.7 1445 17 tcp *.4506 *.*
root python2.7 1294 14 tcp 127.0.0.1.11813 127.0.0.1.4505
root python2.7 1294 41 tcp 127.0.0.1.61115 127.0.0.1.4506
$ sudo sockstat -4 -c -n -p 4506
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
root python2.7 1294 41 tcp 127.0.0.1.61115 127.0.0.1.4506
"""
port = int(port)
remotes = set()
try:
cmd = salt.utils.args.shlex_split(f"sockstat -4 -c -n -p {port}")
data = subprocess.check_output(cmd)
except subprocess.CalledProcessError as ex:
log.error('Failed "sockstat" with returncode = %s', ex.returncode)
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
chunks = line.split()
if not chunks:
continue
# ['root', 'python2.7', '1456', '37', 'tcp',
# '127.0.0.1.4505-', '127.0.0.1.55703']
# print chunks
if "COMMAND" in chunks[1]:
continue # ignore header
if len(chunks) < 2:
continue
local = chunks[5].split(".")
lport = local.pop()
lhost = ".".join(local)
remote = chunks[6].split(".")
rport = remote.pop()
rhost = ".".join(remote)
if which_end == "local" and int(lport) != port: # ignore if local port not port
continue
if (
which_end == "remote" and int(rport) != port
): # ignore if remote port not port
continue
remotes.add(rhost)
return remotes
def _openbsd_remotes_on(port, which_end): # pragma: no cover
"""
OpenBSD specific helper function.
Returns set of ipv4 host addresses of remote established connections
on local or remote tcp port.
Parses output of shell 'netstat' to get connections
$ netstat -nf inet
Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp 0 0 10.0.0.101.4505 10.0.0.1.45329 ESTABLISHED
tcp 0 0 10.0.0.101.4505 10.0.0.100.50798 ESTABLISHED
"""
remotes = set()
try:
data = subprocess.check_output(["netstat", "-nf", "inet"])
except subprocess.CalledProcessError as exc:
log.error('Failed "netstat" with returncode = %s', exc.returncode)
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
if "ESTABLISHED" not in line:
continue
chunks = line.split()
local_host, local_port = chunks[3].rsplit(".", 1)
remote_host, remote_port = chunks[4].rsplit(".", 1)
if which_end == "remote_port" and int(remote_port) != port:
continue
if which_end == "local_port" and int(local_port) != port:
continue
remotes.add(remote_host)
return remotes
def _windows_remotes_on(port, which_end):
r"""
Windows specific helper function.
Returns set of ipv4 host addresses of remote established connections
on local or remote tcp port.
Parses output of shell 'netstat' to get connections
C:\>netstat -n
Active Connections
Proto Local Address Foreign Address State
TCP 10.2.33.17:3007 130.164.12.233:10123 ESTABLISHED
TCP 10.2.33.17:3389 130.164.30.5:10378 ESTABLISHED
"""
remotes = set()
try:
data = subprocess.check_output(["netstat", "-n"])
except subprocess.CalledProcessError:
log.error("Failed netstat")
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
if "ESTABLISHED" not in line:
continue
chunks = line.split()
local_host, local_port = chunks[1].rsplit(":", 1)
remote_host, remote_port = chunks[2].rsplit(":", 1)
if which_end == "remote_port" and int(remote_port) != port:
continue
if which_end == "local_port" and int(local_port) != port:
continue
remotes.add(remote_host)
return remotes
def _linux_remotes_on(port, which_end):
"""
Linux specific helper function.
Returns set of ip host addresses of remote established connections
on local tcp port port.
Parses output of shell 'lsof'
to get connections
$ sudo lsof -iTCP:4505 -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 9971 root 35u IPv4 0x18a8464a29ca329d 0t0 TCP *:4505 (LISTEN)
Python 9971 root 37u IPv4 0x18a8464a29b2b29d 0t0 TCP 127.0.0.1:4505->127.0.0.1:55703 (ESTABLISHED)
Python 10152 root 22u IPv4 0x18a8464a29c8cab5 0t0 TCP 127.0.0.1:55703->127.0.0.1:4505 (ESTABLISHED)
Python 10153 root 22u IPv4 0x18a8464a29c8cab5 0t0 TCP [fe80::249a]:4505->[fe80::150]:59367 (ESTABLISHED)
"""
remotes = set()
lsof_binary = shutil.which("lsof")
if lsof_binary is None:
return remotes
try:
data = subprocess.check_output(
[
lsof_binary,
f"-iTCP:{port:d}",
"-n",
"-P",
]
)
except subprocess.CalledProcessError as ex:
if ex.returncode == 1:
# Lsof return 1 if any error was detected, including the failure
# to locate Internet addresses, and it is not an error in this case.
log.warning('"lsof" returncode = 1, likely no active TCP sessions.')
return remotes
log.error('Failed "lsof" with returncode = %s', ex.returncode)
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
chunks = line.split()
if not chunks:
continue
# ['Python', '9971', 'root', '37u', 'IPv4', '0x18a8464a29b2b29d', '0t0',
# 'TCP', '127.0.0.1:4505->127.0.0.1:55703', '(ESTABLISHED)']
# print chunks
if "COMMAND" in chunks[0]:
continue # ignore header
if "ESTABLISHED" not in chunks[-1]:
continue # ignore if not ESTABLISHED
# '127.0.0.1:4505->127.0.0.1:55703'
local, remote = chunks[8].split("->")
_, lport = local.rsplit(":", 1)
rhost, rport = remote.rsplit(":", 1)
if which_end == "remote_port" and int(rport) != port:
continue
if which_end == "local_port" and int(lport) != port:
continue
remotes.add(rhost.strip("[]"))
return remotes
def _aix_remotes_on(port, which_end): # pragma: no cover
"""
AIX specific helper function.
Returns set of ipv4 host addresses of remote established connections
on local or remote tcp port.
Parses output of shell 'netstat' to get connections
root@la68pp002_pub:/opt/salt/lib/python2.7/site-packages/salt/modules# netstat -f inet -n
Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 172.29.149.95.50093 209.41.78.13.4505 ESTABLISHED
tcp4 0 0 127.0.0.1.9514 *.* LISTEN
tcp4 0 0 127.0.0.1.9515 *.* LISTEN
tcp4 0 0 127.0.0.1.199 127.0.0.1.32779 ESTABLISHED
tcp4 0 0 127.0.0.1.32779 127.0.0.1.199 ESTABLISHED
tcp4 0 40 172.29.149.95.22 172.29.96.83.41022 ESTABLISHED
tcp4 0 0 172.29.149.95.22 172.29.96.83.41032 ESTABLISHED
tcp4 0 0 127.0.0.1.32771 127.0.0.1.32775 ESTABLISHED
tcp 0 0 127.0.0.1.32775 127.0.0.1.32771 ESTABLISHED
tcp4 0 0 127.0.0.1.32771 127.0.0.1.32776 ESTABLISHED
tcp 0 0 127.0.0.1.32776 127.0.0.1.32771 ESTABLISHED
tcp4 0 0 127.0.0.1.32771 127.0.0.1.32777 ESTABLISHED
tcp 0 0 127.0.0.1.32777 127.0.0.1.32771 ESTABLISHED
tcp4 0 0 127.0.0.1.32771 127.0.0.1.32778 ESTABLISHED
tcp 0 0 127.0.0.1.32778 127.0.0.1.32771 ESTABLISHED
"""
remotes = set()
try:
data = subprocess.check_output(["netstat", "-f", "inet", "-n"])
except subprocess.CalledProcessError:
log.error("Failed netstat")
raise
lines = salt.utils.stringutils.to_str(data).split("\n")
for line in lines:
if "ESTABLISHED" not in line:
continue
chunks = line.split()
local_host, local_port = chunks[3].rsplit(".", 1)
remote_host, remote_port = chunks[4].rsplit(".", 1)
if which_end == "remote_port" and int(remote_port) != port:
continue
if which_end == "local_port" and int(local_port) != port:
continue
remotes.add(remote_host)
return remotes
@jinja_filter("gen_mac")
def gen_mac(prefix="AC:DE:48"):
"""
Generates a MAC address with the defined OUI prefix.
Common prefixes:
- ``00:16:3E`` -- Xen
- ``00:18:51`` -- OpenVZ
- ``00:50:56`` -- VMware (manually generated)
- ``52:54:00`` -- QEMU/KVM
- ``AC:DE:48`` -- PRIVATE
References:
- http://standards.ieee.org/develop/regauth/oui/oui.txt
- https://www.wireshark.org/tools/oui-lookup.html
- https://en.wikipedia.org/wiki/MAC_address
"""
return "{}:{:02X}:{:02X}:{:02X}".format(
prefix,
random.randint(0, 0xFF),
random.randint(0, 0xFF),
random.randint(0, 0xFF),
)
@jinja_filter("mac_str_to_bytes")
def mac_str_to_bytes(mac_str):
"""
Convert a MAC address string into bytes. Works with or without separators:
b1 = mac_str_to_bytes('08:00:27:13:69:77')
b2 = mac_str_to_bytes('080027136977')
assert b1 == b2
assert isinstance(b1, bytes)
"""
if len(mac_str) == 12:
pass
elif len(mac_str) == 17:
sep = mac_str[2]
mac_str = mac_str.replace(sep, "")
else:
raise ValueError("Invalid MAC address")
chars = (int(mac_str[s : s + 2], 16) for s in range(0, 12, 2))
return bytes(chars)
def refresh_dns():
"""
issue #21397: force glibc to re-read resolv.conf
"""
try:
RES_INIT()
except NameError:
# Exception raised loading the library, thus RES_INIT is not defined
pass
@jinja_filter("dns_check")
def dns_check(addr, port, safe=False, ipv6=None):
"""
Return an ip address resolved by dns in a format usable in URLs (ipv6 in brackets).
Obeys system preference for IPv4/6 address resolution - this can be overridden by
the ipv6 flag. Tries to connect to the address before considering it useful. If no
address can be reached, the first one resolved is used as a fallback.
Does not exit on failure, raises an exception.
"""
ip_addrs = []
family = (
socket.AF_INET6
if ipv6
else socket.AF_INET if ipv6 is False else socket.AF_UNSPEC
)
socket_error = False
try:
refresh_dns()
addrinfo = socket.getaddrinfo(addr, port, family, socket.SOCK_STREAM)
ip_addrs = _test_addrs(addrinfo, port)
except TypeError:
raise SaltSystemExit(
code=42,
msg=(
"Attempt to resolve address '{}' failed. Invalid or unresolveable"
" address".format(addr)
),
)
except OSError:
socket_error = True
# If ipv6 is set to True, attempt another lookup using the IPv4 family,
# just in case we're attempting to lookup an IPv4 IP
# as an IPv6 hostname.
if socket_error and ipv6:
try:
refresh_dns()
addrinfo = socket.getaddrinfo(
addr, port, socket.AF_INET, socket.SOCK_STREAM
)
ip_addrs = _test_addrs(addrinfo, port)
except TypeError:
raise SaltSystemExit(
code=42,
msg=(
"Attempt to resolve address '{}' failed. Invalid or unresolveable"
" address".format(addr)
),
)
except OSError:
error = True
if not ip_addrs:
err = f"DNS lookup or connection check of '{addr}' failed."
if safe:
log.error(err)
raise SaltClientError()
raise SaltSystemExit(code=42, msg=err)
return ip_bracket(ip_addrs[0])
def _test_addrs(addrinfo, port):
"""
Attempt to connect to all addresses, return one if it succeeds.
Otherwise, return all addrs.
"""
ip_addrs = []
# test for connectivity, short circuit on success
for a in addrinfo:
ip_family = a[0]
ip_addr = a[4][0]
if ip_addr in ip_addrs:
continue
ip_addrs.append(ip_addr)
s = None
try:
s = socket.socket(ip_family, socket.SOCK_STREAM)
s.settimeout(2)
s.connect((ip_addr, port))
ip_addrs = [ip_addr]
break
except OSError:
pass
finally:
if s is not None:
s.close()
return ip_addrs
def parse_host_port(host_port):
"""
Takes a string argument specifying host or host:port.
Returns a (hostname, port) or (ip_address, port) tuple. If no port is given,
the second (port) element of the returned tuple will be None.
host:port argument, for example, is accepted in the forms of:
- hostname
- hostname:1234
- hostname.domain.tld
- hostname.domain.tld:5678
- [1234::5]:5678
- 1234::5
- 10.11.12.13:4567
- 10.11.12.13
"""
host, port = None, None # default
_s_ = host_port[:]
if _s_[0] == "[":
if "]" in host_port:
host, _s_ = _s_.lstrip("[").rsplit("]", 1)
host = ipaddress.IPv6Address(host).compressed
if _s_[0] == ":":
port = int(_s_.lstrip(":"))
else:
if len(_s_) > 1:
raise ValueError(f'found ambiguous "{_s_}" port in "{host_port}"')
else:
if _s_.count(":") == 1:
host, _hostport_separator_, port = _s_.partition(":")
try:
port = int(port)
except ValueError as _e_:
errmsg = 'host_port "{}" port value "{}" is not an integer.'.format(
host_port, port
)
log.error(errmsg)
raise ValueError(errmsg)
else:
host = _s_
try:
if not isinstance(host, ipaddress._BaseAddress):
host_ip = ipaddress.ip_address(host).compressed
host = host_ip
except ValueError:
log.debug('"%s" Not an IP address? Assuming it is a hostname.', host)
if host != sanitize_host(host):
log.error('bad hostname: "%s"', host)
raise ValueError(f'bad hostname: "{host}"')
return host, port
@jinja_filter("filter_by_networks")
def filter_by_networks(values, networks):
"""
Returns the list of IPs filtered by the network list.
If the network list is an empty sequence, no IPs are returned.
If the network list is None, all IPs are returned.
{% set networks = ['192.168.0.0/24', 'fe80::/64'] %}
{{ grains['ip_interfaces'] | filter_by_networks(networks) }}
{{ grains['ipv6'] | filter_by_networks(networks) }}
{{ grains['ipv4'] | filter_by_networks(networks) }}
"""
def _filter(ips, networks):
return [ip for ip in ips for net in networks if ipaddress.ip_address(ip) in net]
if networks is not None:
networks = [ipaddress.ip_network(network) for network in networks]
if isinstance(values, Mapping):
return {
interface: _filter(values[interface], networks) for interface in values
}
elif isinstance(values, Sequence):
return _filter(values, networks)
else:
raise ValueError(f"Do not know how to filter a {type(values)}")
else:
return values
@jinja_filter("ipwrap")
def ipwrap(data):
"""
Returns any input (string, list, tuple) as a string or a list with any IPv6 addresses wrapped in square brackets ([]).
"""
if isinstance(data, (list, tuple)):
ret = []
for element in data:
if _is_ipv(element, 6, options=None):
element = ip_bracket(element)
ret.append(element)
else:
if _is_ipv(data, 6, options=None):
data = ip_bracket(data)
ret = data
return ret
def ip_bracket(addr, strip=False):
"""
Ensure IP addresses are URI-compatible - specifically, add brackets
around IPv6 literals if they are not already present.
"""
addr = str(addr)
addr = addr.lstrip("[")
addr = addr.rstrip("]")
addr = ipaddress.ip_address(addr)
return ("[{}]" if addr.version == 6 and not strip else "{}").format(addr)
Zerion Mini Shell 1.0