salt/salt/utils/network.py
Sean Brennan 69fe65b550
Use rsplit
Fixes exception thrown on ipv6 addresses

ValueError: invalid literal for int() with base 10: ':ffff:1.2.3.4:5678'
2019-05-06 10:10:05 -04:00

2018 lines
66 KiB
Python

# -*- coding: utf-8 -*-
'''
Define some generic socket functions for network modules
'''
# Import python libs
from __future__ import absolute_import, unicode_literals, print_function
import itertools
import os
import re
import types
import socket
import logging
import platform
import random
import subprocess
from string import ascii_letters, digits
# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
# Attempt to import wmi
try:
import wmi
import salt.utils.winapi
except ImportError:
pass
# Import salt libs
import salt.utils.args
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.zeromq
from salt._compat import ipaddress
from salt.exceptions import SaltClientError, SaltSystemExit
from salt.utils.decorators.jinja import jinja_filter
from salt.utils.versions import LooseVersion
# inet_pton does not exist in Windows, this is a workaround
if salt.utils.platform.is_windows():
from salt.ext import win_inet_pton # pylint: disable=unused-import
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
# pylint: disable=C0103
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:
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(self.__class__, self).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 {addr} info via socket: {message}'.format(
addr=hosts.first() or 'localhost (N/A)', message=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 IOError:
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]
if len(info) >= 4:
l.append(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:
log.debug('salt.utils.network.ip_to_host(%r) failed: %s', ip, exc)
hostname = None
return hostname
# pylint: enable=C0103
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):
'''
Returns a bool telling if the passed IP is a valid IPv4 or IPv6 address.
'''
return is_ipv4(ip) or is_ipv6(ip)
def is_ipv4(ip):
'''
Returns a bool telling if the value passed to it was a valid IPv4 address
'''
try:
return ipaddress.ip_address(ip).version == 4
except ValueError:
return False
def is_ipv6(ip):
'''
Returns a bool telling if the value passed to it was a valid IPv6 address
'''
try:
return ipaddress.ip_address(ip).version == 6
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:
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:
return False
@jinja_filter('is_ip')
def is_ip_filter(ip, options=None):
'''
Returns a bool telling if the passed IP is a valid IPv4 or IPv6 address.
'''
return is_ipv4_filter(ip, options=options) or is_ipv6_filter(ip, 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 six.text_type(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 six.text_type(ip_obj)
def _is_ipv(ip, version, options=None):
if not version:
version = 4
if version not in (4, 6):
return None
try:
ip_obj = ipaddress.ip_address(ip)
except ValueError:
# maybe it is an IP network
try:
ip_obj = ipaddress.ip_interface(ip)
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, 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, 4, options=options)
return isinstance(_is_ipv4, six.string_types)
@jinja_filter('is_ipv6')
def is_ipv6_filter(ip, 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, 6, options=options)
return isinstance(_is_ipv6, six.string_types)
def _ipv_filter(value, version, options=None):
if version not in (4, 6):
return
if isinstance(value, (six.string_types, six.text_type, six.binary_type)):
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 six.text_type(ipaddress.ip_interface(ipaddr_filter_out[0]))
return [six.text_type(ipaddress.ip_interface(ip_a)) for ip_a in ipaddr_filter_out]
def _network_hosts(ip_addr_entry):
return [
six.text_type(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, fmt='prefixlen'):
'''
Returns the "natural" mask of an IPv4 address
'''
bits = _ipv4_to_bits(ip)
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):
'''
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.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 += '{0:d}'.format(256 - (2 ** (8 - cidr_bits)))
cidr_bits = 0
return netmask
def _number_of_set_bits_to_ipv4_netmask(set_bits): # pylint: disable=C0103
'''
Returns an IPv4 netmask from the integer representation of that mask.
Ex. 0xffffff00 -> '255.255.255.0'
'''
return cidr_to_ipv4_netmask(_number_of_set_bits(set_bits))
# pylint: disable=C0103
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
# pylint: enable=C0103
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('/') # pylint: disable=C0103
else:
ip = value # pylint: disable=C0103
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'):
if 'secondary' not in cols:
ipaddr, netmask, broadcast, scope = parse_network(value, cols)
if type_ == 'inet':
if 'inet' not in data:
data['inet'] = list()
addr_obj = dict()
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 = dict()
addr_obj['address'] = ipaddr
addr_obj['prefixlen'] = netmask
addr_obj['scope'] = scope
data['inet6'].append(addr_obj)
else:
if 'secondary' not in data:
data['secondary'] = list()
ip_, mask, brd, scp = parse_network(value, cols)
data['secondary'].append({
'type': type_,
'address': ip_,
'netmask': mask,
'broadcast': brd,
'label': iflabel,
})
del ip_, mask, brd, scp
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+))(?: 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('0{0}'.format(chunk) if len(chunk) < 2 else '{0}'.format(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 it's 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(
'{0} link show'.format(ip_path),
shell=True,
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT).communicate()[0]
cmd2 = subprocess.Popen(
'{0} addr show'.format(ip_path),
shell=True,
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT).communicate()[0]
ifaces = _interfaces_ip("{0}\n{1}".format(
salt.utils.stringutils.to_str(cmd1),
salt.utils.stringutils.to_str(cmd2)))
elif ifconfig_path:
cmd = subprocess.Popen(
'{0} -a'.format(ifconfig_path),
shell=True,
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 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 LooseVersion(os.uname()[2]) < LooseVersion('8.0'):
return linux_interfaces()
ifconfig_path = salt.utils.path.which('ifconfig')
cmd = subprocess.Popen(
'{0} -a'.format(ifconfig_path),
shell=True,
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
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 = None
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
'''
with salt.utils.winapi.Com():
c = wmi.WMI()
ifaces = {}
for iface in c.Win32_NetworkAdapterConfiguration(IPEnabled=1):
ifaces[iface.Description] = dict()
if iface.MACAddress:
ifaces[iface.Description]['hwaddr'] = iface.MACAddress
if iface.IPEnabled:
ifaces[iface.Description]['up'] = True
for ip in iface.IPAddress:
if '.' in ip:
if 'inet' not in ifaces[iface.Description]:
ifaces[iface.Description]['inet'] = []
item = {'address': ip,
'label': iface.Description}
if iface.DefaultIPGateway:
broadcast = next((i for i in iface.DefaultIPGateway if '.' in i), '')
if broadcast:
item['broadcast'] = broadcast
if iface.IPSubnet:
netmask = next((i for i in iface.IPSubnet if '.' in i), '')
if netmask:
item['netmask'] = netmask
ifaces[iface.Description]['inet'].append(item)
if ':' in ip:
if 'inet6' not in ifaces[iface.Description]:
ifaces[iface.Description]['inet6'] = []
item = {'address': ip}
if iface.DefaultIPGateway:
broadcast = next((i for i in iface.DefaultIPGateway if ':' in i), '')
if broadcast:
item['broadcast'] = broadcast
if iface.IPSubnet:
netmask = next((i for i in iface.IPSubnet if ':' in i), '')
if netmask:
item['netmask'] = netmask
ifaces[iface.Description]['inet6'].append(item)
else:
ifaces[iface.Description]['up'] = False
return ifaces
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_netbsd():
return netbsd_interfaces()
else:
return linux_interfaces()
def get_net_start(ipaddr, netmask):
'''
Return the address of the network
'''
net = ipaddress.ip_network('{0}/{1}'.format(ipaddr, netmask), strict=False)
return six.text_type(net.network_address)
def get_net_size(mask):
'''
Turns an IPv4 netmask into it's 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 = '{0}/{1}'.format(ipaddr, netmask)
return six.text_type(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 "{0}" not in available interfaces: "{1}"'
''.format(iface, '", "'.join(iface_info.keys())))
log.error(error_msg)
return None, error_msg
def _hw_addr_aix(iface):
'''
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(
'entstat -d {0} | grep \'Hardware Address\''.format(iface),
shell=True,
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 "{0}" 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_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 six.iteritems(interfaces()):
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 {0} calling subnets()'.format(proto))
return
for ip_info in six.itervalues(ifaces):
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('{0}/{1}'.format(intf['address'], intf[subnet]))
else:
intf = ipaddress.ip_interface('{0}/{1}'.format(intf['address'], dflt_cidr))
if not intf.is_loopback:
ret.add(intf.network)
return [six.text_type(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 _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
'''
ret = set()
ifaces = interface_data \
if isinstance(interface_data, dict) \
else interfaces()
if interface is None:
target_ifaces = ifaces
else:
target_ifaces = dict([(k, v) for k, v in six.iteritems(ifaces)
if k == interface])
if not target_ifaces:
log.error('Interface {0} not found.'.format(interface))
for ip_info in six.itervalues(target_ifaces):
addrs = ip_info.get(proto, [])
addrs.extend([addr for addr in ip_info.get('secondary', []) if addr.get('type') == proto])
for addr in addrs:
addr = ipaddress.ip_address(addr.get('address'))
if not addr.is_loopback or include_loopback:
ret.add(addr)
return [six.text_type(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 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 = []
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.append("{0[3]}{0[2]}:{0[1]}{0[0]}".format(ip_part))
else:
ip.append("{0[0]}{0[1]}:{0[2]}{0[3]}".format(ip_part))
try:
address = ipaddress.IPv6Address(":".join(ip))
if address.ipv4_mapped:
return str(address.ipv4_mapped)
else:
return address.compressed
except ipaddress.AddressValueError as ex:
log.error('hex2ip - ipv6 address error: {0}'.format(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 '{0}.{1}.{2}.{3}'.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('0x{0}'.format(eui64), 16)
return '{0}/{1}'.format(net[euil], net.prefixlen)
except Exception:
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 os.path.isfile(statf):
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)
sl = next(iter(iret))
if iret[sl]['state'] == 1: # 1 is ESTABLISHED
del iret[sl]['state']
ret[len(ret)] = iret[sl]
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 os.path.isfile(statf):
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)
sl = next(iter(iret))
if iret[sl][which_end] == port and iret[sl]['state'] == 1: # 1 is ESTABLISHED
ret.add(iret[sl]['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()
sl = comps[0].rstrip(':')
ret[sl] = {}
l_addr, l_port = comps[1].split(':')
r_addr, r_port = comps[2].split(':')
ret[sl]['local_addr'] = hex2ip(l_addr, True)
ret[sl]['local_port'] = int(l_port, 16)
ret[sl]['remote_addr'] = hex2ip(r_addr, True)
ret[sl]['remote_port'] = int(r_port, 16)
ret[sl]['state'] = int(comps[3], 16)
return ret
def _netlink_tool_remote_on(port, which_end):
'''
Returns set of ipv4 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
'''
remotes = set()
valid = False
try:
data = subprocess.check_output(['ss', '-ant']) # pylint: disable=minimum-python-version
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) != port:
continue
if which_end == 'local_port' and int(local_port) != port:
continue
remotes.add(remote_host)
if valid is False:
remotes = None
return remotes
def _sunos_remotes_on(port, which_end):
'''
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']) # pylint: disable=minimum-python-version
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):
'''
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('sockstat -4 -c -p {0}'.format(port))
data = subprocess.check_output(cmd) # pylint: disable=minimum-python-version
except subprocess.CalledProcessError as ex:
log.error('Failed "sockstat" with returncode = {0}'.format(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):
'''
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('sockstat -4 -c -n -p {0}'.format(port))
data = subprocess.check_output(cmd) # pylint: disable=minimum-python-version
except subprocess.CalledProcessError as ex:
log.error('Failed "sockstat" with returncode = {0}'.format(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):
'''
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']) # pylint: disable=minimum-python-version
except subprocess.CalledProcessError:
log.error('Failed netstat')
raise
lines = 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']) # pylint: disable=minimum-python-version
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()
try:
data = subprocess.check_output(
['lsof', '-iTCP:{0:d}'.format(port), '-n', '-P'] # pylint: disable=minimum-python-version
)
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 = {0}'.format(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):
'''
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']) # pylint: disable=minimum-python-version
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 '{0}:{1:02X}:{2:02X}:{3: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) if six.PY3 else b''.join(chr(x) for x in 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 the ip resolved by dns, but do not exit on failure, only raise an
exception. 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.
'''
error = False
lookup = addr
seen_ipv6 = False
family = socket.AF_INET6 if ipv6 else socket.AF_INET if ipv6 is False else socket.AF_UNSPEC
try:
refresh_dns()
hostnames = socket.getaddrinfo(addr, port, family, socket.SOCK_STREAM)
if not hostnames:
error = True
else:
resolved = False
candidates = []
for h in hostnames:
# Input is IP address, passed through unchanged, just return it
if h[4][0] == addr:
resolved = salt.utils.zeromq.ip_bracket(addr)
break
if h[0] == socket.AF_INET and ipv6 is True:
continue
if h[0] == socket.AF_INET6 and ipv6 is False:
continue
candidate_addr = h[4][0]
if h[0] != socket.AF_INET6 or ipv6 is not None:
candidates.append(candidate_addr)
try:
s = socket.socket(h[0], socket.SOCK_STREAM)
s.connect((candidate_addr, port))
s.close()
resolved = candidate_addr
break
except socket.error:
pass
if not resolved:
if len(candidates) > 0:
resolved = candidates[0]
else:
error = True
except TypeError:
err = ('Attempt to resolve address \'{0}\' failed. Invalid or unresolveable address').format(lookup)
raise SaltSystemExit(code=42, msg=err)
except socket.error:
error = True
if error:
err = ('DNS lookup or connection check of \'{0}\' failed.').format(addr)
if safe:
if salt.log.is_console_configured():
# If logging is not configured it also means that either
# the master or minion instance calling this hasn't even
# started running
log.error(err)
raise SaltClientError()
raise SaltSystemExit(code=42, msg=err)
return resolved
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('found ambiguous "{}" port in "{}"'.format(_s_, host_port))
else:
if _s_.count(":") == 1:
host, _hostport_separator_, port = _s_.partition(":")
try:
port = int(port)
except ValueError as _e_:
log.error('host_port "%s" port value "%s" is not an integer.', host_port, port)
raise _e_
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('bad hostname: "{}"'.format(host))
return host, port