network/library/network_connections.py

2705 lines
105 KiB
Python
Executable file

#!/usr/bin/python
# -*- coding: utf-8 -*-
DOCUMENTATION='''
---
module: network_connections
author: "Thomas Haller (thaller@redhat.com)"
short_description: module for network role to manage connection profiles
requirements: for 'nm' provider requires pygobject, dbus and NetworkManager.
version_added: "2.0"
description: Manage networking profiles (connections) for NetworkManager and initscripts
networking providers.
options: Documentation needs to be written. Note that the network_connections module
tightly integrates with the network role and currently it is not expected to use
this module outside the role. Thus, consult README.md for examples for the role.
'''
import socket
import sys
import traceback
import os
###############################################################################
class CheckMode:
PREPARE = 'prepare'
DRY_RUN = 'dry-run'
PRE_RUN = 'pre-run'
REAL_RUN = 'real-run'
DONE = 'done'
class LogLevel:
ERROR = 'error'
WARN = 'warn'
INFO = 'info'
DEBUG = 'debug'
@staticmethod
def fmt(level):
return '<%-6s' % (str(level) + '>')
class MyError(Exception):
pass
class ValidationError(MyError):
def __init__(self, name, message):
Exception.__init__(self, name + ': ' + message)
self.error_message = message
self.name = name
@staticmethod
def from_connection(idx, message):
return ValidationError('connection[' + str(idx) + ']', message)
class Util:
PY3 = (sys.version_info[0] == 3)
STRING_TYPE = (str if PY3 else basestring)
@staticmethod
def first(iterable, default = None, pred = None):
for v in iterable:
if pred is None or pred(v):
return v
return default
@staticmethod
def check_output(argv, lang = None):
# subprocess.check_output is python 2.7.
with open('/dev/null', 'wb') as DEVNULL:
import subprocess
ev = os.environ.copy()
ev['LANG'] = lang if lang is not None else 'C'
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=DEVNULL, env=ev)
out = p.communicate()[0]
if p.returncode != 0:
raise MyError('failure calling %s: exit with %s' % (argv, p.returncode))
return out
@classmethod
def create_uuid(cls):
cls.NM()
return str(cls._uuid.uuid4())
@classmethod
def NM(cls):
n = getattr(cls, '_NM', None)
if n is None:
import gi
gi.require_version('NM', '1.0')
from gi.repository import NM, GLib, Gio, GObject
cls._NM = NM
cls._GLib = GLib
cls._Gio = Gio
cls._GObject = GObject
n = NM
import uuid
cls._uuid = uuid
return n
@classmethod
def GLib(cls):
cls.NM()
return cls._GLib
@classmethod
def Gio(cls):
cls.NM()
return cls._Gio
@classmethod
def GObject(cls):
cls.NM()
return cls._GObject
@classmethod
def Timestamp(cls):
return cls.GLib().get_monotonic_time()
@classmethod
def GMainLoop(cls):
gmainloop = getattr(cls, '_GMainLoop', None)
if gmainloop is None:
gmainloop = cls.GLib().MainLoop()
cls._GMainLoop = gmainloop
return gmainloop
@classmethod
def GMainLoop_run(cls, timeout = None):
if timeout is None:
cls.GMainLoop().run()
return True
GLib = cls.GLib()
result = []
loop = cls.GMainLoop()
def _timeout_cb(unused):
result.append(1)
loop.quit()
return False
timeout_id = GLib.timeout_add(int(timeout * 1000), _timeout_cb, None)
loop.run()
if result:
return False
GLib.source_remove(timeout_id)
return True
@classmethod
def GMainLoop_iterate(cls, may_block = False):
return cls.GMainLoop().get_context().iteration(may_block)
@classmethod
def GMainLoop_iterate_all(cls):
c = 0
while cls.GMainLoop_iterate():
c += 1
return c
@classmethod
def create_cancellable(cls):
return cls.Gio().Cancellable.new()
@classmethod
def error_is_cancelled(cls, e):
GLib = cls.GLib()
if isinstance(e, GLib.GError):
if e.domain == 'g-io-error-quark' and e.code == cls.Gio().IOErrorEnum.CANCELLED:
return True
return False
@staticmethod
def ifname_valid(ifname):
# see dev_valid_name() in kernel's net/core/dev.c
if not ifname:
return False
if ifname in [ '.', '..' ]:
return False
if len(ifname) >= 16:
return False
if any([c == '/' or c == ':' or c.isspace() for c in ifname]):
return False
# FIXME: encoding issues regarding python unicode string
return True
@staticmethod
def mac_aton(mac_str, force_len=None):
# we also accept None and '' for convenience.
# - None yiels None
# - '' yields []
if mac_str is None:
return mac_str
i = 0
b = []
for c in mac_str:
if i == 2:
if c != ':':
raise MyError('not a valid MAC address: "%s"' % (mac_str))
i = 0
continue
try:
if i == 0:
n = int(c, 16) * 16
i = 1
else:
assert(i == 1)
n = n + int(c, 16)
i = 2
b.append(n)
except:
raise MyError('not a valid MAC address: "%s"' % (mac_str))
if i == 1:
raise MyError('not a valid MAC address: "%s"' % (mac_str))
if force_len is not None:
if force_len != len(b):
raise MyError('not a valid MAC address of length %s: "%s"' % (force_len, mac_str))
return b
@staticmethod
def mac_ntoa(mac):
if mac is None:
return None
return ':'.join(['%02x' % c for c in mac])
@staticmethod
def mac_norm(mac_str, force_len = None):
return Util.mac_ntoa(Util.mac_aton(mac_str, force_len))
@staticmethod
def boolean(arg):
BOOLEANS_TRUE = ['y', 'yes', 'on', '1', 'true', 1, True]
BOOLEANS_FALSE = ['n', 'no', 'off', '0', 'false', 0, False]
if arg is None or isinstance(arg, bool):
return arg
if isinstance(arg, Util.STRING_TYPE):
arg = arg.lower()
if arg in BOOLEANS_TRUE:
return True
elif arg in BOOLEANS_FALSE:
return False
else:
raise MyError('value "%s" is not a boolean' % (arg))
@staticmethod
def parse_ip(addr, family=None):
if addr is None:
return (None, None)
if family is not None:
Util.addr_family_check(family)
a = socket.inet_pton(family, addr)
else:
a = None
family = None
try:
a = socket.inet_pton(socket.AF_INET, addr)
family = socket.AF_INET
except:
a = socket.inet_pton(socket.AF_INET6, addr)
family = socket.AF_INET6
return (socket.inet_ntop(family, a), family)
@staticmethod
def addr_family_check(family):
if family != socket.AF_INET and family != socket.AF_INET6:
raise MyError('invalid address family %s' % (family))
@staticmethod
def addr_family_to_v(family):
if family is None:
return ''
if family == socket.AF_INET:
return 'v4'
if family == socket.AF_INET6:
return 'v6'
raise MyError('invalid address family "%s"' % (family))
@staticmethod
def addr_family_default_prefix(family):
Util.addr_family_check(family)
if family == socket.AF_INET:
return 24
else:
return 64
@staticmethod
def addr_family_valid_prefix(family, prefix):
Util.addr_family_check(family)
if family == socket.AF_INET:
m = 32
else:
m = 128
return prefix >= 0 and prefix <= m
@staticmethod
def parse_address(address, family = None):
try:
parts = address.split()
addr_parts = parts[0].split('/')
if len(addr_parts) != 2:
raise MyError('expect two addr-parts: ADDR/PLEN')
a, family = Util.parse_ip(addr_parts[0], family)
prefix = int(addr_parts[1])
if not Util.addr_family_valid_prefix(family, prefix):
raise MyError('invalid prefix %s' % (prefix))
if len(parts) > 1:
raise MyError('too many parts')
return {
'address': a,
'family': family,
'prefix': prefix,
}
except Exception as e:
raise MyError('invalid address "%s"' % (address))
###############################################################################
class SysUtil:
@staticmethod
def _sysctl_read(filename):
try_count = 0
while True:
try_count += 1
try:
with open(filename, 'r') as f:
return f.read()
except Exception as e:
if try_count < 5:
continue
raise
@staticmethod
def _link_read_ifindex(ifname):
c = SysUtil._sysctl_read('/sys/class/net/' + ifname + '/ifindex')
return int(c.strip())
@staticmethod
def _link_read_address(ifname):
c = SysUtil._sysctl_read('/sys/class/net/' + ifname + '/address')
return Util.mac_norm(c.strip())
@staticmethod
def _link_read_permaddress(ifname):
try:
out = Util.check_output(['ethtool', '-P', ifname])
except MyError as e:
return None
import re
m = re.match('^Permanent address: ([0-9A-Fa-f:]*)\n$', out)
if not m:
return None
return Util.mac_norm(m.group(1))
@staticmethod
def _link_infos_fetch():
links = {}
for ifname in os.listdir('/sys/class/net/'):
if not os.path.islink('/sys/class/net/' + ifname):
# /sys/class/net may contain certain entries that are not
# interface names, like 'bonding_master'. Skip over files
# that are not links.
continue
links[ifname] = {
'ifindex': SysUtil._link_read_ifindex(ifname),
'ifname': ifname,
'address': SysUtil._link_read_address(ifname),
'perm-address': SysUtil._link_read_permaddress(ifname),
}
return links
@classmethod
def link_infos(cls, refresh=False):
if refresh:
l = None
else:
l = getattr(cls, '_link_infos', None)
if l is None:
try_count = 0
b = None
while True:
try:
# there is a race in that we lookup properties by ifname
# and interfaces can be renamed. Try to avoid that by fetching
# the info twice and repeat until we get the same result.
if b is None:
b = SysUtil._link_infos_fetch()
l = SysUtil._link_infos_fetch()
if l != b:
b = l
raise Exception('cannot read stable link-infos. They keep changing')
except:
if try_count < 50:
raise
continue
break
cls._link_infos = l
return l
@classmethod
def link_info_find(cls, refresh = False, mac = None, ifname = None):
if mac is not None:
mac = Util.mac_norm(mac)
for li in cls.link_infos(refresh).values():
if mac is not None and mac not in [li.get('perm-address', None), li.get('address', None)]:
continue
if ifname is not None and ifname != li.get('ifname', None):
continue
return li
return None
###############################################################################
class ArgUtil:
@staticmethod
def connection_find_by_name(name, connections, n_connections = None):
if not name:
raise ValueError('missing name argument')
c = None
for idx, connection in enumerate(connections):
if n_connections is not None and idx >= n_connections:
break
if 'name' not in connection or name != connection['name']:
continue
if connection['state'] == 'absent':
c = None
elif 'type' in connection:
assert connection['state'] in ['up', 'present']
c = connection
return c
@staticmethod
def connection_find_master(name, connections, n_connections = None):
c = ArgUtil.connection_find_by_name(name, connections, n_connections)
if not c:
raise MyError('invalid master/parent "%s"' % (name))
if c['interface_name'] is None:
raise MyError('invalid master/parent "%s" which needs an "interface_name"' % (name))
if not Util.ifname_valid(c['interface_name']):
raise MyError('invalid master/parent "%s" which has not a valid "interface_name" ("%s")' % (name, c['interface_name']))
return c['interface_name']
@staticmethod
def connection_find_master_uuid(name, connections, n_connections = None):
c = ArgUtil.connection_find_by_name(name, connections, n_connections)
if not c:
raise MyError('invalid master/parent "%s"' % (name))
return c['nm.uuid']
@staticmethod
def connection_get_non_absent_names(connections):
# @idx is the index with state['absent']. This will
# return the names of all explicitly mentioned profiles.
# That is, the names of profiles that should not be deleted.
l = set()
for connection in connections:
if 'name' not in connection:
continue
if not connection['name']:
continue
l.add(connection['name'])
return l
class ArgValidator:
MISSING = object()
def __init__(self, name = None, required = False, default_value = None):
self.name = name
self.required = required
self.default_value = default_value
def get_default_value(self):
try:
return self.default_value()
except:
return self.default_value
def validate(self, value, name = None):
name = name or self.name or ''
v = self._validate(value, name)
return self._validate_post(value, name, v)
def _validate_post(self, value, name, result):
return result
class ArgValidatorStr(ArgValidator):
def __init__(self, name, required = False, default_value = None, enum_values = None, allow_empty = False):
ArgValidator.__init__(self, name, required, default_value)
self.enum_values = enum_values
self.allow_empty = allow_empty
def _validate(self, value, name):
if not isinstance(value, Util.STRING_TYPE):
raise ValidationError(name, 'must be a string but is "%s"' % (value))
v = str(value)
if self.enum_values is not None and v not in self.enum_values:
raise ValidationError(name, 'is "%s" but must be one of "%s"' % (value, '" "'.join(sorted(self.enum_values))))
if not self.allow_empty and not v:
raise ValidationError(name, 'cannot be empty')
return v
class ArgValidatorNum(ArgValidator):
def __init__(self, name, required = False, val_min = None, val_max = None,
default_value = ArgValidator.MISSING,
numeric_type = int):
ArgValidator.__init__(self, name, required, \
numeric_type(0) if default_value is ArgValidator.MISSING else default_value)
self.val_min = val_min
self.val_max = val_max
self.numeric_type = numeric_type
def _validate(self, value, name):
v = None
try:
if isinstance(value, self.numeric_type):
v = value
else:
v2 = self.numeric_type(value)
if isinstance(value, Util.STRING_TYPE) \
or v2 == value:
v = v2
except:
pass
if v is None:
raise ValidationError(name, 'must be an integer number but is "%s"' % (value))
if self.val_min is not None and v < self.val_min:
raise ValidationError(name, 'value is %s but cannot be less then %s' % (value, self.val_min))
if self.val_max is not None and v > self.val_max:
raise ValidationError(name, 'value is %s but cannot be greater then %s' % (value, self.val_max))
return v
class ArgValidatorBool(ArgValidator):
def __init__(self, name, required = False, default_value = False):
ArgValidator.__init__(self, name, required, default_value)
def _validate(self, value, name):
try:
if isinstance(value, bool):
return value
if isinstance(value, Util.STRING_TYPE) or isinstance(value, int):
return Util.boolean(value)
except:
pass
raise ValidationError(name, 'must be an boolean but is "%s"' % (value))
class ArgValidatorDict(ArgValidator):
def __init__(self, name = None, required = False, nested = None, default_value = None, all_missing_during_validate = False):
ArgValidator.__init__(self, name, required, default_value)
if nested is not None:
self.nested = dict([(v.name, v) for v in nested])
else:
self.nested = {}
self.all_missing_during_validate = all_missing_during_validate
def _validate(self, value, name):
result = {}
seen_keys = set()
try:
l = list(value.items())
except:
raise ValidationError(name, 'invalid content is not a dictionary')
for (k,v) in l:
if k in seen_keys:
raise ValidationError(name, 'duplicate key "%s"' % (k))
seen_keys.add(k)
validator = self.nested.get(k, None)
if validator is None:
raise ValidationError(name, 'invalid key "%s"' % (k))
try:
vv = validator.validate(v, name + '.' + k)
except ValidationError as e:
raise ValidationError(e.name, e.error_message)
result[k] = vv
for (k,v) in self.nested.items():
if k in seen_keys:
continue
if v.required:
raise ValidationError(name, 'missing required key "%s"' % (k))
vv = v.get_default_value()
if not self.all_missing_during_validate and vv is not ArgValidator.MISSING:
result[k] = vv
return result
class ArgValidatorList(ArgValidator):
def __init__(self, name, nested, default_value = None):
ArgValidator.__init__(self, name, required = False, default_value = default_value)
self.nested = nested
def construct_name(self, name):
return ('' if n is None else n) + name
def _validate(self, value, name):
if isinstance(value, Util.STRING_TYPE):
# we expect a list. However, for convenience allow to
# specify a string, separated by space. Escaping is
# not supported. If you need that, define a proper list.
value = [s for s in value.split(' ') if s]
result = []
for (idx, v) in enumerate(value):
try:
vv = self.nested.validate(v, name + '[' + str(idx) + ']')
except ValidationError as e:
raise ValidationError(e.name, e.error_message)
result.append(vv)
return result
class ArgValidatorIP(ArgValidatorStr):
def __init__(self, name, family = None, required = False, default_value = None, plain_address = True):
ArgValidatorStr.__init__(self, name, required, default_value, None)
self.family = family
self.plain_address = plain_address
def _validate(self, value, name):
v = ArgValidatorStr._validate(self, value, name)
try:
addr, family = Util.parse_ip(v, self.family)
except:
raise ValidationError(name, 'value "%s" is not a valid IP%s address' % (value, Util.addr_family_to_v(self.family)))
if self.plain_address:
return addr
return { 'family': family, 'address': addr }
class ArgValidatorMac(ArgValidatorStr):
def __init__(self, name, force_len = None, required = False, default_value = None):
ArgValidatorStr.__init__(self, name, required, default_value, None)
self.force_len = force_len
def _validate(self, value, name):
v = ArgValidatorStr._validate(self, value, name)
try:
addr = Util.mac_aton(v, self.force_len)
except MyError as e:
raise ValidationError(name, 'value "%s" is not a valid MAC address' % (value))
if not addr:
raise ValidationError(name, 'value "%s" is not a valid MAC address' % (value))
return Util.mac_ntoa(addr)
class ArgValidatorIPAddr(ArgValidatorDict):
def __init__(self, name, family = None, required = False, default_value = None):
ArgValidatorDict.__init__(self,
name,
required,
nested = [
ArgValidatorIP ('address', family = family, required = True, plain_address = False),
ArgValidatorNum('prefix', default_value = None, val_min = 0),
],
)
self.family = family
def _validate(self, value, name):
if isinstance(value, Util.STRING_TYPE):
v = str(value)
if not v:
raise ValidationError(name, 'cannot be empty')
try:
return Util.parse_address(v, self.family)
except:
raise ValidationError(name, 'value "%s" is not a valid IP%s address with prefix length' % (value, Util.addr_family_to_v(self.family)))
v = ArgValidatorDict._validate(self, value, name)
return {
'address': v['address']['address'],
'family': v['address']['family'],
'prefix': v['prefix'],
}
def _validate_post(self, value, name, result):
family = result['family']
prefix = result['prefix']
if prefix is None:
prefix = Util.addr_family_default_prefix(family)
result['prefix'] = prefix
elif not Util.addr_family_valid_prefix(family, prefix):
raise ValidationError(name, 'invalid prefix %s in "%s"' % (prefix, value))
return result
class ArgValidatorIPRoute(ArgValidatorDict):
def __init__(self, name, family = None, required = False, default_value = None):
ArgValidatorDict.__init__(self,
name,
required,
nested = [
ArgValidatorIP ('network', family = family, required = True, plain_address = False),
ArgValidatorNum('prefix', default_value = None, val_min = 0),
ArgValidatorIP ('gateway', family = family, default_value = None, plain_address = False),
ArgValidatorNum('metric', default_value = -1, val_min = -1, val_max = 0xFFFFFFFF),
],
)
self.family = family
def _validate_post(self, value, name, result):
network = result['network']
family = network['family']
result['network'] = network['address']
result['family'] = family
gateway = result['gateway']
if gateway is not None:
if family != gateway['family']:
raise ValidationError(name, 'conflicting address family between network and gateway \"%s\"' % (gateway['address']))
result['gateway'] = gateway['address']
prefix = result['prefix']
if prefix is None:
prefix = Util.addr_family_default_prefix(family)
result['prefix'] = prefix
elif not Util.addr_family_valid_prefix(family, prefix):
raise ValidationError(name, 'invalid prefix %s in "%s"' % (prefix, value))
return result
class ArgValidator_DictIP(ArgValidatorDict):
def __init__(self):
ArgValidatorDict.__init__(self,
name = 'ip',
nested = [
ArgValidatorBool('dhcp4', default_value = None),
ArgValidatorBool('dhcp4_send_hostname', default_value = None),
ArgValidatorIP ('gateway4', family = socket.AF_INET),
ArgValidatorNum ('route_metric4', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorBool('auto6', default_value = None),
ArgValidatorIP ('gateway6', family = socket.AF_INET6),
ArgValidatorNum ('route_metric6', val_min = -1, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorList('address',
nested = ArgValidatorIPAddr('address[?]'),
default_value = list,
),
ArgValidatorList('route',
nested = ArgValidatorIPRoute('route[?]'),
default_value = list,
),
ArgValidatorBool('route_append_only'),
ArgValidatorBool('rule_append_only'),
ArgValidatorList('dns',
nested = ArgValidatorIP('dns[?]', plain_address=False),
default_value = list,
),
ArgValidatorList('dns_search',
nested = ArgValidatorStr('dns_search[?]'),
default_value = list,
),
],
default_value = lambda: {
'dhcp4': True,
'dhcp4_send_hostname': None,
'gateway4': None,
'route_metric4': None,
'auto6': True,
'gateway6': None,
'route_metric6': None,
'address': [],
'route': [],
'route_append_only': False,
'rule_append_only': False,
'dns': [],
'dns_search': [],
},
)
def _validate_post(self, value, name, result):
if result['dhcp4'] is None:
result['dhcp4'] = result['dhcp4_send_hostname'] is not None or not any([a for a in result['address'] if a['family'] == socket.AF_INET])
if result['auto6'] is None:
result['auto6'] = not any([a for a in result['address'] if a['family'] == socket.AF_INET6])
if result['dhcp4_send_hostname'] is not None:
if not result['dhcp4']:
raise ValidationError(name, '"dhcp4_send_hostname" is only valid if "dhcp4" is enabled')
return result
class ArgValidator_DictEthernet(ArgValidatorDict):
def __init__(self):
ArgValidatorDict.__init__(self,
name = 'ethernet',
nested = [
ArgValidatorBool('autoneg', default_value = None),
ArgValidatorNum ('speed', val_min = 0, val_max = 0xFFFFFFFF, default_value = 0),
ArgValidatorStr ('duplex', enum_values = ['half', 'full']),
],
default_value = ArgValidator.MISSING,
)
def get_default_ethernet(self):
return {
'autoneg': None,
'speed': 0,
'duplex': None,
}
def _validate_post(self, value, name, result):
has_speed_or_duplex = result['speed'] != 0 \
or result['duplex'] is not None
if result['autoneg'] is None:
if has_speed_or_duplex:
result['autoneg'] = False
elif result['autoneg']:
if has_speed_or_duplex:
raise ValidationError(name, 'cannot specify "%s" with "autoneg" enabled' % ('duplex' if result['duplex'] is not None else 'speed'))
else:
if not has_speed_or_duplex:
raise ValidationError(name, 'need to specify "duplex" and "speed" with "autoneg" enabled')
if ( has_speed_or_duplex \
and ( result['speed'] == 0 \
or result['duplex'] is None)):
raise ValidationError(name, 'need to specify both "speed" and "duplex" with "autoneg" disabled')
return result
class ArgValidator_DictBond(ArgValidatorDict):
VALID_MODES = [ 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']
def __init__(self):
ArgValidatorDict.__init__(self,
name = 'bond',
nested = [
ArgValidatorStr ('mode', enum_values = ArgValidator_DictBond.VALID_MODES),
ArgValidatorNum ('miimon', val_min = 0, val_max = 1000000, default_value = None),
],
default_value = ArgValidator.MISSING,
)
def get_default_bond(self):
return {
'mode': ArgValidator_DictBond.VALID_MODES[0],
'miimon': None,
}
class ArgValidator_DictConnection(ArgValidatorDict):
VALID_STATES = ['up', 'down', 'present', 'absent', 'wait']
VALID_TYPES = [ 'ethernet', 'infiniband', 'bridge', 'team', 'bond', 'vlan' ]
VALID_SLAVE_TYPES = [ 'bridge', 'bond', 'team' ]
def __init__(self):
ArgValidatorDict.__init__(self,
name = 'connections[?]',
nested = [
ArgValidatorStr ('name'),
ArgValidatorStr ('state', enum_values = ArgValidator_DictConnection.VALID_STATES),
ArgValidatorBool('force_state_change', default_value = None),
ArgValidatorNum ('wait', val_min = 0, val_max = 3600, numeric_type = float),
ArgValidatorStr ('type', enum_values = ArgValidator_DictConnection.VALID_TYPES),
ArgValidatorBool('autoconnect', default_value = True),
ArgValidatorStr ('slave_type', enum_values = ArgValidator_DictConnection.VALID_SLAVE_TYPES),
ArgValidatorStr ('master'),
ArgValidatorStr ('interface_name'),
ArgValidatorMac ('mac'),
ArgValidatorNum ('mtu', val_min = 0, val_max = 0xFFFFFFFF, default_value = None),
ArgValidatorStr ('zone'),
ArgValidatorBool('check_iface_exists', default_value = True),
ArgValidatorStr ('parent'),
ArgValidatorNum ('vlan_id', val_min = 0, val_max = 4094, default_value = None),
ArgValidatorBool('ignore_errors', default_value = None),
ArgValidatorStr ('infiniband_transport_mode', enum_values = ['datagram', 'connected']),
ArgValidatorNum ('infiniband_p_key', val_min = -1, val_max = 0xFFFF, default_value = None),
ArgValidator_DictIP(),
ArgValidator_DictEthernet(),
ArgValidator_DictBond(),
],
default_value = dict,
all_missing_during_validate = True,
)
def _validate_post(self, value, name, result):
if 'state' not in result:
if 'type' in result:
result['state'] = 'present'
elif list(result.keys()) == [ 'wait' ]:
result['state'] = 'wait'
else:
result['state'] = 'up'
if result['state'] == 'present' or (result['state'] == 'up' and 'type' in result):
VALID_FIELDS = list(self.nested.keys())
if result['state'] == 'present':
VALID_FIELDS.remove('wait')
VALID_FIELDS.remove('force_state_change')
elif result['state'] in ['up', 'down']:
VALID_FIELDS = ['name', 'state', 'wait', 'ignore_errors', 'force_state_change']
elif result['state'] == 'absent':
VALID_FIELDS = ['name', 'state', 'ignore_errors']
elif result['state'] == 'wait':
VALID_FIELDS = ['state', 'wait']
else:
assert False
VALID_FIELDS = set(VALID_FIELDS)
for k in result:
if k not in VALID_FIELDS:
raise ValidationError(name + '.' + k, 'property is not allowed for state "%s"' % (result['state']))
if result['state'] != 'wait':
if result['state'] == 'absent':
if 'name' not in result:
result['name'] = '' # set to empty string to mean *absent all others*
else:
if 'name' not in result:
raise ValidationError(name, 'missing "name"')
if result['state'] in [ 'wait', 'up', 'down' ]:
if 'wait' not in result:
result['wait'] = None
else:
if 'wait' in result:
raise ValidationError(name + '.wait', '"wait" is not allowed for state "%s"' % (result['state']))
if result['state'] == 'present' and 'type' not in result:
raise ValidationError(name + '.state', '"present" state requires a "type" argument')
if 'type' in result:
if 'master' in result:
if 'slave_type' not in result:
result['slave_type'] = None
if result['master'] == result['name']:
raise ValidationError(name + '.master', '"master" cannot refer to itself')
else:
if 'slave_type' in result:
raise ValidationError(name + '.slave_type', '"slave_type" requires a "master" property')
if 'ip' in result:
if 'master' in result:
raise ValidationError(name + '.ip', 'a slave cannot have an "ip" property')
else:
if 'master' not in result:
result['ip'] = self.nested['ip'].get_default_value()
if 'zone' in result:
if 'master' in result:
raise ValidationError(name + '.zone', '"zone" cannot be configured for slave types')
else:
result['zone'] = None
if 'mac' in result:
if result['type'] not in [ 'ethernet', 'infiniband' ]:
raise ValidationError(name + '.mac', 'a "mac" address is only allowed for type "ethernet" or "infiniband"')
l = len(Util.mac_aton(result['mac']))
if result['type'] == 'ethernet' and l != 6:
raise ValidationError(name + '.mac', 'a "mac" address for type ethernet requires 6 octets but is "%s"' % result['mac'])
if result['type'] == 'infiniband' and l != 20:
raise ValidationError(name + '.mac', 'a "mac" address for type ethernet requires 20 octets but is "%s"' % result['mac'])
if 'infiniband_transport_mode' in result:
if result['type'] != 'infiniband':
raise ValidationError(name + '.infiniband_transport_mode', 'a "infiniband_transport_mode" property is only allowed for type "infiniband"')
elif result['type'] == 'infiniband':
result['infiniband_transport_mode'] = 'datagram'
if 'infiniband_p_key' in result:
if result['type'] != 'infiniband':
raise ValidationError(name + '.infiniband_p_key', 'a "infiniband_p_key" property is only allowed for type "infiniband"')
if 'mac' not in result and \
'parent' not in result:
raise ValidationError(name + '.infiniband_p_key', 'a infiniband device with "infiniband_p_key" property also needs "mac" or "parent" property')
if 'interface_name' in result:
if not Util.ifname_valid(result['interface_name']):
raise ValidationError(name + '.interface_name', 'invalid "interface_name" "%s"' % (result['interface_name']))
else:
if result['type'] in [ 'bridge', 'bond', 'team', 'vlan' ]:
if not Util.ifname_valid(result['name']):
raise ValidationError(name + '.interface_name', 'requires "interface_name" as "name" "%s" is not valid' % (result['name']))
result['interface_name'] = result['name']
if result['type'] == 'vlan':
if 'vlan_id' not in result:
raise ValidationError(name + '.vlan_id', 'missing "vlan_id" for "type" "vlan"')
if 'parent' not in result:
raise ValidationError(name + '.parent', 'missing "parent" for "type" "vlan"')
else:
if 'vlan_id' in result:
raise ValidationError(name + '.vlan_id', '"vlan_id" is only allowed for "type" "vlan"')
if 'parent' in result:
if result['type'] not in ['vlan', 'infiniband']:
raise ValidationError(name + '.parent', '"parent" is only allowed for type "vlan" or "infiniband"')
if result['parent'] == result['name']:
raise ValidationError(name + '.parent', '"parent" cannot refer to itself')
if result['type'] == 'bond':
if 'bond' not in result:
result['bond'] = self.nested['bond'].get_default_bond()
else:
if 'bond' in result:
raise ValidationError(name + '.bond', '"bond" settings are not allowed for "type" "%s"' % (result['type']))
if result['type'] in ['ethernet', 'vlan', 'bridge', 'bond', 'team']:
if 'ethernet' not in result:
result['ethernet'] = self.nested['ethernet'].get_default_ethernet()
else:
if 'ethernet' in result:
raise ValidationError(name + '.ethernet', '"ethernet" settings are not allowed for "type" "%s"' % (result['type']))
for k in VALID_FIELDS:
if k in result:
continue
v = self.nested[k]
vv = v.get_default_value()
if vv is not ArgValidator.MISSING:
result[k] = vv
return result
class ArgValidator_ListConnections(ArgValidatorList):
def __init__(self):
ArgValidatorList.__init__(self,
name = 'connections',
nested = ArgValidator_DictConnection(),
default_value = list
)
def _validate_post(self, value, name, result):
for idx, connection in enumerate(result):
if connection['state'] in ['down', 'up']:
if connection['state'] == 'up' and 'type' in connection:
pass
elif not ArgUtil.connection_find_by_name(connection['name'], result, idx):
raise ValidationError(name + '[' + str(idx) + '].name', 'state "%s" references non-existing connection "%s"' % (connection['state'], connection['name']))
if 'type' in connection:
if connection['master']:
c = ArgUtil.connection_find_by_name(connection['master'], result, idx)
if not c:
raise ValidationError(name + '[' + str(idx) + '].master', 'references non-existing "master" connection "%s"' % (connection['master']))
if c['type'] not in ArgValidator_DictConnection.VALID_SLAVE_TYPES:
raise ValidationError(name + '[' + str(idx) + '].master', 'references "master" connection "%s" which is not a master type by "%s"' % (connection['master'], c['type']))
if connection['slave_type'] is None:
connection['slave_type'] = c['type']
elif connection['slave_type'] != c['type']:
raise ValidationError(name + '[' + str(idx) + '].master', 'references "master" connection "%s" which is of type "%s" instead of slave_type "%s"' % (connection['master'], c['type'], connection['slave_type']))
if connection['parent']:
if not ArgUtil.connection_find_by_name(connection['parent'], result, idx):
raise ValidationError(name + '[' + str(idx) + '].parent', 'references non-existing "parent" connection "%s"' % (connection['parent']))
return result
VALIDATE_ONE_MODE_NM = 'nm'
VALIDATE_ONE_MODE_INITSCRIPTS = 'initscripts'
def validate_connection_one(self, mode, connections, idx):
connection = connections[idx]
if 'type' not in connection:
return
if ( (connection['parent'])
and ( ( (mode == self.VALIDATE_ONE_MODE_INITSCRIPTS)
and (connection['type'] == 'vlan'))
or ( (connection['type'] == 'infiniband')
and (connection['infiniband_p_key'] not in [ None, -1 ])))):
try:
ArgUtil.connection_find_master(connection['parent'], connections, idx)
except MyError as e:
raise ValidationError.from_connection(idx, 'profile references a parent "%s" which has \'interface_name\' missing' % (connection['parent']))
if ( (connection['master'])
and (mode == self.VALIDATE_ONE_MODE_INITSCRIPTS)):
try:
ArgUtil.connection_find_master(connection['master'], connections, idx)
except MyError as e:
raise ValidationError.from_connection(idx, 'profile references a master "%s" which has \'interface_name\' missing' % (connection['master']))
###############################################################################
class IfcfgUtil:
FILE_TYPES = [
'ifcfg',
'keys',
'route',
'route6',
'rule',
'rule6',
]
@classmethod
def _file_types(cls, file_type):
if file_type is None:
return cls.FILE_TYPES
else:
return [ file_type ]
@classmethod
def ifcfg_paths(cls, name, file_types = None):
paths = []
if file_types is None:
file_types = cls.FILE_TYPES
for f in file_types:
paths.append(cls.ifcfg_path(name, f))
return paths
@classmethod
def ifcfg_path(cls, name, file_type = None):
n = str(name)
if not name or \
n == '.' or \
n == '..' or \
n.find('/') != -1:
raise MyError('invalid ifcfg-name %s' % (name))
if file_type is None:
file_type = 'ifcfg'
if file_type not in cls.FILE_TYPES:
raise MyError('invalid file-type %s' % (file_type))
return '/etc/sysconfig/network-scripts/' + file_type + '-' + n
@classmethod
def KeyValid(cls, name):
r = getattr(cls, '_CHECKSTR_VALID_KEY', None)
if r is None:
import re
r = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
cls._CHECKSTR_VALID_KEY = r
return bool(r.match(name))
@classmethod
def ValueEscape(cls, value):
r = getattr(cls, '_re_ValueEscape', None)
if r is None:
import re
r = re.compile('^[a-zA-Z_0-9-.]*$')
cls._re_ValueEscape = r
if r.match(value):
return value
if any([ord(c) < ord(' ') for c in value]):
# needs ansic escaping due to ANSI control caracters (newline)
s = '$\''
for c in value:
if ord(c) < ord(c):
s += '\\' + str(ord(c))
elif c == '\\' or c == '\'':
s += '\\' + c
else:
# non-unicode chars are fine too to take literally
# as utf8
s += c
s += '\''
else:
# double quoting
s = '"'
for c in value:
if c == '"' or c == '\\' or c == '$' or c == '`':
s += '\\' + c
else:
# non-unicode chars are fine too to take literally
# as utf8
s += c
s += '"'
return s
@classmethod
def _ifcfg_route_merge(cls, route, append_only, current):
if not append_only or current is None:
if not route:
return None
return '\n'.join(route) + '\n'
if route:
# the 'route' file is processed line by line by initscripts' ifup-route. Hence,
# the order of the route matters. _ifcfg_route_merge() is not sophisticated
# enough to understand pre-existing lines. It will only append lines that
# don't exist yet, which hopefully is correct.
# It's better to always rewrite the entire file with route_append_only=False.
changed = False
c_lines = list(current.split('\n'))
for r in route:
if r not in c_lines:
changed = True
c_lines.append(r)
if changed:
return '\n'.join(c_lines) + '\n'
return current
@classmethod
def ifcfg_create(cls, connections, idx, warn_fcn = lambda msg: None, content_current = None):
connection = connections[idx]
ip = connection['ip']
ifcfg = {}
keys_file = None
route4_file = None
route6_file = None
rule4_file = None
rule6_file = None
if ip['dhcp4_send_hostname'] is not None:
warn_fcn('ip.dhcp4_send_hostname is not supported by initscripts provider')
if ip['route_metric4'] is not None and ip['route_metric4'] >= 0:
warn_fcn('ip.route_metric4 is not supported by initscripts provider')
if ip['route_metric6'] is not None and ip['route_metric6'] >= 0:
warn_fcn('ip.route_metric6 is not supported by initscripts provider')
ifcfg['NM_CONTROLLED'] = 'no'
if connection['autoconnect']:
ifcfg['ONBOOT'] = 'yes'
ifcfg['DEVICE'] = connection['interface_name']
if connection['type'] == 'ethernet':
ifcfg['TYPE'] = 'Ethernet'
ifcfg['HWADDR'] = connection['mac']
elif connection['type'] == 'infiniband':
ifcfg['TYPE'] = 'InfiniBand'
ifcfg['HWADDR'] = connection['mac']
ifcfg['CONNECTED_MODE'] = 'yes' if (connection['infiniband_transport_mode'] == 'connected') else 'no'
if connection['infiniband_p_key'] not in [ None, -1 ]:
ifcfg['PKEY'] = 'yes'
ifcfg['PKEY_ID'] = str(connection['infiniband_p_key'])
if connection['parent']:
ifcfg['PHYSDEV'] = ArgUtil.connection_find_master(connection['parent'], connections, idx)
elif connection['type'] == 'bridge':
ifcfg['TYPE'] = 'Bridge'
elif connection['type'] == 'bond':
ifcfg['TYPE'] = 'Bond'
ifcfg['BONDING_MASTER'] = 'yes'
opts = [ 'mode=%s' % (connection['bond']['mode']) ]
if connection['bond']['miimon'] is not None:
opts.append(' miimon=%s' % (connection['bond']['miimon']))
ifcfg['BONDING_OPTS'] = ' '.join(opts)
elif connection['type'] == 'team':
ifcfg['DEVICETYPE'] = 'Team'
elif connection['type'] == 'vlan':
ifcfg['VLAN'] = 'yes'
ifcfg['TYPE'] = 'Vlan'
ifcfg['PHYSDEV'] = ArgUtil.connection_find_master(connection['parent'], connections, idx)
ifcfg['VID'] = str(connection['vlan_id'])
else:
raise MyError('unsupported type %s' % (connection['type']))
if connection['mtu']:
ifcfg['MTU'] = str(connection['mtu'])
if 'ethernet' in connection:
if connection['ethernet']['autoneg'] is not None:
if connection['ethernet']['autoneg']:
s = 'autoneg on'
else:
s = 'autoneg off speed %s duplex %s' % (connection['ethernet']['speed'],
connection['ethernet']['duplex'])
ifcfg['ETHTOOL_OPTS'] = s
if connection['master'] is not None:
m = ArgUtil.connection_find_master(connection['master'], connections, idx)
if connection['slave_type'] == 'bridge':
ifcfg['BRIDGE'] = m
elif connection['slave_type'] == 'bond':
ifcfg['MASTER'] = m
ifcfg['SLAVE'] = 'yes'
elif connection['slave_type'] == 'team':
ifcfg['TEAM_MASTER'] = m
if 'TYPE' in ifcfg:
del ifcfg['TYPE']
if connection['type'] != 'team':
ifcfg['DEVICETYPE'] = 'TeamPort'
else:
raise MyError('invalid slave_type "%s"' % (connection['slave_type']))
if ip['route_append_only'] and content_current:
route4_file = content_current['route']
route6_file = content_current['route6']
else:
if connection['zone']:
ifcfg['ZONE'] = connection['zone']
addrs4 = list([a for a in ip['address'] if a['family'] == socket.AF_INET])
addrs6 = list([a for a in ip['address'] if a['family'] == socket.AF_INET6])
if ip['dhcp4']:
ifcfg['BOOTPROTO'] = 'dhcp'
elif addrs4:
ifcfg['BOOTPROTO'] = 'static'
else:
ifcfg['BOOTPROTO'] = 'none'
for i in range(0, len(addrs4)):
a = addrs4[i]
ifcfg['IPADDR' + ('' if i == 0 else str(i))] = a['address']
ifcfg['PREFIX' + ('' if i == 0 else str(i))] = str(a['prefix'])
if ip['gateway4'] is not None:
ifcfg['GATEWAY'] = ip['gateway4']
for idx, dns in enumerate(ip['dns']):
ifcfg['DNS' + str(idx+1)] = dns['address']
if ip['dns_search']:
ifcfg['DOMAIN'] = ' '.join(ip['dns_search'])
if ip['auto6']:
ifcfg['IPV6INIT'] = 'yes'
ifcfg['IPV6_AUTOCONF'] = 'yes'
elif addrs6:
ifcfg['IPV6INIT'] = 'yes'
ifcfg['IPV6_AUTOCONF'] = 'no'
else:
ifcfg['IPV6INIT'] = 'no'
if addrs6:
ifcfg['IPVADDR'] = addrs6[0]['address'] + '/' + str(addrs6[0]['prefix'])
if len(addrs6) > 1:
ifcfg['IPVADDR_SECONDARIES'] = ' '.join([a['address'] + '/' + str(a['prefix']) for a in addrs6[1:]])
if ip['gateway6'] is not None:
ifcfg['IPV6_DEFAULTGW'] = ip['gateway6']
route4 = []
route6 = []
for r in ip['route']:
line = r['network'] + '/' + str(r['prefix'])
if r['gateway']:
line += ' via ' + r['gateway']
if r['metric'] != -1:
line += ' metric ' + str(r['metric'])
if r['family'] == socket.AF_INET:
route4.append(line)
else:
route6.append(line)
route4_file = cls._ifcfg_route_merge(route4,
ip['route_append_only'] and content_current,
content_current['route'] if content_current else None)
route6_file = cls._ifcfg_route_merge(route6,
ip['route_append_only'] and content_current,
content_current['route6'] if content_current else None)
if ip['rule_append_only'] and content_current:
rule4_file = content_current['rule']
rule6_file = content_current['rule6']
for key in list(ifcfg.keys()):
v = ifcfg[key]
if v is None:
del ifcfg[key]
continue
if type(v) == type(True):
ifcfg[key] = 'yes' if v else 'no'
return {
'ifcfg': ifcfg,
'keys': keys_file,
'route': route4_file,
'route6': route6_file,
'rule': rule4_file,
'rule6': rule6_file,
}
@classmethod
def ifcfg_parse_line(cls, line):
r1 = getattr(cls, '_re_parse_line1', None)
if r1 is None:
import re
import shlex
r1 = re.compile('^[ \t]*([a-zA-Z_][a-zA-Z_0-9]*)=(.*)$')
cls._re_parse_line1 = r1
cls._shlex = shlex
m = r1.match(line)
if not m:
return None
key = m.group(1)
val = m.group(2)
val = val.rstrip()
# shlex isn't up to the task of parsing shell. Whatever,
# we can only parse shell to a certain degree and this is
# good enough for now.
try:
c = list(cls._shlex.split(val, comments=True, posix=True))
except:
return None
if len(c) != 1:
return None
return (key, c[0])
@classmethod
def ifcfg_parse(cls, content):
if content is None:
return None
ifcfg = {}
for line in content.splitlines():
val = cls.ifcfg_parse_line(line)
if val:
ifcfg[val[0]] = val[1]
return ifcfg
@classmethod
def content_from_dict(cls, ifcfg_all, file_type = None, header = None):
content = {}
for file_type in cls._file_types(file_type):
h = ifcfg_all[file_type]
if file_type == 'ifcfg':
if header is not None:
s = header + '\n'
else:
s = ""
for key in sorted(h.keys()):
value = h[key]
if not cls.KeyValid(key):
raise MyError('invalid ifcfg key %s' % (key))
if value is not None:
s += key + '=' + cls.ValueEscape(value) + '\n'
content[file_type] = s
else:
content[file_type] = h
return content
@classmethod
def content_to_dict(cls, content, file_type = None):
ifcfg_all = {}
for file_type in cls._file_types(file_type):
ifcfg_all[file_type] = cls.ifcfg_parse(content[file_type])
return ifcfg_all
@classmethod
def content_from_file(cls, name, file_type = None):
content = {}
for file_type in cls._file_types(file_type):
path = cls.ifcfg_path(name, file_type)
try:
with open(path, 'r') as content_file:
i_content = content_file.read()
except Exception as e:
i_content = None
content[file_type] = i_content
return content
@classmethod
def content_to_file(cls, name, content, file_type = None):
for file_type in cls._file_types(file_type):
path = cls.ifcfg_path(name, file_type)
h = content[file_type]
if h is None:
try:
os.unlink(path)
except OSError as e:
import errno
if e.errno != errno.ENOENT:
raise
else:
with open(path, 'w') as text_file:
text_file.write(h)
@classmethod
def connection_seems_active(cls, name):
# we don't know whether a ifcfg file is currently active,
# and we also don't know which.
#
# Do a very basic guess based on whether the interface
# is in operstate "up".
#
# But first we need to find the interface name. Do
# some naive parsing and check for DEVICE setting.
content = cls.content_from_file(name, 'ifcfg')
if content['ifcfg'] is not None:
content = cls.ifcfg_parse(content['ifcfg'])
else:
content = {}
if 'DEVICE' not in content:
return None
path = '/sys/class/net/' + content['DEVICE'] + '/operstate'
try:
with open(path, 'r') as content_file:
i_content = str(content_file.read())
except Exception as e:
return None
if i_content.strip() != 'up':
return False
return True
###############################################################################
class NMUtil:
def __init__(self, nmclient = None):
if nmclient is None:
nmclient = Util.NM().Client.new(None)
self.nmclient = nmclient
def setting_ip_config_get_routes(self, s_ip):
if s_ip is not None:
for i in range(0, s_ip.get_num_routes()):
yield s_ip.get_route(i)
def connection_ensure_setting(self, connection, setting_type):
setting = connection.get_setting(setting_type)
if not setting:
setting = setting_type.new()
connection.add_setting(setting)
return setting
def device_is_master_type(self, dev):
if dev:
NM = Util.NM()
GObject = Util.GObject()
if GObject.type_is_a(dev, NM.DeviceBond) \
or GObject.type_is_a(dev, NM.DeviceBridge) \
or GObject.type_is_a(dev, NM.DeviceTeam):
return True
return False
def active_connection_list(self, connections = None, black_list = None):
active_cons = self.nmclient.get_active_connections()
if connections:
connections = set(connections)
active_cons = [ac for ac in active_cons if ac.get_connection() in connections]
if black_list:
active_cons = [ac for ac in active_cons if ac not in black_list]
return list(active_cons)
def connection_list(self, name = None, uuid = None, black_list = None, black_list_names = None, black_list_uuids = None):
cons = self.nmclient.get_connections()
if name is not None:
cons = [c for c in cons if c.get_id() == name]
if uuid is not None:
cons = [c for c in cons if c.get_uuid() == uuid]
if black_list:
cons = [c for c in cons if c not in black_list]
if black_list_uuids:
cons = [c for c in cons if c.get_uuid() not in black_list_uuids]
if black_list_names:
cons = [c for c in cons if c.get_id() not in black_list_names]
cons = list(cons)
def _cmp(a, b):
s_a = a.get_setting_connection()
s_b = b.get_setting_connection()
if not s_a and not s_b:
return 0
if not s_a:
return 1
if not s_b:
return -1
t_a = s_a.get_timestamp()
t_b = s_b.get_timestamp()
if t_a == t_b:
return 0
if t_a <= 0:
return 1
if t_b <= 0:
return -1
return cmp(t_a, t_b)
cons.sort(cmp = _cmp)
return cons
def connection_compare(self, con_a, con_b, normalize_a = False, normalize_b = False, compare_flags = None):
NM = Util.NM()
if normalize_a:
con_a = NM.SimpleConnection.new_clone(con_a)
try:
con_a.normalize()
except:
pass
if normalize_b:
con_b = NM.SimpleConnection.new_clone(con_b)
try:
con_b.normalize()
except:
pass
if compare_flags == None:
compare_flags = NM.SettingCompareFlags.IGNORE_TIMESTAMP
return not(not(con_a.compare (con_b, compare_flags)))
def connection_is_active(self, con):
NM = Util.NM()
for ac in self.active_connection_list(connections=[con]):
if ac.get_state() >= NM.ActiveConnectionState.ACTIVATING \
and ac.get_state() <= NM.ActiveConnectionState.ACTIVATED:
return True
return False
def connection_create(self, connections, idx, connection_current = None):
NM = Util.NM()
connection = connections[idx]
con = NM.SimpleConnection.new()
s_con = self.connection_ensure_setting(con, NM.SettingConnection)
s_con.set_property(NM.SETTING_CONNECTION_ID, connection['name'])
s_con.set_property(NM.SETTING_CONNECTION_UUID, connection['nm.uuid'])
s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, connection['autoconnect'])
s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, connection['interface_name'])
if connection['type'] == 'ethernet':
s_con.set_property(NM.SETTING_CONNECTION_TYPE, '802-3-ethernet')
s_wired = self.connection_ensure_setting(con, NM.SettingWired)
s_wired.set_property(NM.SETTING_WIRED_MAC_ADDRESS, connection['mac'])
elif connection['type'] == 'infiniband':
s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'infiniband')
s_infiniband = self.connection_ensure_setting(con, NM.SettingInfiniband)
s_infiniband.set_property(NM.SETTING_INFINIBAND_MAC_ADDRESS, connection['mac'])
s_infiniband.set_property(NM.SETTING_INFINIBAND_TRANSPORT_MODE, connection['infiniband_transport_mode'])
if connection['infiniband_p_key'] not in [ None, -1 ]:
s_infiniband.set_property(NM.SETTING_INFINIBAND_P_KEY, connection['infiniband_p_key'])
if connection['parent']:
s_infiniband.set_property(NM.SETTING_INFINIBAND_PARENT, ArgUtil.connection_find_master(connection['parent'], connections, idx))
elif connection['type'] == 'bridge':
s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'bridge')
s_bridge = self.connection_ensure_setting(con, NM.SettingBridge)
s_bridge.set_property(NM.SETTING_BRIDGE_STP, False)
elif connection['type'] == 'bond':
s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'bond')
s_bond = self.connection_ensure_setting(con, NM.SettingBond)
s_bond.add_option('mode', connection['bond']['mode'])
if connection['bond']['miimon'] is not None:
s_bond.add_option('miimon', str(connection['bond']['miimon']))
elif connection['type'] == 'team':
s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'team')
elif connection['type'] == 'vlan':
s_vlan = self.connection_ensure_setting(con, NM.SettingVlan)
s_vlan.set_property(NM.SETTING_VLAN_ID, connection['vlan_id'])
s_vlan.set_property(NM.SETTING_VLAN_PARENT, ArgUtil.connection_find_master_uuid(connection['parent'], connections, idx))
else:
raise MyError('unsupported type %s' % (connection['type']))
if 'ethernet' in connection:
if connection['ethernet']['autoneg'] is not None:
s_wired = self.connection_ensure_setting(con, NM.SettingWired)
s_wired.set_property(NM.SETTING_WIRED_AUTO_NEGOTIATE, connection['ethernet']['autoneg'])
s_wired.set_property(NM.SETTING_WIRED_DUPLEX, connection['ethernet']['duplex'])
s_wired.set_property(NM.SETTING_WIRED_SPEED, connection['ethernet']['speed'])
if connection['mtu']:
if connection['type'] == 'infiniband':
s_infiniband = self.connection_ensure_setting(con, NM.SettingInfiniband)
s_infiniband.set_property(NM.SETTING_INFINIBAND_MTU, connection['mtu'])
else:
s_wired = self.connection_ensure_setting(con, NM.SettingWired)
s_wired.set_property(NM.SETTING_WIRED_MTU, connection['mtu'])
if connection['master'] is not None:
s_con.set_property(NM.SETTING_CONNECTION_SLAVE_TYPE, connection['slave_type'])
s_con.set_property(NM.SETTING_CONNECTION_MASTER, ArgUtil.connection_find_master_uuid(connection['master'], connections, idx))
else:
if connection['zone']:
s_con.set_property(NM.SETTING_CONNECTION_ZONE, connection['zone'])
ip = connection['ip']
s_ip4 = self.connection_ensure_setting(con, NM.SettingIP4Config)
s_ip6 = self.connection_ensure_setting(con, NM.SettingIP6Config)
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto')
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto')
addrs4 = list([a for a in ip['address'] if a['family'] == socket.AF_INET])
addrs6 = list([a for a in ip['address'] if a['family'] == socket.AF_INET6])
if ip['dhcp4']:
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto')
s_ip4.set_property(NM.SETTING_IP_CONFIG_DHCP_SEND_HOSTNAME, ip['dhcp4_send_hostname'] != False)
elif addrs4:
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, 'manual')
else:
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, 'disabled')
for a in addrs4:
s_ip4.add_address(NM.IPAddress.new(a['family'], a['address'], a['prefix']))
if ip['gateway4'] is not None:
s_ip4.set_property(NM.SETTING_IP_CONFIG_GATEWAY, ip['gateway4'])
if ip['route_metric4'] is not None and ip['route_metric4'] >= 0:
s_ip4.set_property(NM.SETTING_IP_CONFIG_ROUTE_METRIC, ip['route_metric4'])
for d in ip['dns']:
if d['family'] == socket.AF_INET:
s_ip4.add_dns(d['address'])
for s in ip['dns_search']:
s_ip4.add_dns_search(s)
if ip['auto6']:
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto')
elif addrs6:
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, 'manual')
else:
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, 'ignore')
for a in addrs6:
s_ip6.add_address(NM.IPAddress.new(a['family'], a['address'], a['prefix']))
if ip['gateway6'] is not None:
s_ip6.set_property(NM.SETTING_IP_CONFIG_GATEWAY, ip['gateway6'])
if ip['route_metric6'] is not None and ip['route_metric6'] >= 0:
s_ip6.set_property(NM.SETTING_IP_CONFIG_ROUTE_METRIC, ip['route_metric6'])
for d in ip['dns']:
if d['family'] == socket.AF_INET6:
s_ip6.add_dns(d['address'])
if ip['route_append_only'] and connection_current:
for r in self.setting_ip_config_get_routes(connection_current.get_setting(NM.SettingIP4Config)):
s_ip4.add_route(r)
for r in self.setting_ip_config_get_routes(connection_current.get_setting(NM.SettingIP6Config)):
s_ip6.add_route(r)
for r in ip['route']:
rr = NM.IPRoute.new(r['family'], r['network'], r['prefix'], r['gateway'], r['metric'])
if r['family'] == socket.AF_INET:
s_ip4.add_route(rr)
else:
s_ip6.add_route(rr)
try:
con.normalize()
except Exception as e:
raise MyError('created connection failed to normalize: %s' % (e))
return con
def connection_add(self, con, timeout = 10):
def add_cb(client, result, cb_args):
con = None
try:
con = client.add_connection_finish(result)
except Exception as e:
if Util.error_is_cancelled(e):
return
cb_args['error'] = str(e)
cb_args['con'] = con
Util.GMainLoop().quit()
cancellable = Util.create_cancellable()
cb_args = {}
self.nmclient.add_connection_async(con, True, cancellable, add_cb, cb_args)
if not Util.GMainLoop_run(timeout):
cancellable.cancel()
raise MyError('failure to add connection: %s' % ('timeout'))
if not cb_args.get('con', None):
raise MyError('failure to add connection: %s' % (cb_args.get('error', 'unknown error')))
return cb_args['con']
def connection_update(self, con, con_new, timeout = 10):
con.replace_settings_from_connection(con_new)
def update_cb(connection, result, cb_args):
success = False
try:
success = connection.commit_changes_finish(result)
except Exception as e:
if Util.error_is_cancelled(e):
return
cb_args['error'] = str(e)
cb_args['success'] = success
Util.GMainLoop().quit()
cancellable = Util.create_cancellable()
cb_args = {}
con.commit_changes_async(True, cancellable, update_cb, cb_args)
if not Util.GMainLoop_run(timeout):
cancellable.cancel()
raise MyError('failure to update connection: %s' % ('timeout'))
if not cb_args.get('success', False):
raise MyError('failure to update connection: %s' % (cb_args.get('error', 'unknown error')))
return True
def connection_delete(self, connection, timeout = 10):
c_uuid = connection.get_uuid()
def delete_cb(connection, result, cb_args):
success = False
try:
success = connection.delete_finish(result)
except Exception as e:
if Util.error_is_cancelled(e):
return
cb_args['error'] = str(e)
cb_args['success'] = success
Util.GMainLoop().quit()
cancellable = Util.create_cancellable()
cb_args = {}
connection.delete_async(cancellable, delete_cb, cb_args)
if not Util.GMainLoop_run(timeout):
cancellable.cancel()
raise MyError('failure to delete connection: %s' % ('timeout'))
if not cb_args.get('success', False):
raise MyError('failure to delete connection: %s' % (cb_args.get('error', 'unknown error')))
# workaround libnm oddity. The connection may not yet be gone if the
# connection was active and is deactivating. Wait.
wait_count = 0
while True:
connections = self.connection_list(uuid = c_uuid)
if not connections:
return
wait_count += 1
if wait_count > 10:
break;
import time
time.sleep(1)
Util.GMainLoop_iterate_all()
raise MyError('connection %s was supposedly deleted successfully, but it\'s still here' % (c_uuid))
def connection_activate(self, connection, timeout = 15, wait_time = None):
already_retried = False;
while True:
def activate_cb(client, result, cb_args):
active_connection = None
try:
active_connection = client.activate_connection_finish(result)
except Exception as e:
if Util.error_is_cancelled(e):
return
cb_args['error'] = str(e)
cb_args['active_connection'] = active_connection
Util.GMainLoop().quit()
cancellable = Util.create_cancellable()
cb_args = {}
self.nmclient.activate_connection_async(connection, None, None, cancellable, activate_cb, cb_args)
if not Util.GMainLoop_run(timeout):
cancellable.cancel()
raise MyError('failure to activate connection: %s' % ('timeout'))
if cb_args.get('active_connection', None):
ac = cb_args['active_connection']
self.connection_activate_wait(ac, wait_time)
return ac
# there is a bug in NetworkManager, that the connection might already be in the process
# of activating. In that case, NM would reject the activation request with
# "Connection '$PROFILE' is not available on the device $DEV at this time."
#
# Try to work around it by waiting a bit and retrying.
if already_retried:
raise MyError('failure to activate connection: %s' % (cb_args.get('error', 'unknown error')))
already_retried = True
import time
time.sleep(1)
def connection_activate_wait(self, ac, wait_time):
if not wait_time:
return
NM = Util.NM()
state = ac.get_state()
if state == NM.ActiveConnectionState.ACTIVATED:
return
if state != NM.ActiveConnectionState.ACTIVATING:
raise MyError('activation is in unexpected state "%s"' % (state))
def check_activated(ac, dev):
ac_state = ac.get_state()
# the state reason was for active-connection was introduced in NM 1.8 API.
# Work around for older library version.
try:
ac_reason = ac.get_state_reason()
except AttributeError as e:
ac_reason = None
if dev:
dev_state = dev.get_state()
if ac_state == NM.ActiveConnectionState.ACTIVATING:
if self.device_is_master_type(dev) \
and dev_state >= NM.DeviceState.IP_CONFIG \
and dev_state <= NM.DeviceState.ACTIVATED:
# master connections qualify as activated once they reach IP-Config state.
# That is because they may wait for slave devices to attach
return True, None
# fall through
elif ac_state == NM.ActiveConnectionState.ACTIVATED:
return True, None
elif ac_state == NM.ActiveConnectionState.DEACTIVATED:
if not dev \
or ( ac_reason is not None \
and ac_reason != NM.ActiveConnectionStateReason.DEVICE_DISCONNECTED) \
or dev.get_active_connection() is not ac:
return True, ((ac_reason.value_nick if ac_reason else None) or 'unknown reason')
# the state of the active connection is not very helpful.
# see if the device-state is better.
if dev_state <= NM.DeviceState.DISCONNECTED or dev_state > NM.DeviceState.DEACTIVATING:
return True, (dev.get_state_reason().value_nick or (ac_reason.value_nick if ac_reason else None) or 'unknown reason')
# fall through, wait longer for a better state reason.
# wait longer.
return False, None
dev = Util.first(ac.get_devices())
complete, failure_reason = check_activated(ac, dev)
if not complete:
cb_out = []
def check_activated_cb():
complete, failure_reason = check_activated(ac, dev)
if complete:
cb_out.append(failure_reason)
Util.GMainLoop().quit()
try:
# 'state-changed' signal is 1.8 API. Workaround for older libnm API version
ac_id = ac.connect('state-changed', lambda source, state, reason: check_activated_cb())
except:
ac_id = None
if dev:
dev_id = dev.connect('notify::state', lambda source, pspec: check_activated_cb())
try:
if not Util.GMainLoop_run(wait_time):
raise MyError('connection not fully activated after timeout')
finally:
if dev:
dev.handler_disconnect(dev_id)
if ac_id is not None:
ac.handler_disconnect(ac_id)
failure_reason = cb_out[0]
if failure_reason:
raise MyError('connection not activated: %s' % (failure_reason))
def active_connection_deactivate(self, ac, timeout = 10, wait_time = None):
def deactivate_cb(client, result, cb_args):
success = False
try:
success = client.deactivate_connection_finish(result)
except Exception as e:
if Util.error_is_cancelled(e):
return
cb_args['error'] = str(e)
cb_args['success'] = success
Util.GMainLoop().quit()
cancellable = Util.create_cancellable()
cb_args = {}
self.nmclient.deactivate_connection_async(ac, cancellable, deactivate_cb, cb_args)
if not Util.GMainLoop_run(timeout):
cancellable.cancel()
raise MyError('failure to deactivate connection: %s' % (timeout))
if not cb_args.get('success', False):
raise MyError('failure to deactivate connection: %s' % (cb_args.get('error', 'unknown error')))
self.active_connection_deactivate_wait(ac, wait_time)
return True
def active_connection_deactivate_wait(self, ac, wait_time):
if not wait_time:
return
NM = Util.NM()
def check_deactivated(ac):
return ac.get_state() >= NM.ActiveConnectionState.DEACTIVATED
if not check_deactivated(ac):
def check_deactivated_cb():
if check_deactivated(ac):
Util.GMainLoop().quit()
ac_id = ac.connect('notify::state', lambda source, pspec: check_deactivated_cb())
try:
if not Util.GMainLoop_run(wait_time):
raise MyError('connection not fully deactivated after timeout')
finally:
ac.handler_disconnect(ac_id)
###############################################################################
class RunEnvironment:
def __init__(self):
self._check_mode = None
@property
def ifcfg_header(self):
return None
def log(self,
connections,
idx,
severity,
msg,
is_changed = False,
ignore_errors = False,
warn_traceback = False,
force_fail = False):
raise NotImplementedError()
def run_command(self, argv, encoding = None):
raise NotImplementedError()
def _check_mode_changed(self, old_check_mode, new_check_mode, connections):
raise NotImplementedError()
def check_mode_set(self, check_mode, connections = None):
c = self._check_mode
self._check_mode = check_mode
assert( (c is None and check_mode in [CheckMode.PREPARE]) \
or (c == CheckMode.PREPARE and check_mode in [CheckMode.PRE_RUN, CheckMode.DRY_RUN]) \
or (c == CheckMode.PRE_RUN and check_mode in [CheckMode.REAL_RUN]) \
or (c == CheckMode.REAL_RUN and check_mode in [CheckMode.DONE]) \
or (c == CheckMode.DRY_RUN and check_mode in [CheckMode.DONE]))
self._check_mode_changed(c, check_mode, connections)
class RunEnvironmentAnsible(RunEnvironment):
ARGS = {
'ignore_errors': { 'required': False, 'default': False, 'type': 'str' },
'force_state_change': { 'required': False, 'default': False, 'type': 'bool' },
'provider': { 'required': True, 'default': None, 'type': 'str' },
'connections': { 'required': False, 'default': None, 'type': 'list' },
}
def __init__(self):
RunEnvironment.__init__(self)
self._run_results = []
self._log_idx = 0
from ansible.module_utils.basic import AnsibleModule
module = AnsibleModule(
argument_spec = self.ARGS,
supports_check_mode = True,
)
self.module = module
@property
def ifcfg_header(self):
return '# this file was created by ansible'
def run_command(self, argv, encoding = None):
return self.module.run_command(argv, encoding = encoding)
def _run_results_push(self, n_connections):
c = []
for cc in range(0, n_connections + 1):
c.append({
'log': [],
})
self._run_results.append(c)
@property
def run_results(self):
return self._run_results[-1]
def _check_mode_changed(self, old_check_mode, new_check_mode, connections):
if old_check_mode is None:
self._run_results_push(len(connections))
elif old_check_mode == CheckMode.PREPARE:
self._run_results_push(len(self.run_results) - 1)
elif old_check_mode == CheckMode.PRE_RUN:
# when switching from RRE_RUN to REAL_RUN, we drop the run-results
# we just collected and reset to empty. The PRE_RUN succeeded.
n_connections = len(self.run_results) - 1
del self._run_results[-1]
self._run_results_push(n_connections)
def log(self,
connections,
idx,
severity,
msg,
is_changed = False,
ignore_errors = False,
warn_traceback = False,
force_fail = False):
assert(idx >= -1)
self._log_idx += 1
self.run_results[idx]['log'].append((severity, msg, self._log_idx))
if severity == LogLevel.ERROR:
if force_fail \
or not ignore_errors:
self.fail_json(connections, 'error: %s' % (msg), changed = is_changed, warn_traceback = warn_traceback)
def _complete_kwargs_loglines(self, rr, connections, idx):
if idx == len(connections):
prefix = '#'
else:
c = connections[idx]
prefix = '#%s, state:%s' % (idx, c['state'])
if c['state'] != 'wait':
prefix = prefix + (', "%s"' % (c['name']))
for r in rr['log']:
yield (r[2], '[%03d] %s %s: %s' % (r[2], LogLevel.fmt(r[0]), prefix, r[1]))
def _complete_kwargs(self, connections, kwargs, traceback_msg = None):
if 'warnings' in kwargs:
logs = list(kwargs['warnings'])
else:
logs = []
l = []
for res in self._run_results:
for idx, rr in enumerate(res):
l.extend(self._complete_kwargs_loglines(rr, connections, idx))
l.sort(key = lambda x: x[0])
logs.extend([x[1] for x in l])
if traceback_msg is not None:
logs.append(traceback_msg)
kwargs['warnings'] = logs
return kwargs
def exit_json(self, connections, changed = False, **kwargs):
kwargs['changed'] = changed
self.module.exit_json(**self._complete_kwargs(connections, kwargs))
def fail_json(self, connections, msg, changed = False, warn_traceback = False, **kwargs):
traceback_msg = None
if warn_traceback:
traceback_msg = 'exception: %s' % (traceback.format_exc())
kwargs['msg'] = msg
kwargs['changed'] = changed
self.module.fail_json(**self._complete_kwargs(connections, kwargs, traceback_msg))
###############################################################################
class Cmd:
def __init__(self,
run_env,
connections_unvalidated,
connection_validator,
is_check_mode = False,
ignore_errors = False,
force_state_change = False):
self.run_env = run_env
self._connections_unvalidated = connections_unvalidated
self._connection_validator = connection_validator
self._is_check_mode = is_check_mode
self._ignore_errors = Util.boolean(ignore_errors)
self._force_state_change = Util.boolean(force_state_change)
self._connections = None
self._connections_data = None
self._check_mode = CheckMode.PREPARE
self._is_changed = False
def run_command(argv, encoding = None):
return self.run_env.run_command(argv, encoding = encoding)
@property
def is_changed(self):
return self._is_changed
@property
def connections(self):
c = self._connections
if c is None:
try:
c = self._connection_validator.validate(self._connections_unvalidated)
except ValidationError as e:
raise MyError('configuration error: %s' % (e))
self._connections = c
return c
@property
def connections_data(self):
c = self._connections_data
if c is None:
assert(self.check_mode in [CheckMode.DRY_RUN, CheckMode.PRE_RUN, CheckMode.REAL_RUN])
c = []
for idx in range(0, len(self.connections)):
c.append({
'changed': False,
})
self._connections_data = c
return c
def connections_data_reset(self):
for c in self.connections_data:
c['changed'] = False
self._is_changed = False
def connections_data_set_changed(self, idx, changed = True):
if not changed:
return
self.connections_data[idx]['changed'] = changed
if changed:
self._is_changed = True
def log_debug(self, idx, msg):
self.log(idx, LogLevel.DEBUG, msg)
def log_info(self, idx, msg):
self.log(idx, LogLevel.INFO, msg)
def log_warn(self, idx, msg):
self.log(idx, LogLevel.WARN, msg)
def log_error(self, idx, msg, warn_traceback = False, force_fail = False):
self.log(idx, LogLevel.ERROR, msg, warn_traceback = warn_traceback, force_fail = force_fail)
def log_fatal(self, idx, msg, warn_traceback = False):
self.log(idx, LogLevel.ERROR, msg, warn_traceback = warn_traceback, force_fail = True)
def log(self, idx, severity, msg, warn_traceback = False, force_fail = False):
self.run_env.log(connections,
idx,
severity,
msg,
is_changed = self.is_changed,
ignore_errors = self.connection_ignore_errors(connections[idx]),
warn_traceback = warn_traceback,
force_fail = force_fail)
@staticmethod
def create(provider, **kwargs):
if provider == 'nm':
return Cmd_nm(**kwargs)
elif provider == 'initscripts':
return Cmd_initscripts(**kwargs)
raise MyError('unsupported provider %s' % (provider))
def connection_force_state_change(self, connection):
v = connection['force_state_change']
if v is not None:
return v
return self._force_state_change
def connection_ignore_errors(self, connection):
v = connection['ignore_errors']
if v is not None:
return v
return self._ignore_errors
def connection_modified_earlier(self, idx):
# for index @idx, check if any of the previous profiles [0..idx[
# modify the connection.
con = self.connections[idx]
assert(con['state'] in ['up', 'down'])
# also check, if the current profile is 'up' with a 'type' (which
# possibly modifies the connection as well)
if con['state'] == 'up' \
and 'type' in con \
and self.connections_data[idx]['changed']:
return True
for i in reversed(range(idx)):
c = self.connections[i]
if 'name' not in c:
continue
if c['name'] != con['name']:
continue
c_state = c['state']
if c_state == 'up' and 'type' not in c:
pass
elif c_state == 'down':
return True
elif c_state == 'absent':
return True
elif c_state in ['present', 'up']:
if self.connections_data[idx]['changed']:
return True
return False
@property
def check_mode(self):
return self._check_mode
def check_mode_next(self):
if self._check_mode == CheckMode.PREPARE:
if self._is_check_mode:
c = CheckMode.DRY_RUN
else:
c = CheckMode.PRE_RUN
elif self.check_mode == CheckMode.PRE_RUN:
self.connections_data_reset()
c = CheckMode.REAL_RUN
elif self._check_mode != CheckMode.DONE:
c = CheckMode.DONE
else:
assert False
self._check_mode = c
self.run_env.check_mode_set(c)
return c
def run(self):
self.run_env.check_mode_set(CheckMode.PREPARE, self.connections)
for idx, connection in enumerate(self.connections):
try:
self._connection_validator.validate_connection_one(self.validate_one_type,
self.connections,
idx)
except ValidationError as e:
self.log_fatal(idx, str(e))
self.run_prepare()
while self.check_mode_next() != CheckMode.DONE:
for idx, connection in enumerate(self.connections):
try:
state = connection['state']
if state == 'wait':
w = connection['wait']
if w is None:
w = 10
self.log_info(idx, 'wait for %s seconds' % (w))
if self.check_mode == CheckMode.REAL_RUN:
import time
time.sleep(w)
elif state == 'absent':
self.run_state_absent(idx)
elif state == 'present':
self.run_state_present(idx)
elif state == 'up':
if 'type' in connection:
self.run_state_present(idx)
self.run_state_up(idx)
elif state == 'down':
self.run_state_down(idx)
else:
assert False
except Exception as e:
self.log_warn(idx, 'failure: %s [[%s]]' % (e, traceback.format_exc()))
raise
def run_prepare(self):
for idx, connection in enumerate(self.connections):
if 'type' in connection and connection['check_iface_exists']:
# when the profile is tied to a certain interface via 'interface_name' or 'mac',
# check that such an interface exists.
#
# This check has many flaws, as we don't check whether the existing
# interface has the right device type. Also, there is some ambiguity
# between the current MAC address and the permanent MAC address.
li_mac = None
li_ifname = None
if connection['mac']:
li_mac = SysUtil.link_info_find(mac = connection['mac'])
if not li_mac:
self.log_fatal(idx, 'profile specifies mac "%s" but no such interface exists' % (connection['mac']))
if connection['interface_name']:
li_ifname = SysUtil.link_info_find(ifname = connection['interface_name'])
if not li_ifname:
if connection['type'] == 'ethernet':
self.log_fatal(idx, 'profile specifies interface_name "%s" but no such interface exists' % (connection['interface_name']))
elif connection['type'] == 'infiniband':
if connection['infiniband_p_key'] in [None, -1]:
self.log_fatal(idx, 'profile specifies interface_name "%s" but no such infiniband interface exists' % (connection['interface_name']))
if li_mac and li_ifname and li_mac != li_ifname:
self.log_fatal(idx, 'profile specifies interface_name "%s" and mac "%s" but no such interface exists' % (connection['interface_name'], connection['mac']))
###############################################################################
class Cmd_nm(Cmd):
def __init__(self, **kwargs):
Cmd.__init__(self, **kwargs)
self._nmutil = None
self.validate_one_type = ArgValidator_ListConnections.VALIDATE_ONE_MODE_NM
@property
def nmutil(self):
if self._nmutil is None:
try:
nmclient = Util.NM().Client.new(None)
except Exception as e:
raise MyError('failure loading libnm library: %s' % (e))
self._nmutil = NMUtil(nmclient)
return self._nmutil
def run_prepare(self):
Cmd.run_prepare(self)
names = {}
for connection in self.connections:
if connection['state'] not in ['up', 'down', 'present', 'absent']:
continue
name = connection['name']
if not name:
assert(connection['state'] == 'absent')
continue;
if name in names:
exists = names[name]['nm.exists']
uuid = names[name]['nm.uuid']
else:
c = Util.first(self.nmutil.connection_list(name = name))
exists = (c is not None)
if c is not None:
uuid = c.get_uuid()
else:
uuid = Util.create_uuid()
names[name] = {
'nm.exists': exists,
'nm.uuid': uuid,
}
connection['nm.exists'] = exists
connection['nm.uuid'] = uuid
def run_state_absent(self, idx):
changed = False
seen = set()
name = self.connections[idx]['name']
black_list_names = None
if not name:
name = None
black_list_names = ArgUtil.connection_get_non_absent_names(self.connections)
while True:
connections = self.nmutil.connection_list(name = name, black_list_names = black_list_names, black_list = seen)
if not connections:
break
c = connections[-1]
seen.add(c)
self.connections_data_set_changed(idx)
self.log_info(idx, 'delete connection %s, %s' % (c.get_id(), c.get_uuid()))
if self.check_mode == CheckMode.REAL_RUN:
try:
self.nmutil.connection_delete(c)
except MyError as e:
self.log_error(idx, 'delete connection failed: %s' % (e))
if not seen:
self.log_info(idx, 'no connection "%s"' % (name))
def run_state_present(self, idx):
connection = self.connections[idx]
con_cur = Util.first(self.nmutil.connection_list(name = connection['name'], uuid = connection['nm.uuid']))
con_new = self.nmutil.connection_create(self.connections, idx, con_cur)
changed = False
if con_cur is None:
self.log_info(idx, 'add connection %s, %s' % (connection['name'], connection['nm.uuid']))
changed = True
try:
if self.check_mode == CheckMode.REAL_RUN:
con_cur = self.nmutil.connection_add(con_new)
except MyError as e:
self.log_error(idx, 'adding connection failed: %s' % (e))
elif not self.nmutil.connection_compare(con_cur, con_new, normalize_a = True):
changed = True
self.log_info(idx, 'update connection %s, %s' % (con_cur.get_id(), con_cur.get_uuid()))
if self.check_mode == CheckMode.REAL_RUN:
try:
self.nmutil.connection_update(con_cur, con_new)
except MyError as e:
self.log_error(idx, 'updating connection failed: %s' % (e))
else:
self.log_info(idx, 'connection %s, %s already up to date' % (con_cur.get_id(), con_cur.get_uuid()))
seen = set()
if con_cur is not None:
seen.add(con_cur)
while True:
connections = self.nmutil.connection_list(name = connection['name'], black_list = seen, black_list_uuids = [connection['nm.uuid']])
if not connections:
break
c = connections[-1]
self.log_info(idx, 'delete duplicate connection %s, %s' % (c.get_id(), c.get_uuid()))
changed = True
if self.check_mode == CheckMode.REAL_RUN:
try:
self.nmutil.connection_delete(c)
except MyError as e:
self.log_error(idx, 'delete duplicate connection failed: %s' % (e))
seen.add(c)
self.connections_data_set_changed(idx, changed)
def run_state_up(self, idx):
connection = self.connections[idx]
con = Util.first(self.nmutil.connection_list(name = connection['name'], uuid = connection['nm.uuid']))
if not con:
if self.check_mode == CheckMode.REAL_RUN:
self.log_error(idx, 'up connection %s, %s failed: no connection' % (connection['name'], connection['nm.uuid']))
else:
self.log_info(idx, 'up connection %s, %s' % (connection['name'], connection['nm.uuid']))
return
is_active = self.nmutil.connection_is_active(con)
is_modified = self.connection_modified_earlier(idx)
force_state_change = self.connection_force_state_change(connection)
if is_active and not force_state_change and not is_modified:
self.log_info(idx, 'up connection %s, %s skipped because already active' %
(con.get_id(), con.get_uuid()))
return
self.log_info(idx, 'up connection %s, %s (%s)' %
(con.get_id(), con.get_uuid(),
'not-active' if not is_active else \
'is-modified' if is_modified else \
'force-state-change'))
if self.check_mode == CheckMode.REAL_RUN:
try:
ac = self.nmutil.connection_activate (con)
except MyError as e:
self.log_error(idx, 'up connection failed: %s' % (e))
wait_time = connection['wait']
if wait_time is None:
wait_time = 90
try:
self.nmutil.connection_activate_wait(ac, wait_time)
except MyError as e:
self.log_error(idx, 'up connection failed while waiting: %s' % (e))
self.connections_data_set_changed(idx)
def run_state_down(self, idx):
connection = self.connections[idx]
cons = self.nmutil.connection_list(name = connection['name'])
changed = False
if cons:
seen = set()
while True:
ac = Util.first(self.nmutil.active_connection_list(connections = cons, black_list = seen))
if ac is None:
break
changed = True
seen.add(ac)
self.log_info(idx, 'down connection %s: %s' % (connection['name'], ac.get_path()))
if self.check_mode == CheckMode.REAL_RUN:
try:
self.nmutil.active_connection_deactivate(ac)
except MyError as e:
self.log_error(idx, 'down connection failed: %s' % (e))
wait_time = connection['wait']
if wait_time is None:
wait_time = 10
try:
self.nmutil.active_connection_deactivate_wait(ac, wait_time)
except MyError as e:
self.log_error(idx, 'down connection failed while waiting: %s' % (e))
cons = self.nmutil.connection_list(name = connection['name'])
if not changed:
self.log_info(idx, 'down connection %s failed: no connection' % (connection['name']))
self.connections_data_set_changed(idx, changed)
###############################################################################
class Cmd_initscripts(Cmd):
def __init__(self, **kwargs):
Cmd.__init__(self, **kwargs)
self.validate_one_type = ArgValidator_ListConnections.VALIDATE_ONE_MODE_INITSCRIPTS
def check_name(self, idx, name = None):
if name is None:
name = self.connections[idx]['name']
try:
f = IfcfgUtil.ifcfg_path(name)
except MyError as e:
self.log_error(idx, 'invalid name %s for connection' % (name))
return None
return f
def run_state_absent(self, idx):
changed = False
n = self.connections[idx]['name']
name = n
if not name:
names = []
black_list_names = ArgUtil.connection_get_non_absent_names(self.connections)
for f in os.listdir('/etc/sysconfig/network-scripts'):
if not f.startswith('ifcfg-'):
continue
name = f[6:]
if name in black_list_names:
continue
if name == 'lo':
continue
names.append(name)
else:
if not self.check_name(idx):
return
names = [name]
for name in names:
for path in IfcfgUtil.ifcfg_paths(name):
if not os.path.isfile(path):
continue
changed = True
self.log_info(idx, 'delete ifcfg-rh file "%s"' % (path))
if self.check_mode == CheckMode.REAL_RUN:
try:
os.unlink(path)
except Exception as e:
self.log_error(idx, 'delete ifcfg-rh file "%s" failed: %s' % (path, e))
if not changed:
self.log_info(idx, 'delete ifcfg-rh files for %s (no files present)' % ('"'+n+'"' if n else '*'))
self.connections_data_set_changed(idx, changed)
def run_state_present(self, idx):
if not self.check_name(idx):
return
connection = self.connections[idx]
name = connection['name']
old_content = IfcfgUtil.content_from_file(name)
ifcfg_all = IfcfgUtil.ifcfg_create(self.connections, idx,
lambda msg: self.log_warn(idx, msg),
old_content)
new_content = IfcfgUtil.content_from_dict(ifcfg_all,
header = self.run_env.ifcfg_header)
if old_content == new_content:
self.log_info(idx, 'ifcfg-rh profile "%s" already up to date' % (name))
return
op = 'add' if (old_content['ifcfg'] is None) else 'update'
self.log_info(idx, '%s ifcfg-rh profile "%s"' % (op, name))
if self.check_mode == CheckMode.REAL_RUN:
try:
IfcfgUtil.content_to_file(name, new_content)
except MyError as e:
self.log_error(idx, '%s ifcfg-rh profile "%s" failed: %s' % (op, name, e))
self.connections_data_set_changed(idx)
def _run_state_updown(self, idx, do_up):
if not self.check_name(idx):
return
connection = self.connections[idx]
name = connection['name']
if connection['wait'] is not None:
# initscripts don't support wait, they always block until the ifup/ifdown
# command completes. Silently ignore the argument.
pass
path = IfcfgUtil.ifcfg_path(name)
if not os.path.isfile(path):
if self.check_mode == CheckMode.REAL_RUN:
self.log_error(idx, 'ifcfg file "%s" does not exist' % (path))
else:
self.log_info(idx, 'ifcfg file "%s" does not exist in check mode' % (path))
return
is_active = IfcfgUtil.connection_seems_active(name)
is_modified = self.connection_modified_earlier(idx)
force_state_change = self.connection_force_state_change(connection)
if do_up:
if is_active is True and not force_state_change and not is_modified:
self.log_info(idx, 'up connection %s skipped because already active' %
(name))
return
self.log_info(idx, 'up connection %s (%s)' %
(name,
'not-active' if is_active is not True else \
'is-modified' if is_modified else \
'force-state-change'))
cmd = 'ifup'
else:
if is_active is False and not force_state_change:
self.log_info(idx, 'down connection %s skipped because not active' %
(name))
return
self.log_info(idx, 'up connection %s (%s)' %
(name,
'active' if is_active is not False else \
'force-state-change'))
cmd = 'ifdown'
if self.check_mode == CheckMode.REAL_RUN:
rc, out, err = self.run_env.run_command([cmd, name])
self.log_info(idx, 'call `%s %s`: rc=%d, out="%s", err="%s"' % (cmd, name, rc, out, err))
if rc != 0:
self.log_error(idx, 'call `%s %s` failed with exit status %d' % (cmd, name, rc))
self.connections_data_set_changed(idx)
def run_state_up(self, idx):
self._run_state_updown(idx, True)
def run_state_down(self, idx):
self._run_state_updown(idx, False)
###############################################################################
if __name__ == '__main__':
connections = None
cmd = None
run_env_ansible = RunEnvironmentAnsible()
try:
params = run_env_ansible.module.params
cmd = Cmd.create(params['provider'],
run_env = run_env_ansible,
connections_unvalidated = params['connections'],
connection_validator = ArgValidator_ListConnections(),
is_check_mode = run_env_ansible.module.check_mode,
ignore_errors = params['ignore_errors'],
force_state_change = params['force_state_change'])
connections = cmd.connections
cmd.run()
except Exception as e:
run_env_ansible.fail_json(connections,
'fatal error: %s' % (e),
changed = (cmd is not None and cmd.is_changed),
warn_traceback = not isinstance(e, MyError))
run_env_ansible.exit_json(connections,
changed = (cmd is not None and cmd.is_changed))