From edcb4a2850399ec4b0b88becbd64cfadc81180c7 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Fri, 16 Dec 2016 16:56:12 +0100 Subject: [PATCH] library: let the module handle all connections at once Instead, of having the tasks call the "network_connections.py" library for each connection profile individually (using with_items), pass all profiles at once. The advantage is: - the module can validate the input arguments better as it has access to all profiles. For example, when a slave connection refers to another master profile from the same play. Previously, each invocation of the module only sees the current profile and cannot verify whether the reference is valid. - while configuring the network, the play might need to shortly disconnect the control connection. In the previous way, after tearing down the network the target host becomes unreachable for ansible and the following steps cannot be executed anymore. Now, all steps are done as a whole on the target host, via one connection. If the host becomes unreachable for a short time, that is not a problem as long as the connectivty is restored at the end. Ansible also supports to switch the host IP (or SSH port). With this new way, the ansible play can apply a bunch of profiles autonomously and the ansible play can potentially handle a changing IP configuration. --- TEST/test-playbook-3.yml | 2 + library/network_connections.py | 1716 ++++++++++++++++----------- library/test_network_connections.py | 250 ++++ tasks/main.yml | 14 +- 4 files changed, 1267 insertions(+), 715 deletions(-) create mode 100755 library/test_network_connections.py diff --git a/TEST/test-playbook-3.yml b/TEST/test-playbook-3.yml index 3931f4c..3b421ba 100644 --- a/TEST/test-playbook-3.yml +++ b/TEST/test-playbook-3.yml @@ -9,6 +9,7 @@ type: ethernet - name: p-3-auto + - wait: 10 - name: p-3-manual autoconnect: no @@ -22,6 +23,7 @@ - "192.168.122.{{ network_iphost }}/24" - name: p-3-manual + - wait: 10 - name: p-3-br0 autoconnect: no diff --git a/library/network_connections.py b/library/network_connections.py index 40377fa..41a98c0 100755 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -12,24 +12,54 @@ description: Manage networking profiles (connections) for NetworkManager and ini 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, consule README.md for examples for the role. + this module outside the role. Thus, consult README.md for examples for the role. ''' import socket import sys import traceback -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, -else: - string_types = basestring, - ############################################################################### +class CheckMode: + 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 + 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 not None and pred(v): + continue + return v + return default + @classmethod def create_uuid(cls): cls.NM() @@ -73,18 +103,19 @@ class Util: return gmainloop @classmethod - def GMainLoop_iterate(cls, may_block = False): - cls.GMainLoop().get_context().iteration(may_block) + def GMainLoop_run(cls): + cls.GMainLoop().run() @classmethod - def create_nm_client(cls): - return cls.NM().Client.new(None) + def GMainLoop_iterate(cls, may_block = False): + return cls.GMainLoop().get_context().iteration(may_block) - @staticmethod - def kwargs_extend(d, **kwargs): - d = dict(d) - d.update(kwargs) - return d + @classmethod + def GMainLoop_iterate_all(cls): + r = False + while cls.GMainLoop_iterate(): + r = True + return r @staticmethod def ifname_valid(ifname): @@ -100,6 +131,21 @@ class Util: # FIXME: encoding issues regarding python unicode string return True + @staticmethod + def boolean(arg): + import ansible.module_utils.basic as basic + + if arg is None or isinstance(arg, bool): + return arg + if isinstance(arg, Util.STRING_TYPE): + arg = arg.lower() + if arg in basic.BOOLEANS_TRUE: + return True + elif arg in basic.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: @@ -118,151 +164,225 @@ class Util: return (socket.inet_ntop(family, a), family) @staticmethod - def parse_address(address): + 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 parse_address(address, family = None): result = {} try: parts = address.split() addr_parts = parts[0].split('/') if len(addr_parts) != 2: - raise Exception('expect two addr-parts: ADDR/PLEN') - a, family = Util.parse_ip(addr_parts[0]) + raise MyError('expect two addr-parts: ADDR/PLEN') + a, family = Util.parse_ip(addr_parts[0], family) result['address'] = a result['is_v4'] = (family == socket.AF_INET) result['family'] = family prefix = int(addr_parts[1]) if not (prefix >=0 and prefix <= (32 if family == socket.AF_INET else 128)): - raise Exception('invalid prefix %s' % (prefix)) + raise MyError('invalid prefix %s' % (prefix)) result['prefix'] = prefix if len(parts) > 1: - raise Exception('too many parts') + raise MyError('too many parts') except Exception as e: - raise Exception('invalid address "%s"' % (address)) + raise MyError('invalid address "%s"' % (address)) return result ############################################################################### -class _AnsibleUtil: +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 - ARGS = { - 'provider': { 'required': True, 'default': None, 'type': 'str' }, - 'name': { 'required': True, 'default': None, 'type': 'str' }, - 'state': { 'required': False, 'default': None, 'type': 'str' }, - 'wait': { 'required': False, 'default': 0, 'type': 'int' }, - 'type': { 'required': False, 'default': None, 'type': 'str' }, - 'autoconnect': { 'required': False, 'default': True, 'type': 'bool' }, - 'slave_type': { 'required': False, 'default': None, 'type': 'str' }, - 'master': { 'required': False, 'default': None, 'type': 'str' }, - 'interface_name': { 'required': False, 'default': None, 'type': 'str' }, - 'mac': { 'required': False, 'default': None, 'type': 'str' }, - 'vlan_id': { 'required': False, 'default': -1, 'type': 'int' }, - 'parent': { 'required': False, 'default': None, 'type': 'str' }, - 'ip': { 'required': False, 'default': None, 'type': 'dict' }, - } + if connection['state'] == 'absent': + c = None + elif 'type' in connection: + assert connection['state'] in ['up', 'present'] + c = connection + return c - def __init__(self): - from ansible.module_utils.basic import AnsibleModule +class ArgValidator: + MISSING = object() - self.AnsibleModule = AnsibleModule - self._module = None - self._rc = None - self._warnings = list() + def __init__(self, name = None, required = False, default_value = None): + self.name = name + self.required = required + self.default_value = default_value - @property - def module(self): - module = self._module - if module is None: - module = self.AnsibleModule( - argument_spec = self.ARGS, - supports_check_mode = True, - ) - self._module = module - return module - - @property - def params(self): - return self.module.params - - @property - def params_wait(self): - wait = AnsibleUtil.params['wait'] - if not (wait >= 0): - wait = 90 - return wait - - @property - def check_mode(self): - return self.module.check_mode - - def warn(self, msg): - self._warnings.append(msg) - - def check_type_boolean(self, key, value, allow_none = True): - if allow_none and value is None: - return None + def get_default_value(self): try: - if isinstance(value, bool): - return value - if isinstance(value, string_types) or isinstance(value, int): - return self.module.boolean(value) + return self.default_value() except: - pass - raise TypeError('%s must be a bool but is %s' % (key, value)) + return self.default_value - def check_type_int(self, key, value, allow_none = True, val_min = None, val_max = None): - if allow_none and value is None: - return None + 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): + ArgValidator.__init__(self, name, required, default_value) + self.enum_values = enum_values + 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)))) + return v + +class ArgValidatorInt(ArgValidator): + def __init__(self, name, required = False, val_min = None, val_max = None, default_value = 0): + ArgValidator.__init__(self, name, required, default_value) + self.val_min = val_min + self.val_max = val_max + def _validate(self, value, name): v = None try: if isinstance(value, int): v = value - if isinstance(value, string_types): + if isinstance(value, Util.STRING_TYPE): v = int(value) except: pass if v is None: - raise TypeError('%s must be an int but is %s' % (key, value)) - if val_min is not None and v < val_min: - raise TypeError('%s is %s but cannot be less then %s' % (key, v, val_min)) - if val_max is not None and v > val_max: - raise TypeError('%s is %s but cannot be greater then %s' % (key, v, val_max)) + 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 - @property - def ansible_managed(self): - return '# this file was created by ansible' - - @property - def rc(self): - return self._rc - - @rc.setter - def rc(self, value): - if self._rc is not None: - raise Exception('cannot set rc multiple times') - value = int(value) - self._rc = value - - def kwargs_whitelist(self, kwargs, allow_defaults, *allowed_args): - s = set(allowed_args) - if allow_defaults: - s.update(['provider', 'name', 'state']) - for key in kwargs: - if key in self.ARGS: - if kwargs[key] == self.ARGS[key]['default']: - continue - if key in s: - continue - raise Exception('command does not support variable %s' % (key)) - - def kwargs_whitelist_check(self, allow_defaults, *allowed_args): +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: - self.kwargs_whitelist(self.params, allow_defaults, *allowed_args) - except Exception as e: - self.fail_json(str(e)) + 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)) - def kwargs_check_connection_args_ip(self, ip): - if ip is None: - return { +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 ArgValidatorIPAddr(ArgValidatorStr): + def __init__(self, name, family = None, required = False, default_value = None): + ArgValidatorStr.__init__(self, name, required, default_value, None) + self.family = family + def _validate(self, value, name): + v = ArgValidatorStr._validate(self, value, name) + 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))) + +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): + 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 ArgValidator_DictIP(ArgValidatorDict): + def __init__(self): + ArgValidatorDict.__init__(self, + name = 'ip', + required = False, + nested = [ + ArgValidatorBool('dhcp4', default_value = None), + ArgValidatorBool('dhcp4_send_hostname', default_value = None), + ArgValidatorIP ('gateway4', family = socket.AF_INET), + ArgValidatorInt ('route_metric4', val_min = -1, val_max = 0xFFFFFFFF, default_value = None), + ArgValidatorBool('auto6'), + ArgValidatorIP ('gateway6', family = socket.AF_INET6), + ArgValidatorInt ('route_metric6', val_min = -1, val_max = 0xFFFFFFFF, default_value = None), + ArgValidatorList('address', + nested = ArgValidatorIPAddr('address[?]'), + default_value = list, + ), + ], + default_value = lambda: { 'ip_is_present': False, 'dhcp4': True, 'dhcp4_send_hostname': None, @@ -272,153 +392,178 @@ class _AnsibleUtil: 'gateway6': None, 'route_metric6': None, 'address': [], - } - valid_keys = [ - 'dhcp4', - 'dhcp4_send_hostname', - 'gateway4', - 'route_metric4', - 'auto6', - 'gateway6', - 'route_metric6', - 'address', - ] - for key in ip.keys(): - if key not in valid_keys: - raise Exception('invalid argument ip.%s' % (key)) - r = { - 'ip_is_present': True, - 'dhcp4': self.check_type_boolean('ip.dhcp4', ip.get('dhcp4', None)), - 'dhcp4_send_hostname': self.check_type_boolean('ip.dhcp4_send_hostname', ip.get('dhcp4_send_hostname', None)), - 'gateway4': Util.parse_ip(ip.get('gateway4', None), socket.AF_INET)[0], - 'route_metric4': self.check_type_int('route_metric4', ip.get('route_metric4', None), val_min = -1, val_max = 0xFFFFFFFF), - 'auto6': self.check_type_boolean('ip.auto6', ip.get('auto6', None)), - 'gateway6': Util.parse_ip(ip.get('gateway6', None), socket.AF_INET6)[0], - 'route_metric6': self.check_type_int('route_metric6', ip.get('route_metric6', None), val_min = -1, val_max = 0xFFFFFFFF), - 'address': list([Util.parse_address(a) for a in ip.get('address',[])]), - } - if r['dhcp4'] is None: - r['dhcp4'] = r['dhcp4_send_hostname'] is not None or not any([a for a in r['address'] if a['is_v4']]) - if r['auto6'] is None: - r['auto6'] = not any([a for a in r['address'] if not a['is_v4']]) - if not r['dhcp4'] and r['dhcp4_send_hostname'] is not None: - raise Exception('ip.dhcp4_send_hostname can only be set with ip.dhcp4') - return r + }, + all_missing_during_validate = False, + ) - def kwargs_check_connection_args(self, kwargs): - args = { } - handled_keys = set() - - args['name'] = kwargs['name'] - handled_keys.add('name') - - if not kwargs['type']: - raise Exception('missing "type" property') + def _validate_post(self, value, name, result): + if 'ip_is_present' not in result: + result['ip_is_present'] = True + 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['is_v4']]) + if result['auto6'] is None: + result['auto6'] = not any([a for a in result['address'] if not a['is_v4']]) + if result['dhcp4_send_hostname'] is None: + result['dhcp4_send_hostname'] = False else: - if kwargs['type'] not in [ 'ethernet', 'bridge', 'team', 'bond', 'vlan' ]: - raise Exception('invalid type "%s"' % (kwargs['type'])) - args['type'] = kwargs['type'] - handled_keys.add('type') + if not result['dhcp4']: + raise ValidationError(name, '"dhcp4_send_hostname" is only valid if "dhcp4" is enabled') + return result - if kwargs['slave_type'] is not None: - if kwargs['slave_type'] not in [ 'bridge', 'bond', 'team' ]: - raise Exception('invalid slave_type "%s"' % (kwargs['slave_type'])) - if kwargs['master'] is None: - raise Exception('A slave_type "%s" requires a master' % (kwargs['slave_type'])) - args['slave_type'] = kwargs['slave_type'] - args['master'] = kwargs['master'] - elif kwargs['master'] is not None: - raise Exception('A slave with a "master" property needs a "slave_type" specified') - handled_keys.add('slave_type') - handled_keys.add('master') +class ArgValidator_DictConnection(ArgValidatorDict): - if 'slave_type' in args: - if kwargs['ip'] is not None: - raise Exception('slave type "%s" does not support an ip configuration' % (args['slave_type'])) - args['ip'] = self.kwargs_check_connection_args_ip(kwargs['ip']) - handled_keys.add('ip') + VALID_STATES = ['up', 'down', 'present', 'absent', 'wait'] + VALID_TYPES = [ 'ethernet', 'bridge', 'team', 'bond', 'vlan' ] + VALID_SLAVE_TYPES = [ 'bridge', 'bond', 'team' ] - if kwargs['mac']: - if kwargs['type'] not in [ 'ethernet' ]: - raise Exception('mac is not supported for type "%s"' % (kwargs['type'])) - args['mac'] = kwargs['mac'] - handled_keys.add('mac') + def __init__(self): + ArgValidatorDict.__init__(self, + name = 'connections[?]', + required = False, + nested = [ + ArgValidatorStr ('name'), + ArgValidatorStr ('state', enum_values = ArgValidator_DictConnection.VALID_STATES), + ArgValidatorInt ('wait', val_min = -1, val_max = 1200), + 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'), + ArgValidatorStr ('mac'), + ArgValidatorStr ('parent'), + ArgValidatorInt ('vlan_id', val_min = 0, val_max = 4095, default_value = None), + ArgValidatorStr ('on_error', enum_values = ['continue', 'fail'], default_value = 'fail'), + ArgValidator_DictIP(), + ], + default_value = dict, + all_missing_during_validate = True, + ) - args['interface_name'] = kwargs['interface_name'] - handled_keys.add('interface_name') - if not args['interface_name']: - if kwargs['type'] in [ 'bridge', 'bond', 'team', 'vlan' ]: - args['interface_name'] = kwargs['name'] - if args['interface_name'] is not None and not Util.ifname_valid(args['interface_name']): - raise Exception('invalid interface-name "%s"' % (args['interface_name'])) + 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 args['type'] == 'vlan': - if kwargs['vlan_id'] == -1: - raise Exception('missing vlan_id') - try: - i = int(kwargs['vlan_id']) - if i < 0 or i >= 4095: - i = None - except: - i = None - if i is None: - raise Exception('invalid vlan_id "%s"' % (kwargs['vlan_id'])) - args['vlan_id'] = str(i) - elif kwargs['vlan_id'] != -1: - raise Exception('vlan_id not allowed for type "%s"' % (args['type'])) - handled_keys.add('vlan_id') - - if args['type'] == 'vlan': - if not kwargs['parent']: - raise Exception('vlan needs a parent connection') - args['parent'] = kwargs['parent'] + 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') + elif result['state'] in ['up', 'down']: + VALID_FIELDS = ['name', 'state', 'wait', 'on_error'] + elif result['state'] == 'absent': + VALID_FIELDS = ['name', 'state', 'on_error'] + elif result['state'] == 'wait': + VALID_FIELDS = ['state', 'wait'] else: - if kwargs['parent']: - raise Exception('type "%s" does not support needs a parent connection') - handled_keys.add('parent') + assert False - args['autoconnect'] = kwargs['autoconnect'] - handled_keys.add('autoconnect') + 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'])) - for k in kwargs: - if k in [ 'provider', 'state', 'wait' ]: + if result['state'] != 'wait': + if 'name' not in result: + raise ValidationError(name, 'missing "name"') + if not result['name']: + raise ValidationError(name + '.name', 'empty "name" is invalid') + + if result['state'] == 'wait': + if result.get('wait', -1) == -1: + result['wait'] = 10 + elif result['wait'] == 0: + raise ValidationError(name + '.wait', 'the "wait" value for state "wait" must be positive') + elif result['state'] in ['up', 'down']: + if result.get('wait', -1) == -1: + result['wait'] = 90 + 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 'slave_type' in result: + if 'master' not in result: + raise ValidationError(name + '.slave_type', '"slave_type" requires a "master" property') + if result['master'] == result['name']: + raise ValidationError(name + '.master', '"master" cannot refer to itself') + else: + if 'master' in result: + raise ValidationError(name + '.master', '"master" requires a "slave_type" property') + + if 'ip' in result: + if 'slave_type' in result: + raise ValidationError(name + '.ip', 'a "slave_type" cannot have an "ip" property') + else: + if 'slave_type' not in result: + result['ip'] = self.nested['ip'].get_default_value() + + if 'mac' in result: + if result['type'] != 'ethernet': + raise ValidationError(name + '.mac', 'a "mac" address is only allowed for type "ethernet"') + + 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"') + if result['parent'] == result['name']: + raise ValidationError(name + '.parent', '"parent" cannot refer to itself') + else: + if 'vlan_id' in result: + raise ValidationError(name + '.vlan_id', '"vlan_id" is only allowed for "type" "vlan"') + if 'parent' in result: + raise ValidationError(name + '.parent', '"parent" is only allowed for "type" "vlan"') + + for k in VALID_FIELDS: + if k in result: continue - if k not in handled_keys: - raise Exception('unsupported key "%s"' % (k)) + v = self.nested[k] + vv = v.get_default_value() + if vv is not ArgValidator.MISSING: + result[k] = vv - return args + return result - def _complete_kwargs(self, kwargs): - if 'warnings' in kwargs: - kwargs['warnings'] = self._warnings + kwargs['warnings'] - else: - kwargs['warnings'] = self._warnings - if 'rc' in kwargs: - self.rc = kwargs['rc'] - rc = self.rc - else: - rc = self.rc - if rc is not None: - kwargs['rc'] = rc - return kwargs +class ArgValidator_ListConnections(ArgValidatorList): + def __init__(self): + ArgValidatorList.__init__(self, + name = 'connections', + nested = ArgValidator_DictConnection(), + default_value = list + ) - def exit_json(self, changed = True, **kwargs): - kwargs['changed'] = changed - self.module.exit_json(**self._complete_kwargs(kwargs)) - - def fail_json(self, msg, **kwargs): - kwargs['msg'] = msg - self.module.fail_json(**self._complete_kwargs(kwargs)) - - def fail_json_check(self, msg, **kwargs): - if self.check_mode: - self.warn("would-fail: " + msg) - if 'changed' not in kwargs: - kwargs['changed'] = True - self.exit_json(**kwargs) - self.fail_json(msg, **kwargs) - -AnsibleUtil = _AnsibleUtil() + 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', 'references non-existing connection "%s"' % (connection['name'])) + if 'type' in connection: + if connection['master']: + if not ArgUtil.connection_find_by_name(connection['master'], result, idx): + raise ValidationError(name + '[' + str(idx) + '].master', 'references non-existing "master" connection "%s"' % (connection['master'])) + 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 ############################################################################### @@ -456,11 +601,11 @@ class IfcfgUtil: n == '.' or \ n == '..' or \ n.find('/') != -1: - raise Exception('invalid ifcfg-name %s' % (name)) + raise MyError('invalid ifcfg-name %s' % (name)) if file_type is None: file_type = 'ifcfg' if file_type not in cls.FILE_TYPES: - raise Exception('invalid file-type %s' % (file_type)) + raise MyError('invalid file-type %s' % (file_type)) return '/etc/sysconfig/network-scripts/' + file_type + '-' + n @classmethod @@ -511,92 +656,71 @@ class IfcfgUtil: return s @classmethod - def ifcfg_find_master_from_file(cls, args, field): - content = cls.content_from_file(args[field], 'ifcfg') - if content['ifcfg'] is None: - raise Exception('cannot lookup %s connection from file "ifcfg-%s"' % (field, args[field])) - cdict = cls.content_to_dict(content, 'ifcfg') - ifcfg = cdict['ifcfg'] - if 'DEVICE' not in ifcfg: - raise Exception('cannot lookup DEVICE in %s connection from file "ifcfg-%s"' % (field, args[field])) - if not Util.ifname_valid(ifcfg['DEVICE']): - raise Exception('invalid DEVICE for %s connection in file "ifcfg-%s"' % (field, args[field])) - return ifcfg['DEVICE'] + def _connection_find_master(cls, 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 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'] @classmethod - def ifcfg_find_master(cls, args, field, check_mode = False): - try: - return cls.ifcfg_find_master_from_file(args, field) - except: - if not check_mode: - raise - return None + def ifcfg_create(cls, connections, idx, warn_fcn): + connection = connections[idx] + ip = connection['ip'] - @classmethod - def ifcfg_create(cls, check_mode, **kwargs): ifcfg_all = {} for file_type in cls.FILE_TYPES: ifcfg_all[file_type] = {} ifcfg = ifcfg_all['ifcfg'] - dirty = False - - args = AnsibleUtil.kwargs_check_connection_args(kwargs) - ip = args['ip'] - if ip['dhcp4_send_hostname'] is not None: - AnsibleUtil.warn('ip.dhcp4_send_hostname is not supported by initscripts provider') + warn_fcn('ip.dhcp4_send_hostname is not supported by initscripts provider') if ip['route_metric4'] is not None and ip['route_metric4'] >= 0: - AnsibleUtil.warn('ip.route_metric4 is not supported by initscripts provider') + warn_fcn('ip.route_metric4 is not supported by initscripts provider') if ip['route_metric6'] is not None and ip['route_metric6'] >= 0: - AnsibleUtil.warn('ip.route_metric6 is not supported by initscripts provider') + warn_fcn('ip.route_metric6 is not supported by initscripts provider') ifcfg['NM_CONTROLLED'] = 'no' - if args['autoconnect']: + if connection['autoconnect']: ifcfg['ONBOOT'] = 'yes' - ifcfg['DEVICE'] = args['interface_name'] + ifcfg['DEVICE'] = connection['interface_name'] - if args['type'] == 'ethernet': + if connection['type'] == 'ethernet': ifcfg['TYPE'] = 'Ethernet' - ifcfg['HWADDR'] = args['mac'] - elif args['type'] == 'bridge': + ifcfg['HWADDR'] = connection['mac'] + elif connection['type'] == 'bridge': ifcfg['TYPE'] = 'Bridge' - elif args['type'] == 'bond': + elif connection['type'] == 'bond': ifcfg['TYPE'] = 'Bond' ifcfg['BONDING_MASTER'] = 'yes' - elif args['type'] == 'team': + elif connection['type'] == 'team': ifcfg['DEVICETYPE'] = 'Team' - elif args['type'] == 'vlan': + elif connection['type'] == 'vlan': ifcfg['VLAN'] = 'yes' ifcfg['TYPE'] = 'Vlan' - m = cls.ifcfg_find_master(args, 'parent', check_mode) - if m is None: - dirty = True - else: - ifcfg['PHYSDEV'] = m - ifcfg['VID'] = args['vlan_id'] + ifcfg['PHYSDEV'] = cls._connection_find_master(connection['parent'], connections, idx) + ifcfg['VID'] = connection['vlan_id'] else: - raise Exception('unsupported type %s' % (args['type'])) + raise MyError('unsupported type %s' % (connection['type'])) - if 'slave_type' in args: - m = cls.ifcfg_find_master(args, 'master', check_mode) - if m is None: - dirty = True - if args['slave_type'] == 'bridge': + if connection['slave_type'] is not None: + m = cls._connection_find_master(connection['master'], connections, idx) + if connection['slave_type'] == 'bridge': ifcfg['BRIDGE'] = m - elif args['slave_type'] == 'bond': + elif connection['slave_type'] == 'bond': ifcfg['MASTER'] = m ifcfg['SLAVE'] = 'yes' - elif args['slave_type'] == 'team': + elif connection['slave_type'] == 'team': ifcfg['TEAM_MASTER'] = m if 'TYPE' in ifcfg: del ifcfg['TYPE'] - if args['type'] != 'team': + if connection['type'] != 'team': ifcfg['DEVICETYPE'] = 'TeamPort' else: - raise Exception('invalid slave_type "%s"' % (args['slave_type'])) + raise MyError('invalid slave_type "%s"' % (connection['slave_type'])) else: addrs4 = list([a for a in ip['address'] if a['is_v4']]) addrs6 = list([a for a in ip['address'] if not a['is_v4']]) @@ -611,7 +735,7 @@ class IfcfgUtil: 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']: + if ip['gateway4'] is not None: ifcfg['GATEWAY'] = ip['gateway4'] if ip['auto6']: @@ -626,7 +750,7 @@ class IfcfgUtil: 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']: + if ip['gateway6'] is not None: ifcfg['IPV6_DEFAULTGW'] = ip['gateway6'] for file_type in cls.FILE_TYPES: @@ -638,7 +762,7 @@ class IfcfgUtil: if type(h[key]) == type(True): h[key] = 'yes' if h[key] else 'no' - return (ifcfg_all, dirty) + return ifcfg_all @classmethod def ifcfg_parse_line(cls, line): @@ -678,6 +802,8 @@ class IfcfgUtil: ifcfg[val[0]] = val[1] return ifcfg + ANSIBLE_MANAGED = '# this file was created by ansible' + @classmethod def content_from_dict(cls, ifcfg_all, file_type = None): content = {} @@ -687,11 +813,11 @@ class IfcfgUtil: if file_type != 'ifcfg': content[file_type] = None continue - s = AnsibleUtil.ansible_managed + '\n' + s = cls.ANSIBLE_MANAGED + '\n' for key in sorted(h.keys()): value = h[key] if not cls.KeyValid(key): - raise Exception('invalid ifcfg key %s' % (key)) + raise MyError('invalid ifcfg key %s' % (key)) if value is not None: s += key + '=' + cls.ValueEscape(value) + '\n' content[file_type] = s @@ -719,7 +845,7 @@ class IfcfgUtil: @classmethod def content_to_file(cls, name, content, file_type = None): - import os, errno + import os for file_type in cls._file_types(file_type): path = cls.ifcfg_path(name, file_type) h = content[file_type] @@ -727,6 +853,7 @@ class IfcfgUtil: try: os.unlink(path) except OSError as e: + import errno if e.errno != errno.ENOENT: raise else: @@ -735,12 +862,14 @@ class IfcfgUtil: ############################################################################### -class NMCmd: +class NMUtil: def __init__(self, nmclient): self.nmclient = nmclient - def active_connection_list(self, connections = None, black_list = None): + def active_connection_list(self, connections = None, black_list = None, mainloop_iterate = True): + if mainloop_iterate: + Util.GMainLoop_iterate_all() active_cons = self.nmclient.get_active_connections() if connections: connections = set(connections) @@ -750,7 +879,9 @@ class NMCmd: active_cons = list(active_cons) return active_cons; - def connection_list(self, name = None, uuid = None, black_list = None): + def connection_list(self, name = None, uuid = None, black_list = None, black_list_uuid = None, mainloop_iterate = True): + if mainloop_iterate: + Util.GMainLoop_iterate_all() cons = self.nmclient.get_connections() if name is not None: cons = [c for c in cons if c.get_id() == name] @@ -759,260 +890,386 @@ class NMCmd: if black_list: cons = [c for c in cons if c not in black_list] + if black_list_uuid: + cons = [c for c in cons if c.get_uuid() not in black_list] cons = list(cons) - def _get_timestamp(connection): - s_con = connection.get_setting_connection() - if not s_con: - return 0L - return s_con.get_timestamp() - cons.sort(key = _get_timestamp) + 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_find_master_connection(self, args, field, expected_type = None): - # we lookup the master/parent by the ID. That is different from NetworkManager, for - # which connection.master either names the ifname of the parent interface - # or the UUID of the parent connection. - cons = self.connection_list(name = args[field]) - if not cons: - raise Exception('a %s connection "%s" was not found' % (field, args[field])) - if len(cons) != 1: - raise Exception('a unique %s connection "%s" was not found, instead there are [ %s ]' % (field, args[field], ' '.join([c.get_uuid() for c in cons]))) - con = cons[0] - if expected_type is not None and con.get_connection_type() != expected_type: - raise Exception('the %s connection "%s" is expected to be of type %s but is %s (%s)' % (field, args[field], expected_type, con.get_connection_type(), con.get_uuid())) - return con - - def connection_find_master(self, args, field, check_mode = False, expected_type = None): - try: - con = self.connection_find_master_connection(args, field, expected_type) - except: - if not check_mode: - raise - return None - return con.get_uuid() - - def _ip_settings_create(self, ip, connection = None): + def connection_compare(self, con_a, con_b, normalize_a = False, normalize_b = False, compare_flags = None): NM = Util.NM() - s_ip4 = NM.SettingIP4Config.new() - s_ip6 = NM.SettingIP6Config.new() + 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 - s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto') - s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, 'auto') + return not(not(con_a.compare (con_b, compare_flags))) - addrs4 = list([a for a in ip['address'] if a['is_v4']]) - addrs6 = list([a for a in ip['address'] if not a['is_v4']]) + @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)) + assert c.get('nm.uuid', None) + return c['nm.uuid'] - 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']: - 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']) - - 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']: - 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']) - - if connection is not None: - connection.add_setting(s_ip4) - connection.add_setting(s_ip6) - return (s_ip4, s_ip6) - - def connection_create(self, check_mode, uuid = None, **kwargs): + def connection_create(self, connections, idx): NM = Util.NM() - if uuid is None: - uuid = Util.create_uuid() + connection = connections[idx] - dirty = False - - args = AnsibleUtil.kwargs_check_connection_args(kwargs) - - connection = NM.SimpleConnection.new() + con = NM.SimpleConnection.new() s_con = NM.SettingConnection.new() - connection.add_setting(s_con) + con.add_setting(s_con) - s_con.set_property(NM.SETTING_CONNECTION_ID, args['name']) - s_con.set_property(NM.SETTING_CONNECTION_UUID, uuid) - s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, args['autoconnect']) - s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, args['interface_name']) + 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 args['type'] == 'ethernet': + if connection['type'] == 'ethernet': s_con.set_property(NM.SETTING_CONNECTION_TYPE, '802-3-ethernet') s_wired = NM.SettingWired.new() - connection.add_setting(s_wired) - s_wired.set_property(NM.SETTING_WIRED_MAC_ADDRESS, args['mac']) - elif args['type'] == 'bridge': + con.add_setting(s_wired) + s_wired.set_property(NM.SETTING_WIRED_MAC_ADDRESS, connection['mac']) + elif connection['type'] == 'bridge': s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'bridge') s_bridge = NM.SettingBridge.new() - connection.add_setting(s_bridge) + con.add_setting(s_bridge) s_bridge.set_property(NM.SETTING_BRIDGE_STP, False) - elif args['type'] == 'bond': + elif connection['type'] == 'bond': s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'bond') - elif args['type'] == 'team': + elif connection['type'] == 'team': s_con.set_property(NM.SETTING_CONNECTION_TYPE, 'team') - elif args['type'] == 'vlan': + elif connection['type'] == 'vlan': s_vlan = NM.SettingVlan.new() - connection.add_setting(s_vlan) - s_vlan.set_property(NM.SETTING_VLAN_ID, int(args['vlan_id'])) - m = self.connection_find_master(args, 'parent', check_mode) - if m is None: - dirty = True - else: - s_vlan.set_property(NM.SETTING_VLAN_PARENT, m) + con.add_setting(s_vlan) + s_vlan.set_property(NM.SETTING_VLAN_ID, int(connection['vlan_id'])) + s_vlan.set_property(NM.SETTING_VLAN_PARENT, self._connection_find_master_uuid(connection['parent'], connections, idx)) else: - raise Exception('unsupported type %s' % (args['type'])) + raise MyError('unsupported type %s' % (connection['type'])) - if 'slave_type' in args: - s_con.set_property(NM.SETTING_CONNECTION_SLAVE_TYPE, args['slave_type']) - m = self.connection_find_master(args, 'master', check_mode, args['slave_type']) - if m is None: - dirty = True - else: - s_con.set_property(NM.SETTING_CONNECTION_MASTER, m) + if connection['slave_type'] is not None: + s_con.set_property(NM.SETTING_CONNECTION_SLAVE_TYPE, connection['slave_type']) + s_con.set_property(NM.SETTING_CONNECTION_MASTER, self._connection_find_master_uuid(connection['master'], connections, idx)) else: - self._ip_settings_create(args['ip'], connection) + ip = connection['ip'] + + s_ip4 = NM.SettingIP4Config.new() + s_ip6 = NM.SettingIP6Config.new() + + 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['is_v4']]) + addrs6 = list([a for a in ip['address'] if not a['is_v4']]) + + 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']) + + 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']) + + con.add_setting(s_ip4) + con.add_setting(s_ip6) try: - connection.normalize() + con.normalize() except Exception as e: - if not check_mode: - raise Exception('failure to create normalized connection: %s' % (e)) - return (connection, dirty) + raise MyError('created connection failed to normalize: %s' % (e)) + return con + def connection_add(self, con): - def _connection_add_cb(self, client, result, cb_args): - con = None - try: - con = client.add_connection_finish(result) - except Exception as e: - cb_args['error'] = str(e) - cb_args['con'] = con - Util.GMainLoop().quit() - - def connection_add(self, check_mode = False, **kwargs): - connection, dirty = self.connection_create(check_mode, **kwargs) - if check_mode: - return True + def add_cb(client, result, cb_args): + con = None + try: + con = client.add_connection_finish(result) + except Exception as e: + cb_args['error'] = str(e) + cb_args['con'] = con + Util.GMainLoop().quit() cb_args = {} - self.nmclient.add_connection_async(connection, True, - None, self._connection_add_cb, cb_args) - Util.GMainLoop().run() + self.nmclient.add_connection_async(con, True, + None, add_cb, cb_args) + Util.GMainLoop_run() if not cb_args.get('con', None): - raise Exception('failure to add connection: %s' % (cb_args.get('error', 'unknown error'))) - return True + raise MyError('failure to add connection: %s' % (cb_args.get('error', 'unknown error'))) + return cb_args['con'] - - def _connection_update_cb(self, connection, result, cb_args): - success = False - try: - success = connection.commit_changes_finish(result) - except Exception as e: - cb_args['error'] = str(e) - cb_args['success'] = success - Util.GMainLoop().quit() - - def connection_update(self, connection, check_mode = False, **kwargs): + def connection_update(self, con, con_new): NM = Util.NM() - connection_new, dirty = self.connection_create(check_mode, **Util.kwargs_extend (kwargs, uuid = connection.get_uuid())) + con.replace_settings_from_connection(con_new) - connection_cur = NM.SimpleConnection.new_clone(connection) - try: - connection_cur.normalize() - except: - pass - changed = not connection_cur.compare (connection_new, NM.SettingCompareFlags.IGNORE_TIMESTAMP) - - if check_mode: - return changed or dirty - - if not changed: - return False - - connection.replace_settings_from_connection(connection_new) + def update_cb(connection, result, cb_args): + success = False + try: + success = connection.commit_changes_finish(result) + except Exception as e: + cb_args['error'] = str(e) + cb_args['success'] = success + Util.GMainLoop().quit() cb_args = {} - connection.commit_changes_async(True, None, self._connection_update_cb, cb_args) - Util.GMainLoop().run() + con.commit_changes_async(True, None, update_cb, cb_args) + Util.GMainLoop_run() if not cb_args.get('success', False): - raise Exception('failure to update connection: %s' % (cb_args.get('error', 'unknown error'))) + raise MyError('failure to update connection: %s' % (cb_args.get('error', 'unknown error'))) return True - - def _connection_delete_cb(self, connection, result, cb_args): - success = False - try: - success = connection.delete_finish(result) - except Exception as e: - cb_args['error'] = str(e) - cb_args['success'] = success - Util.GMainLoop().quit() - def connection_delete(self, connection): + + def delete_cb(connection, result, cb_args): + success = False + try: + success = connection.delete_finish(result) + except Exception as e: + cb_args['error'] = str(e) + cb_args['success'] = success + Util.GMainLoop().quit() + cb_args = {} - connection.delete_async(None, self._connection_delete_cb, cb_args) - Util.GMainLoop().run() + connection.delete_async(None, delete_cb, cb_args) + Util.GMainLoop_run() if not cb_args.get('success', False): - raise Exception('failure to delete connection: %s' % (cb_args.get('error', 'unknown error'))) - - - def _connection_activate_cb(self, client, result, cb_args): - active_connection = False - try: - active_connection = client.activate_connection_finish(result) - except Exception as e: - cb_args['error'] = str(e) - cb_args['active_connection'] = active_connection - Util.GMainLoop().quit() + raise MyError('failure to delete connection: %s' % (cb_args.get('error', 'unknown error'))) def connection_activate(self, connection): + + def activate_cb(client, result, cb_args): + active_connection = False + try: + active_connection = client.activate_connection_finish(result) + except Exception as e: + cb_args['error'] = str(e) + cb_args['active_connection'] = active_connection + Util.GMainLoop().quit() + cb_args = {} - self.nmclient.activate_connection_async(connection, None, None, None, self._connection_activate_cb, cb_args) - Util.GMainLoop().run() + self.nmclient.activate_connection_async(connection, None, None, None, activate_cb, cb_args) + Util.GMainLoop_run() if not cb_args.get('active_connection', None): - raise Exception('failure to activate connection: %s' % (cb_args.get('error', 'unknown error'))) + raise MyError('failure to activate connection: %s' % (cb_args.get('error', 'unknown error'))) return cb_args['active_connection'] - - def _active_connection_deactivate_cb(self, client, result, cb_args): - success = False - try: - success = client.deactivate_connection_finish(result) - except Exception as e: - cb_args['error'] = str(ex) - cb_args['success'] = success - Util.GMainLoop().quit() - def active_connection_deactivate(self, ac): + + def deactivate_cb(client, result, cb_args): + success = False + try: + success = client.deactivate_connection_finish(result) + except Exception as e: + cb_args['error'] = str(ex) + cb_args['success'] = success + Util.GMainLoop().quit() + cb_args = {} - self.nmclient.deactivate_connection_async(ac, None, self._active_connection_deactivate_cb, cb_args) - Util.GMainLoop().run() + self.nmclient.deactivate_connection_async(ac, None, deactivate_cb, cb_args) + Util.GMainLoop_run() if not cb_args.get('success', False): - raise Exception('failure to deactivate connection: %s' % (cb_args.get('error', 'unknown error'))) + raise MyError('failure to deactivate connection: %s' % (cb_args.get('error', 'unknown error'))) return True ############################################################################### +class _AnsibleUtil: + + ARGS = { + 'provider': { 'required': True, 'default': None, 'type': 'str' }, + 'connections': { 'required': False, 'default': None, 'type': 'list' }, + } + + ARGS_CONNECTIONS = ArgValidator_ListConnections() + + def __init__(self): + from ansible.module_utils.basic import AnsibleModule + + self.AnsibleModule = AnsibleModule + self._module = None + self._connections = None + self._run_results = None + self._check_mode = None + + @property + def check_mode(self): + if self._check_mode == None: + raise MyError('check_mode is not initialized') + return self._check_mode + + def check_mode_next(self): + if self._check_mode == None: + if self.module.check_mode: + self._check_mode = CheckMode.DRY_RUN + else: + self._check_mode = CheckMode.PRE_RUN + return self._check_mode + if self.check_mode == CheckMode.PRE_RUN: + self._run_results = None + self._check_mode = CheckMode.REAL_RUN + return CheckMode.REAL_RUN + if self._check_mode != CheckMode.DONE: + self._check_mode = CheckMode.DONE + return CheckMode.DONE + assert False + + @property + def module(self): + module = self._module + if module is None: + module = self.AnsibleModule( + argument_spec = self.ARGS, + supports_check_mode = True, + ) + self._module = module + return module + + @property + def params(self): + return self.module.params + + @property + def connections(self): + c = self._connections + if c is None: + try: + c = self.ARGS_CONNECTIONS.validate(self.params['connections']) + except ValidationError as e: + self.fail_json('configuration error: %s' % (e), + warn_traceback = True) + self._connections = c + return c + + @property + def run_results(self): + c = self._run_results + if c is None: + c = [] + for cc in self.connections: + c.append({ + 'changed': False, + 'log': [], + 'rc': [], + }) + self._run_results = c + return c + + def run_results_changed(self, idx, changed = None): + if changed is None: + changed = True + self.run_results[idx]['changed'] = bool(changed) + + def run_results_rc(self, idx, rc, msg): + self.run_results[idx]['rc'].append((rc, msg)) + self.log(idx, LogLevel.INFO, 'command: %s (rc=%s)' % (msg, rc)) + + 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): + self.log(idx, LogLevel.ERROR, msg, warn_traceback = warn_traceback) + + def log(self, idx, severity, msg, warn_traceback = False): + self.run_results[idx]['log'].append((severity, msg)) + if severity == LogLevel.ERROR and self.connections[idx]['on_error'] != 'continue': + self.fail_json('error: %s' % (msg), warn_traceback = warn_traceback) + + def _complete_kwargs(self, kwargs, traceback_msg = None): + if 'warnings' in kwargs: + logs = list(kwargs['warnings']) + else: + logs = [] + if self._run_results is not None: + for idx, rr in enumerate(self.run_results): + c = self.connections[idx] + prefix = 'state:%s' % (c['state']) + if c['state'] != 'wait': + prefix = prefix + (', "%s"' % (c['name'])) + for r in rr['log']: + logs.append('%s #%s, %s: %s' % (LogLevel.fmt(r[0]), idx, prefix, r[1])) + if traceback_msg is not None: + logs.append(traceback_msg) + kwargs['warnings'] = logs + return kwargs + + def exit_json(self, **kwargs): + changed = False + if self._run_results is not None: + for rr in self.run_results: + if rr['changed']: + changed = True + kwargs['changed'] = changed + self.module.exit_json(**self._complete_kwargs(kwargs)) + + def fail_json(self, msg, warn_traceback = False, **kwargs): + traceback_msg = None + if warn_traceback: + traceback_msg = 'exception: %s' % (traceback.format_exc()) + kwargs['msg'] = msg + self.module.fail_json(**self._complete_kwargs(kwargs, traceback_msg)) + +AnsibleUtil = _AnsibleUtil() + +############################################################################### + class Cmd: @staticmethod @@ -1020,252 +1277,307 @@ class Cmd: provider = AnsibleUtil.params['provider'] if provider == 'nm': return Cmd_nm() - if provider == 'initscripts': + elif provider == 'initscripts': return Cmd_initscripts() AnsibleUtil.fail_json('unsupported provider %s' % (provider)) def run(self): - state = AnsibleUtil.params['state'] - if state is None: - if AnsibleUtil.params['type'] is None: - state = 'up' - else: - state = 'present' - if state == 'absent': - AnsibleUtil.kwargs_whitelist_check(True) - self.run_state_absent() - elif state == 'present': - self.run_state_present() - elif state == 'up': - AnsibleUtil.kwargs_whitelist_check(True, 'wait') - self.run_state_up() - elif state == 'down': - AnsibleUtil.kwargs_whitelist_check(True) - self.run_state_down() - else: - AnsibleUtil.fail_json('invalid state "%s"' % (state)) + self.run_prepare() + while AnsibleUtil.check_mode_next() != CheckMode.DONE: + for idx, connection in enumerate(AnsibleUtil.connections): + try: + state = connection['state'] + if state == 'wait': + AnsibleUtil.log_info(idx, "wait for %s seconds" % (connection['wait'])) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + import time + time.sleep(connection['wait']) + 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: + AnsibleUtil.log_warn(idx, "failure: %s [[%s]]" % (e, traceback.format_exc())) + raise - def run_state_absent(self): - AnsibleUtil.fail_json('state absent not implemented') - - def run_state_present(self): - AnsibleUtil.fail_json('state present not implemented') - - def run_state_up(self): - AnsibleUtil.fail_json('state up not implemented') - - def run_state_down(self): - AnsibleUtil.fail_json('state down not implemented') + def run_prepare(self): + pass ############################################################################### class Cmd_nm(Cmd): def __init__(self): - self._nmcmd = None + self._nmutil = None @property - def nmcmd(self): - if self._nmcmd is None: + def nmutil(self): + if self._nmutil is None: try: - nmclient = Util.create_nm_client() + nmclient = Util.NM().Client.new(None) except Exception as e: AnsibleUtil.fail_json('failure loading libnm library: %s' % (e)) - self._nmcmd = NMCmd(nmclient) - return self._nmcmd + self._nmutil = NMUtil(nmclient) + return self._nmutil - def run_state_absent(self): + def run_prepare(self): + Cmd.run_prepare(self) + names = {} + for connection in AnsibleUtil.connections: + if connection['state'] in ['up', 'down', 'present', 'absent']: + name = connection['name'] + 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 = AnsibleUtil.connections[idx]['name'] while True: - connections = self.nmcmd.connection_list(name = AnsibleUtil.params['name'], black_list = seen) + connections = self.nmutil.connection_list(name = name, black_list = seen) if not connections: - AnsibleUtil.exit_json(changed) - if AnsibleUtil.check_mode: - AnsibleUtil.exit_json() + break c = connections[-1] - try: - self.nmcmd.connection_delete(c) - except Exception as e: - AnsibleUtil.fail_json('delete connection failed: %s' % (e)) - changed = True seen.add(c) - Util.GMainLoop_iterate() - - def run_state_present(self): - connections = self.nmcmd.connection_list(name = AnsibleUtil.params['name']) - if not connections: - try: - self.nmcmd.connection_add(check_mode = AnsibleUtil.check_mode, - **AnsibleUtil.params) - except Exception as e: - AnsibleUtil.warn('exception: %s' % (traceback.format_exc())) - AnsibleUtil.fail_json('failure adding connection: %s' % (e)) - AnsibleUtil.exit_json() + AnsibleUtil.run_results_changed(idx) + AnsibleUtil.log_info(idx, 'delete connection %s, %s' % (c.get_id(), c.get_uuid())) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + self.nmutil.connection_delete(c) + except MyError as e: + AnsibleUtil.log_error(idx, 'delete connection failed: %s' % (e)) + if not seen: + AnsibleUtil.log_info(idx, 'no connection "%s"' % (name)) + def run_state_present(self, idx): + connection = AnsibleUtil.connections[idx] + con_cur = Util.first(self.nmutil.connection_list(name = connection['name'], uuid = connection['nm.uuid'])) + con_new = self.nmutil.connection_create(AnsibleUtil.connections, idx) changed = False - connection = connections[0] - try: - changed = self.nmcmd.connection_update(connection, - check_mode = AnsibleUtil.check_mode, - **AnsibleUtil.params) - except Exception as e: - AnsibleUtil.warn('exception: %s' % (traceback.format_exc())) - AnsibleUtil.fail_json('failure updating connection %s: %s' % (connection.get_id(), e)) - - seen = set([connection]) - while True: - Util.GMainLoop_iterate() - connections = self.nmcmd.connection_list(name = AnsibleUtil.params['name'], black_list = seen) - if not connections: - AnsibleUtil.exit_json(changed) - if AnsibleUtil.check_mode: - AnsibleUtil.exit_json() - c = connections[-1] - try: - self.nmcmd.connection_delete(c) - except Exception as e: - AnsibleUtil.fail_json('delete duplicate connection failed: %s' % (e)) + if con_cur is None: + AnsibleUtil.log_info(idx, 'add connection %s, %s' % (connection['name'], connection['nm.uuid'])) changed = True - seen.add(c) + try: + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + con_cur = self.nmutil.connection_add(con_new) + except MyError as e: + AnsibleUtil.log_error(idx, 'adding connection failed: %s' % (e)) + elif not self.nmutil.connection_compare(con_cur, con_new, normalize_a = True): + changed = True + AnsibleUtil.log_info(idx, 'update connection %s, %s' % (con_cur.get_id(), con_cur.get_uuid())) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + self.nmutil.connection_update(con_cur, con_new) + except MyError as e: + AnsibleUtil.log_error(idx, 'updating connection failed: %s' % (e)) + else: + AnsibleUtil.log_info(idx, 'connection %s, %s already up to date' % (con_cur.get_id(), con_cur.get_uuid())) - def run_state_up(self): - if AnsibleUtil.params_wait != 0: - AnsibleUtil.warn('wait for activation is not yet implemented') - connections = self.nmcmd.connection_list(name = AnsibleUtil.params['name']) - if not connections: - AnsibleUtil.fail_json_check('failure to up connection %s: does not exist' % (AnsibleUtil.params['name'])) - if AnsibleUtil.check_mode: - AnsibleUtil.exit_json() - active_connection = None - try: - active_connection = self.nmcmd.connection_activate (connections[0]) - except Exception as e: - AnsibleUtil.fail_json('up connection failed: %s' % (e)) - AnsibleUtil.exit_json() - - def run_state_down(self): - if AnsibleUtil.params_wait != 0: - AnsibleUtil.warn('wait for activation is not yet implemented') - connections = self.nmcmd.connection_list(name = AnsibleUtil.params['name']) - if not connections: - AnsibleUtil.fail_json_check('failure to down connection %s: does not exist' % (AnsibleUtil.params['name'])) - changed = False seen = set() + if con_cur is not None: + seen.add(con_cur) + while True: - acons = self.nmcmd.active_connection_list(connections, black_list = seen) - if AnsibleUtil.check_mode or not acons: - AnsibleUtil.exit_json(changed) - ac = acons[0] - seen.add(ac) - del acons - try: - self.nmcmd.active_connection_deactivate(ac) - except Exception as e: - AnsibleUtil.fail_json('failure deactivating connection: %s' % (e)) + connections = self.nmutil.connection_list(name = connection['name'], black_list = seen, black_list_uuid = [connection['nm.uuid']]) + if not connections: + break + c = connections[-1] + AnsibleUtil.log_info(idx, 'delete duplicate connection %s, %s' % (c.get_id(), c.get_uuid())) changed = True - Util.GMainLoop_iterate() + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + self.nmutil.connection_delete(c) + except MyError as e: + AnsibleUtil.log_error(idx, 'delete duplicate connection failed: %s' % (e)) + seen.add(c) + + AnsibleUtil.run_results_changed(idx, changed) + + def run_state_up(self, idx): + connection = AnsibleUtil.connections[idx] + + if connection['wait'] != 0: + AnsibleUtil.log_warn(idx, 'wait for activation is not yet implemented') + + con = Util.first(self.nmutil.connection_list(name = connection['name'], uuid = connection['nm.uuid'])) + if not con: + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + AnsibleUtil.log_error(idx, 'up connection %s, %s failed: no connection' % (connection['name'], connection['nm.uuid'])) + else: + AnsibleUtil.log_info(idx, 'up connection %s, %s' % (connection['name'], connection['nm.uuid'])) + return + AnsibleUtil.log_info(idx, 'up connection %s, %s' % (con.get_id(), con.get_uuid())) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + self.nmutil.connection_activate (con) + except MyError as e: + AnsibleUtil.log_error(idx, 'up connection failed: %s' % (e)) + + AnsibleUtil.run_results_changed(idx) + + def run_state_down(self, idx): + connection = AnsibleUtil.connections[idx] + + if connection['wait'] != 0: + AnsibleUtil.log_warn(idx, 'wait for activation is not yet implemented') + + 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) + AnsibleUtil.log_info(idx, 'down connection %s: %s' % (connection['name'], ac.get_path())) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + self.nmutil.active_connection_deactivate(ac) + except MyError as e: + AnsibleUtil.log_error(idx, 'down connection failed: %s' % (e)) + cons = self.nmutil.connection_list(name = connection['name']) + + if not changed: + AnsibleUtil.log_info(idx, 'down connection %s failed: no connection' % (connection['name'])) + AnsibleUtil.run_results_changed(idx, changed) + ############################################################################### class Cmd_initscripts(Cmd): - @property - def name(self): - return AnsibleUtil.params['name'] - - def check_name(self, name = None): + def check_name(self, idx, name = None): if name is None: - name = self.name + name = AnsibleUtil.connections[idx]['name'] try: - f = self.ifcfg_path(name) - except Exception as e: - AnsibleUtil.fail_json('invalid name %s for connection' % (name)) + f = IfcfgUtil.ifcfg_path(name) + except MyError as e: + AnsibleUtil.log_error(idx, 'invalid name %s for connection' % (name)) + return None return f - def ifcfg_paths(self, name = None, file_types = None): - if name is None: - name = self.name - return IfcfgUtil.ifcfg_paths(name, file_types) + def run_prepare(self): + pass - def ifcfg_path(self, name = None, file_type = None): - if name is None: - name = self.name - return IfcfgUtil.ifcfg_path(name, file_type) - - def run_state_absent(self): - import os, errno - self.check_name() - paths = self.ifcfg_paths() + def run_state_absent(self, idx): + if not self.check_name(idx): + return + import os + name = AnsibleUtil.connections[idx]['name'] changed = False - for path in paths: - if AnsibleUtil.check_mode: - if os.path.isfile(path): - AnsibleUtil.exit_json() - else: + for path in IfcfgUtil.ifcfg_paths(name): + if not os.path.isfile(path): + continue + changed = True + AnsibleUtil.log_info(idx, 'delete ifcfg-rh file "%s"' % (path)) + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: try: - try: - os.unlink(path) - changed = True - except OSError as e: - if e.errno != errno.ENOENT: - raise + os.unlink(path) except Exception as e: - AnsibleUtil.fail_json('failure deleting ifcfg file %s: %s' % (path, e)) - AnsibleUtil.exit_json(changed) + AnsibleUtil.log_error(idx, 'delete ifcfg-rh file "%s" failed: %s' % (path, e)) - def run_state_present(self): - self.check_name() + if not changed: + AnsibleUtil.log_info(idx, 'delete ifcfg-rh files for "%s" (no files present)' % (name)) + AnsibleUtil.run_results_changed(idx, changed) - try: - ifcfg_all, dirty = IfcfgUtil.ifcfg_create(AnsibleUtil.check_mode, **AnsibleUtil.params) - except Exception as e: - AnsibleUtil.warn('exception: %s' % (traceback.format_exc())) - AnsibleUtil.fail_json('failure constructing ifcfg file: %s' % (e)) + def run_state_present(self, idx): + if not self.check_name(idx): + return - old_content = IfcfgUtil.content_from_file(self.name) + connection = AnsibleUtil.connections[idx] + name = connection['name'] + + ifcfg_all = IfcfgUtil.ifcfg_create(AnsibleUtil.connections, idx, + lambda msg: AnsibleUtil.log_warn(idx, msg)) + + old_content = IfcfgUtil.content_from_file(name) new_content = IfcfgUtil.content_from_dict(ifcfg_all) - if not dirty and old_content == new_content: - AnsibleUtil.exit_json(False) - if AnsibleUtil.check_mode: - AnsibleUtil.exit_json() + if old_content == new_content: + AnsibleUtil.log_info(idx, 'ifcfg-rh profile "%s" already up to date' % (name)) + return - try: - IfcfgUtil.content_to_file(self.name, new_content) - except Exception as e: - AnsibleUtil.warn('exception: %s' % (traceback.format_exc())) - AnsibleUtil.fail_json('writing ifcfg file failed: %s' % (e)) + op = 'add' if (old_content['ifcfg'] is None) else 'update' - AnsibleUtil.exit_json() + AnsibleUtil.log_info(idx, '%s ifcfg-rh profile "%s"' % (op, name)) - def _run_state_updown(self, cmd): - import os - import subprocess - self.check_name() + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + try: + IfcfgUtil.content_to_file(name, new_content) + except MyError as e: + AnsibleUtil.log_error(idx, '%s ifcfg-rh profile "%s" failed: %s' % (op, name, e)) - if AnsibleUtil.params_wait != 0: + AnsibleUtil.run_results_changed(idx) + + def _run_state_updown(self, idx, cmd): + if not self.check_name(idx): + return + + connection = AnsibleUtil.connections[idx] + name = connection['name'] + + if connection['wait'] != 0: # initscripts don't support wait, they always block until the ifup/ifdown # command completes. Silently ignore the argument. pass - path = self.ifcfg_path() + import os + + AnsibleUtil.log_info(idx, 'call `%s %s`' % (cmd, name)) + + path = IfcfgUtil.ifcfg_path(name) if not os.path.isfile(path): - AnsibleUtil.fail_json_check('ifcfg file "%s" does not exist' % (path)) - if AnsibleUtil.check_mode: - AnsibleUtil.exit_json() + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + AnsibleUtil.log_error(idx, 'ifcfg file "%s" does not exist' % (path)) + else: + AnsibleUtil.log_info(idx, 'ifcfg file "%s" does not exist in check mode' % (path)) + else: + if AnsibleUtil.check_mode == CheckMode.REAL_RUN: + rc, out, err = AnsibleUtil.module.run_command([cmd, name], encoding=None) + AnsibleUtil.log_info(idx, 'call `%s %s`: rc=%d, out="%s", err="%s"' % (cmd, name, rc, out, err)) + if rc != 0: + AnsibleUtil.log_error(idx, 'call `%s %s` failed with exit status %d' % (cmd, name, rc)) - rc, out, err = AnsibleUtil.module.run_command([cmd, self.name], encoding=None) - AnsibleUtil.exit_json(rc = rc, stdout = out, stderr = err) + AnsibleUtil.run_results_changed(idx) - def run_state_up(self): - self._run_state_updown('ifup') - def run_state_down(self): - self._run_state_updown('ifdown') + def run_state_up(self, idx): + self._run_state_updown(idx, 'ifup') + + def run_state_down(self, idx): + self._run_state_updown(idx, 'ifdown') ############################################################################### if __name__ == '__main__': - AnsibleUtil.module - Cmd.create().run() + try: + Cmd.create().run() + except Exception as e: + AnsibleUtil.fail_json('fatal error: %s' % (e), + warn_traceback = True) + AnsibleUtil.exit_json() diff --git a/library/test_network_connections.py b/library/test_network_connections.py new file mode 100755 index 0000000..056533b --- /dev/null +++ b/library/test_network_connections.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python + +import sys +import os +import unittest + +sys.path.insert(1, os.path.dirname(os.path.abspath(__file__))) + +import network_connections as n + +class TestValidator(unittest.TestCase): + + def test_validate_str(self): + + v = n.ArgValidatorStr('state') + self.assertEqual('a', v.validate('a')) + with self.assertRaises(n.ValidationError): + v.validate(1) + with self.assertRaises(n.ValidationError): + v.validate(None) + + v = n.ArgValidatorStr('state', required = True) + with self.assertRaises(n.ValidationError): + v.validate(None) + + def test_validate_int(self): + + v = n.ArgValidatorInt('state', default_value = None) + self.assertEqual(1, v.validate(1)) + self.assertEqual(1, v.validate("1")) + with self.assertRaises(n.ValidationError): + v.validate(None) + with self.assertRaises(n.ValidationError): + v.validate("1a") + + v = n.ArgValidatorInt('state', required = True) + with self.assertRaises(n.ValidationError): + v.validate(None) + + def test_validate_bool(self): + + v = n.ArgValidatorBool('state') + self.assertEqual(True, v.validate(True)) + self.assertEqual(True, v.validate("True")) + self.assertEqual(True, v.validate(1)) + self.assertEqual(False, v.validate(False)) + self.assertEqual(False, v.validate("False")) + self.assertEqual(False, v.validate(0)) + with self.assertRaises(n.ValidationError): + v.validate(2) + + with self.assertRaises(n.ValidationError): + v.validate(None) + v = n.ArgValidatorBool('state', required = True) + with self.assertRaises(n.ValidationError): + v.validate(None) + + def test_validate_dict(self): + + v = n.ArgValidatorDict( + 'dict', + nested = [ + n.ArgValidatorInt('i', required = True), + n.ArgValidatorStr('s', required = False, default_value = 's_default'), + n.ArgValidatorStr('l', required = False, default_value = n.ArgValidator.MISSING), + ]) + + self.assertEqual( + { + 'i': 5, + 's': 's_default', + }, + v.validate({ + 'i': '5', + }) + ) + self.assertEqual( + { + 'i': 5, + 's': 's_default', + 'l': '6', + }, + v.validate({ + 'i': '5', + 'l': '6', + }) + ) + with self.assertRaises(n.ValidationError): + v.validate({ 'k': 1 }) + + def test_validate_list(self): + + v = n.ArgValidatorList( + 'list', + nested = n.ArgValidatorInt('i') + ) + self.assertEqual( + [ 1, 5 ], + v.validate([ '1', 5 ]) + ) + with self.assertRaises(n.ValidationError): + v.validate([1, 's']) + + def test_1(self): + + self.assertEqual( + [], + n.AnsibleUtil.ARGS_CONNECTIONS.validate([]), + ) + + self.assertEqual( + [ + { + 'name': '5', + 'state': 'present', + 'type': 'ethernet', + 'autoconnect': True, + 'parent': None, + 'ip': { + 'gateway6': None, + 'gateway4': None, + 'route_metric4': None, + 'auto6': True, + 'dhcp4': True, + 'address': [], + 'route_metric6': None, + 'ip_is_present': False, + 'dhcp4_send_hostname': None, + }, + 'mac': None, + 'master': None, + 'vlan_id': None, + 'on_error': + 'fail', + 'interface_name': None, + 'slave_type': None, + }, + { + 'name': '5', + 'state': 'up', + 'wait': 90, + 'on_error': 'fail', + } + ], + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ + { 'name': '5', + 'type': 'ethernet', + }, + { 'name': '5' } + ]), + ) + self.assertEqual( + [ + { + 'name': '5', + 'state': 'up', + 'type': 'ethernet', + 'autoconnect': True, + 'parent': None, + 'ip': { + 'gateway6': None, + 'gateway4': None, + 'route_metric4': None, + 'auto6': True, + 'dhcp4': True, + 'address': [], + 'route_metric6': None, + 'ip_is_present': False, + 'dhcp4_send_hostname': None, + }, + 'mac': None, + 'master': None, + 'vlan_id': None, + 'on_error': + 'fail', + 'interface_name': None, + 'slave_type': None, + 'wait': 90, + }, + ], + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ + { 'name': '5', + 'state': 'up', + 'type': 'ethernet', + }, + ]), + ) + + with self.assertRaises(n.ValidationError): + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ { 'name': 'a', 'autoconnect': True }]) + + self.assertEqual( + [ + { + 'name': '5', + 'state': 'absent', + 'on_error': 'fail', + } + ], + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ + { + 'name': '5', + 'state': 'absent', + } + ]), + ) + + with self.assertRaises(n.ValidationError): + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ { } ]) + with self.assertRaises(n.ValidationError): + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ { 'name': 'b', 'xxx': 5 } ]) + + self.assertEqual( + [ + { + 'autoconnect': True, + 'interface_name': None, + 'ip': { + 'address': [], + 'auto6': True, + 'dhcp4': True, + 'dhcp4_send_hostname': None, + 'gateway4': None, + 'gateway6': None, + 'ip_is_present': False, + 'route_metric4': None, + 'route_metric6': None, + }, + 'mac': None, + 'master': None, + 'name': '5', + 'parent': None, + 'on_error': 'fail', + 'slave_type': None, + 'state': 'present', + 'type': 'ethernet', + 'vlan_id': None, + }, + ], + n.AnsibleUtil.ARGS_CONNECTIONS.validate([ + { + 'name': '5', + 'type': 'ethernet', + } + ]), + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tasks/main.yml b/tasks/main.yml index e2ca824..2a26409 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -15,19 +15,7 @@ - name: Configure networking connection profiles network_connections: provider: "{{ network.provider | default(network_provider) | mandatory }}" - name: "{{ item.name | mandatory }}" - state: "{{ item.state | default(omit) }}" - wait: "{{ item.wait | default(omit) }}" - type: "{{ item.type | default(omit) }}" - autoconnect: "{{ item.autoconnect | default(omit) }}" - slave_type: "{{ item.slave_type | default(omit) }}" - master: "{{ item.master | default(omit) }}" - interface_name: "{{ item.interface_name | default(omit) }}" - mac: "{{ item.mac | default(omit) }}" - parent: "{{ item.parent | default(omit) }}" - vlan_id: "{{ item.vlan_id | default(omit) }}" - ip: "{{ item.ip | default(omit) }}" - with_items: "{{ network.connections | default([]) }}" + connections: "{{ network.connections | default([]) }}" - name: Re-test connectivity ping: