diff --git a/README.md b/README.md index c16fee6..ebf8de1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ that at least version 1.2 of NetworkManager's API is available. For `initscripts`, it requires the legacy network service as commonly available on Fedora/RHEL. -For each host a list of networking profiles can be configure via the +For each host a list of networking profiles can be configured via the `network_connections` variable. - For initscripts, profiles correspond to ifcfg files in `/etc/sysconfig/network-scripts/ifcfg-*`. @@ -42,6 +42,24 @@ profile with a certain IP configuration without activating the profile. To apply the configuration to the actual networking interface, a command like `nmcli` needs to be used on the target system. +### Warning + +The role updates or creates all connection profiles on the target system as +specified in the `network_connections` variable. Therefore, the role will +remove settings from the specified profiles if the settings are only present on +the system but not in the `network_connections` variable. The following +exceptions apply: + +* For profiles that only contain a `state` setting, the role will only activate + or deactivate the connection without changing its configuration. + +* The `route_append_only` setting allows to only add new routes to the + existing routes on the system. + +* The `rule_append_only` setting allows to preserve the current routing rules. + There is no support to specify routing rules at the moment. + +See also [Limitations](#limitations). Variables --------- @@ -64,18 +82,37 @@ for NetworkManager, a connection can only be active at one device at a time. this role cannot handle a duplicate `name`. Specifying a `name` multiple times refers to the same connection profile. -* For initscripts, the name determines the ifcfg file name `/etc/sysconfig/network-scripts/-ifcfg-$NAME`. +* For initscripts, the name determines the ifcfg file name `/etc/sysconfig/network-scripts/ifcfg-$NAME`. Note that here too the name doesn't specify the `DEVICE` but a filename. As a consequence `'/'` is not a valid character for the name. -### `state` +### `state` and `persistent_state` + +Each connection profile can have a runtime state, represented by the `state` +setting and a persistent state, represtented by the `persistent_state` setting. + +The optional `state` setting supports the following values: + +- `up` +- `down` + +It defines whether the profile is activated (`up`) or deactivated (`down`). If +it is unset, the profile's runtime state will not be changed. + +The `persistent_state` setting is either `present` (default) or `absent`. If +the `persistent_state` setting is `present` and the connection profile contains +a `type` setting, the profile will be created or updated. If the profile is +incomplete (lacks the `type` setting) and `persistent_state` is `present`, +the behavior is undefined. The value `absent` makes the role ensure +that the profile is not present on the target host. + #### Example ```yaml network_connections: - - name: "eth0" - state: "absent" + - name: eth0 + persistent_state: absent ``` Above example ensures the absence of a connection profile. If a profile with `name` `eth0` @@ -90,45 +127,36 @@ exists, it will be deleted. * For initscripts it results in the deletion of the ifcfg file. Usually that has no side-effect, unless some component is watching the sysconfig directory. -We already saw that state `absent` before. There are more states: - - - `absent` - - `present` - - `up` - - `down` - -If the `state` variable is omitted, the default is `up` -- unless a `type` is specified, -in which case the default is `present`. - #### Example ```yaml network_connections: - - name: "eth0" - #state: present # default, as a type is present - type: "ethernet" + - name: eth0 + #persistent_state: present # default + type: ethernet autoconnect: yes - mac: "00:00:5e:00:53:5d" + mac: 00:00:5e:00:53:5d ip: dhcp4: yes ``` Above example creates a new connection profile or ensures that it is present -with the given configuration. It has implicitly `state` `present`, due to the -presence of `type`. On the other hand, the `present` state requires at least a `type` -variable. +with the given configuration. It implies the `persistent_state` setting to be +`present`. Valid values for `type` are: + - `bond` + - `bridge` - `ethernet` - `infiniband` - - `bridge` - - `bond` + - `macvlan` - `team` - `vlan` -`state` `present` does not directly result in a change in the network configuration. -That is, the profile is only created or modified, not activated. +The value `present` for the `persistent_state` setting does not directly +result in a change in the network configuration. That is, without `state` +set to `up`, the profile is only created or modified, not activated. - For NetworkManager, note the new connection profile is created with `autoconnect` turned on by default. Thus, NetworkManager may very well decide @@ -156,11 +184,11 @@ profile. ### `interface_name` -For type `ethernet` and `infiniband`, this option restricts the profile to the -given interface by name. This argument is optional and by default the profile -name is used unless a mac address is specified using the `mac` key. Specifying -an empty string (`""`) allows to specify that the profile is not restricted to -a network interface. +For the types `ethernet` and `infiniband`, this option restricts the profile to +the given interface by name. This argument is optional and by default the +profile name is used unless a mac address is specified using the `mac` key. +Specifying an empty string (`""`) allows to specify that the profile is not +restricted to a network interface. **Note:** With [persistent interface naming](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html), @@ -185,30 +213,27 @@ Slaves to bridge/bond/team devices cannot specify a zone. ```yaml network_connections: - - name: "eth0" - #state: up # implicit default, as there is no type specified - wait: 0 + - name: eth0 + state: up ``` -The above example defaults to `state=up` and requires an existing profile to activate. -Note that if neither `type` nor `state` is specifed, `up` is implied. Thus in above -example the `state` is redundant. +The above example requires an existing profile to activate. - For NetworkManager this results in `nmcli connection id {{name}} up`. - For initscripts it is the same as `ifup {{name}}`. -`up` also supports an optional integer argument `wait`. `wait=0` will only initiate -the activation but not wait until the device is fully connected. Connection will complete -in the background, for example after a DHCP lease was received. -`wait: ` is a timeout for how long we give the device -to activate. The default is `wait=-1` which uses a suitable timeout. Note that this -argument only makes sense for NetworkManager. +State `up` also supports an optional integer setting `wait`. `wait: 0` will +only initiate the activation but not wait until the device is fully connected. +Connection will complete in the background, for example after a DHCP lease was +received. `wait: ` is a timeout for how long we give the device to +activate. The default is using a suitable timeout. Note that this setting is +only supported by NetworkManager. **TODO** `wait` different from zero is not yet implemented. -Note that `up` always re-activates the profile and possibly changes the networking -configuration, even if the profile was already active before. As such, it always -changes the system. +Note that state `up` always re-activates the profile and possibly changes the +networking configuration, even if the profile was already active before. As +such, it always changes the system. ### `state: down` @@ -218,7 +243,6 @@ changes the system. network_connections: - name: eth0 state: down - wait: 0 ``` Another `state` is `down`. @@ -228,8 +252,8 @@ Another `state` is `down`. - For initscripts this means to call `ifdown {{name}}`. This is the opposite of the `up` state. It also will always issue the command -to deactivate the profile, even it if seemingly is currently not active. As such, -`down` always changes the system. +to deactivate the profile, even it if seemingly is currently not active. As +such, `down` always changes the system. For NetworkManager, a `wait` argument is supported like for `up` state. @@ -239,18 +263,19 @@ For NetworkManager, a `wait` argument is supported like for `up` state. ```yaml network_connections: - - name: "Wired0" - type: "ethernet" - interface_name: "eth0" + - name: Wired0 + type: ethernet + interface_name: eth0 ip: dhcp4: yes - - name: "Wired0" + - name: Wired0 + state: up ``` -As said, the `name` identifies a unique profile. However, you can refer to the same -profile multiple times. Thus above example makes perfectly sense to create a profile and -activate it within the same play. +As said, the `name` identifies a unique profile. However, you can refer to the +same profile multiple times. Therefore it is possible to create a profile and +activate it separately. ### `ip` @@ -258,8 +283,8 @@ The IP configuration supports the following options: ```yaml network_connections: - - name: "eth0" - type: "ethernet" + - name: eth0 + type: ethernet ip: route_metric4: 100 dhcp4: no @@ -341,8 +366,8 @@ integer giving the speed in Mb/s, the valid values of `duplex` are `half` and `f ```yaml network_connections: - - name: "eth0" - type: "ethernet" + - name: eth0 + type: ethernet ethernet: autoneg: no @@ -356,9 +381,9 @@ Device types like `bridge`, `bond`, `team` work similar: ```yaml network_connections: - - name: "br0" + - name: br0 type: bridge - #interface_name: br0 # defaults to the connection name + #interface_name: br0 # defaults to the connection name ``` Note that `team` is not supported on RHEL6 kernels. @@ -368,7 +393,8 @@ For slaves of these virtual types, the special properites `slave_type` and ```yaml network_connections: - - name: br0 + - name: internal-br0 + interface_name: br0 type: bridge ip: dhcp4: no @@ -377,7 +403,7 @@ network_connections: - name: br0-bond0 type: bond interface_name: bond0 - master: br0 + master: internal-br0 slave_type: bridge - name: br0-bond0-eth1 @@ -450,8 +476,8 @@ network_connections: parent: eth0-profile macvlan: mode: bridge - promiscuous: True - tap: False + promiscuous: yes + tap: no ip: address: - 192.168.1.1/24 @@ -473,7 +499,7 @@ operating system. This is usually `nm` except for RHEL 6 or CentOS 6 systems. ```yaml network_provider: nm network_connections: - - name: "eth0" + - name: eth0 #... ``` @@ -511,7 +537,7 @@ so that the host is connected to a management LAN or VLAN. It strongly depends o - It seems difficult to change networking of the target host in a way that breaks the current SSH connection of ansible. If you want to do that, ansible-pull might be a solution. Alternatively, a combination of `async`/`poll` with changing the `ansible_host` midway - of the play. + of the play. **TODO** The current role doesn't yet support to easily split the play in a pre-configure step, and a second step to activate the new configuration. diff --git a/examples/eth-with-vlan.yml b/examples/eth-with-vlan.yml index 63c7432..69da673 100644 --- a/examples/eth-with-vlan.yml +++ b/examples/eth-with-vlan.yml @@ -8,6 +8,7 @@ - name: prod2 type: ethernet autoconnect: no + state: up interface_name: "{{ network_interface_name2 }}" ip: dhcp4: no diff --git a/examples/infiniband.yml b/examples/infiniband.yml index e7197fe..22603d9 100644 --- a/examples/infiniband.yml +++ b/examples/infiniband.yml @@ -15,6 +15,7 @@ autoconnect: yes infiniband_p_key: 10 parent: ib0 + state: up ip: dhcp4: no auto6: no diff --git a/examples/macvlan.yml b/examples/macvlan.yml index 0e6ba1b..90cd09d 100644 --- a/examples/macvlan.yml +++ b/examples/macvlan.yml @@ -6,6 +6,7 @@ - name: eth0 type: ethernet + state: up interface_name: eth0 ip: address: @@ -14,6 +15,7 @@ # Create a virtual ethernet card bound to eth0 - name: veth0 type: macvlan + state: up parent: eth0 macvlan: mode: bridge diff --git a/library/network_connections.py b/library/network_connections.py index 4da3ae8..b8c5ec7 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -5,9 +5,21 @@ import functools import os import socket -import sys import traceback +# pylint: disable=import-error, no-name-in-module +from ansible.module_utils.network_lsr import MyError + +# pylint: disable=import-error +from ansible.module_utils.network_lsr.argument_validator import ( + ArgUtil, + ArgValidator_ListConnections, + ValidationError, +) + +# pylint: disable=import-error +from ansible.module_utils.network_lsr.utils import Util + DOCUMENTATION = """ --- module: network_connections @@ -46,21 +58,6 @@ 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) - - # cmp() is not available in python 3 anymore if "cmp" not in dir(__builtins__): @@ -76,281 +73,6 @@ def cmp(x, y): return (x > y) - (x < y) -class Util: - - PY3 = sys.version_info[0] == 3 - - STRING_TYPE = str if PY3 else basestring # noqa:F821 - - @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): - # subprocess.check_output is python 2.7. - with open("/dev/null", "wb") as DEVNULL: - import subprocess - - env = os.environ.copy() - env["LANG"] = "C" - p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=DEVNULL, env=env) - # FIXME: Can we assume this to always be UTF-8? - out = p.communicate()[0].decode("UTF-8") - 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: - # Installing pygobject in a tox virtualenv does not work out of the - # box - # pylint: disable=import-error - 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 Exception: - 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): - if arg is None or isinstance(arg, bool): - return arg - arg0 = arg - if isinstance(arg, Util.STRING_TYPE): - arg = arg.lower() - - if arg in ["y", "yes", "on", "1", "true", 1, True]: - return True - if arg in ["n", "no", "off", "0", "false", 0, False]: - return False - - raise MyError("value '%s' is not a boolean" % (arg0)) - - @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 Exception: - 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: - raise MyError("invalid address '%s'" % (address)) - - -############################################################################### - - class SysUtil: @staticmethod def _sysctl_read(filename): @@ -456,1058 +178,6 @@ def link_info_find(cls, refresh=False, mac=None, ifname=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\' with invalid "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. - result = set() - for connection in connections: - if "name" not in connection: - continue - if not connection["name"]: - continue - result.add(connection["name"]) - return result - - -class ArgValidator: - MISSING = object() - DEFAULT_SENTINEL = 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 Exception: # pylint: disable=broad-except - return self.default_value - - def _validate(self, value, name): - raise NotImplementedError() - - def validate(self, value, name=None): - name = name or self.name or "" - validated = self._validate(value, name) - return self._validate_post(value, name, validated) - - # pylint: disable=unused-argument,no-self-use - def _validate_post(self, value, name, result): - return result - - -class ArgValidatorStr(ArgValidator): - def __init__( # pylint: disable=too-many-arguments - 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)) - value = str(value) - if self.enum_values is not None and value 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 value: - raise ValidationError(name, "cannot be empty") - return value - - -class ArgValidatorNum(ArgValidator): - def __init__( # pylint: disable=too-many-arguments - self, - name, - required=False, - val_min=None, - val_max=None, - default_value=ArgValidator.DEFAULT_SENTINEL, - numeric_type=int, - ): - ArgValidator.__init__( - self, - name, - required, - numeric_type(0) - if default_value is ArgValidator.DEFAULT_SENTINEL - 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 Exception: - 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 Exception: - 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: - items = list(value.items()) - except AttributeError: - raise ValidationError(name, "invalid content is not a dictionary") - for (k, v) in items: - 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 _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 Exception: - 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: - 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 Exception: - 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_DictInfiniband(ArgValidatorDict): - def __init__(self): - ArgValidatorDict.__init__( - self, - name="infiniband", - nested=[ - ArgValidatorStr( - "transport_mode", enum_values=["datagram", "connected"] - ), - ArgValidatorNum("p_key", val_min=-1, val_max=0xFFFF, default_value=-1), - ], - default_value=ArgValidator.MISSING, - ) - - def get_default_infiniband(self): - return {"transport_mode": "datagram", "p_key": -1} - - -class ArgValidator_DictVlan(ArgValidatorDict): - def __init__(self): - ArgValidatorDict.__init__( - self, - name="vlan", - nested=[ArgValidatorNum("id", val_min=0, val_max=4094, required=True)], - default_value=ArgValidator.MISSING, - ) - - def get_default_vlan(self): - return {"id": None} - - -class ArgValidator_DictMacvlan(ArgValidatorDict): - - VALID_MODES = ["vepa", "bridge", "private", "passthru", "source"] - - def __init__(self): - ArgValidatorDict.__init__( - self, - name="macvlan", - nested=[ - ArgValidatorStr( - "mode", - enum_values=ArgValidator_DictMacvlan.VALID_MODES, - default_value="bridge", - ), - ArgValidatorBool("promiscuous", default_value=True), - ArgValidatorBool("tap", default_value=False), - ], - default_value=ArgValidator.MISSING, - ) - - def get_default_macvlan(self): - return {"mode": "bridge", "promiscuous": True, "tap": False} - - def _validate_post(self, value, name, result): - if result["promiscuous"] is False and result["mode"] != "passthru": - raise ValidationError( - name, "non promiscuous operation is allowed only in passthru mode" - ) - return result - - -class ArgValidator_DictConnection(ArgValidatorDict): - - VALID_STATES = ["up", "down", "present", "absent", "wait"] - VALID_TYPES = [ - "ethernet", - "infiniband", - "bridge", - "team", - "bond", - "vlan", - "macvlan", - ] - 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", allow_empty=True), - ArgValidatorMac("mac"), - ArgValidatorNum( - "mtu", val_min=0, val_max=0xFFFFFFFF, default_value=None - ), - ArgValidatorStr("zone"), - ArgValidatorBool("check_iface_exists", default_value=True), - ArgValidatorStr("parent"), - ArgValidatorBool("ignore_errors", default_value=None), - ArgValidator_DictIP(), - ArgValidator_DictEthernet(), - ArgValidator_DictBond(), - ArgValidator_DictInfiniband(), - ArgValidator_DictVlan(), - ArgValidator_DictMacvlan(), - # deprecated options: - ArgValidatorStr( - "infiniband_transport_mode", - enum_values=["datagram", "connected"], - default_value=ArgValidator.MISSING, - ), - ArgValidatorNum( - "infiniband_p_key", - val_min=-1, - val_max=0xFFFF, - default_value=ArgValidator.MISSING, - ), - ArgValidatorNum( - "vlan_id", - val_min=0, - val_max=4094, - default_value=ArgValidator.MISSING, - ), - ], - 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'", - ) - maclen = len(Util.mac_aton(result["mac"])) - if result["type"] == "ethernet" and maclen != 6: - raise ValidationError( - name + ".mac", - "a 'mac' address for type ethernet requires 6 octets " - "but is '%s'" % result["mac"], - ) - if result["type"] == "infiniband" and maclen != 20: - raise ValidationError( - name + ".mac", - "a 'mac' address for type ethernet requires 20 octets " - "but is '%s'" % result["mac"], - ) - - if result["type"] == "infiniband": - if "infiniband" not in result: - result["infiniband"] = self.nested[ - "infiniband" - ].get_default_infiniband() - if "infiniband_transport_mode" in result: - result["infiniband"]["transport_mode"] = result[ - "infiniband_transport_mode" - ] - del result["infiniband_transport_mode"] - if "infiniband_p_key" in result: - result["infiniband"]["p_key"] = result["infiniband_p_key"] - del result["infiniband_p_key"] - else: - if "infiniband_transport_mode" in result: - raise ValidationError( - name + ".infiniband_transport_mode", - "cannot mix deprecated 'infiniband_transport_mode' " - "property with 'infiniband' settings", - ) - if "infiniband_p_key" in result: - raise ValidationError( - name + ".infiniband_p_key", - "cannot mix deprecated 'infiniband_p_key' property " - "with 'infiniband' settings", - ) - if result["infiniband"]["transport_mode"] is None: - result["infiniband"]["transport_mode"] = "datagram" - if result["infiniband"]["p_key"] != -1: - 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", - ) - else: - if "infiniband" in result: - raise ValidationError( - name + ".infiniband", - "'infiniband' settings are only allowed for type 'infiniband'", - ) - if "infiniband_transport_mode" in result: - raise ValidationError( - name + ".infiniband_transport_mode", - "a 'infiniband_transport_mode' property is only " - "allowed for type 'infiniband'", - ) - if "infiniband_p_key" in result: - raise ValidationError( - name + ".infiniband_p_key", - "a 'infiniband_p_key' property is only allowed for " - "type 'infiniband'", - ) - - if "interface_name" in result: - # Ignore empty interface_name - if result["interface_name"] == "": - del result["interface_name"] - elif not Util.ifname_valid(result["interface_name"]): - raise ValidationError( - name + ".interface_name", - "invalid 'interface_name' '%s'" % (result["interface_name"]), - ) - else: - if not result.get("mac"): - if not Util.ifname_valid(result["name"]): - raise ValidationError( - name + ".interface_name", - '\'interface_name\' as "name" "%s" is not valid' - % (result["name"]), - ) - result["interface_name"] = result["name"] - - if "interface_name" not in result and result["type"] in [ - "bond", - "bridge", - "macvlan", - "team", - "vlan", - ]: - raise ValidationError( - name + ".interface_name", - "type '%s' requires 'interface_name'" % (result["type"]), - ) - - if result["type"] == "vlan": - if "vlan" not in result: - if "vlan_id" not in result: - raise ValidationError( - name + ".vlan", 'missing "vlan" settings for "type" "vlan"' - ) - result["vlan"] = self.nested["vlan"].get_default_vlan() - result["vlan"]["id"] = result["vlan_id"] - del result["vlan_id"] - else: - if "vlan_id" in result: - raise ValidationError( - name + ".vlan_id", - "don't use the deprecated 'vlan_id' together with the " - "'vlan' settings'", - ) - if "parent" not in result: - raise ValidationError( - name + ".parent", 'missing "parent" for "type" "vlan"' - ) - else: - if "vlan" in result: - raise ValidationError( - name + ".vlan", '"vlan" is only allowed for "type" "vlan"' - ) - 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", "macvlan", "infiniband"]: - raise ValidationError( - name + ".parent", - '\'parent\' is only allowed for type "vlan", "macvlan" 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"]), - ) - - if result["type"] == "macvlan": - if "macvlan" not in result: - result["macvlan"] = self.nested["macvlan"].get_default_macvlan() - else: - if "macvlan" in result: - raise ValidationError( - name + ".macvlan", - '\'macvlan\' 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 ["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"] != -1) - ) - ): - try: - ArgUtil.connection_find_master(connection["parent"], connections, idx) - except MyError: - 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: - raise ValidationError.from_connection( - idx, - "profile references a master '%s' which has 'interface_name' " - "missing" % (connection["master"]), - ) - - ############################################################################### @@ -1876,6 +546,9 @@ def content_to_dict(cls, content, file_type=None): @classmethod def content_from_file(cls, name, file_type=None): + """ + Return dictionary with all file contents for an initscripts profile + """ content = {} for file_type in cls._file_types(file_type): path = cls.ifcfg_path(name, file_type) @@ -2698,9 +1371,12 @@ def _complete_kwargs_loglines(self, rr, connections, idx): prefix = "#" else: c = connections[idx] - prefix = "#%s, state:%s" % (idx, c["state"]) - if c["state"] != "wait": - prefix = prefix + (", '%s'" % (c["name"])) + prefix = "#%s, state:%s persistent_state:%s" % ( + idx, + c["state"], + c["persistent_state"], + ) + 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])) @@ -2895,13 +1571,14 @@ def connection_modified_earlier(self, idx): continue c_state = c["state"] + c_pstate = c["persistent_state"] if c_state == "up" and "type" not in c: pass elif c_state == "down": return True - elif c_state == "absent": + elif c_pstate == "absent": return True - elif c_state in ["present", "up"]: + elif c_state == "up" or c_pstate == "present": if self.connections_data[idx]["changed"]: return True @@ -2941,28 +1618,17 @@ def run(self): 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 + for action in connection["actions"]: + if action == "absent": + self.run_action_absent(idx) + elif action == "present": + self.run_action_present(idx) + elif action == "up": + self.run_action_up(idx) + elif action == "down": + self.run_action_down(idx) + else: + assert False except Exception as e: self.log_warn( idx, "failure: %s [[%s]]" % (e, traceback.format_exc()) @@ -3017,16 +1683,16 @@ def run_prepare(self): % (connection["interface_name"], connection["mac"]), ) - def run_state_absent(self, idx): + def run_action_absent(self, idx): raise NotImplementedError() - def run_state_present(self, idx): + def run_action_present(self, idx): raise NotImplementedError() - def run_state_down(self, idx): + def run_action_down(self, idx): raise NotImplementedError() - def run_state_up(self, idx): + def run_action_up(self, idx): raise NotImplementedError() @@ -3053,11 +1719,9 @@ 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" + assert connection["persistent_state"] == "absent" continue if name in names: exists = names[name]["nm.exists"] @@ -3074,7 +1738,7 @@ def run_prepare(self): connection["nm.exists"] = exists connection["nm.uuid"] = uuid - def run_state_absent(self, idx): + def run_action_absent(self, idx): seen = set() name = self.connections[idx]["name"] black_list_names = None @@ -3099,13 +1763,23 @@ def run_state_absent(self, idx): if not seen: self.log_info(idx, "no connection '%s'" % (name)) - def run_state_present(self, idx): + def run_action_present(self, idx): connection = self.connections[idx] con_cur = Util.first( self.nmutil.connection_list( name=connection["name"], uuid=connection["nm.uuid"] ) ) + + if not connection.get("type"): + # if the type is not specified, just check that the connection was + # found + if not con_cur: + self.log_error( + idx, "Connection not found on system and 'type' not present" + ) + return + con_new = self.nmutil.connection_create(self.connections, idx, con_cur) if con_cur is None: self.log_info( @@ -3159,7 +1833,7 @@ def run_state_present(self, idx): self.log_error(idx, "delete duplicate connection failed: %s" % (e)) seen.add(c) - def run_state_up(self, idx): + def run_action_up(self, idx): connection = self.connections[idx] con = Util.first( @@ -3223,7 +1897,7 @@ def run_state_up(self, idx): except MyError as e: self.log_error(idx, "up connection failed while waiting: %s" % (e)) - def run_state_down(self, idx): + def run_action_down(self, idx): connection = self.connections[idx] cons = self.nmutil.connection_list(name=connection["name"]) @@ -3300,7 +1974,7 @@ def check_name(self, idx, name=None): return None return f - def run_state_absent(self, idx): + def run_action_absent(self, idx): n = self.connections[idx]["name"] name = n if not name: @@ -3343,7 +2017,7 @@ def run_state_absent(self, idx): % ("'" + n + "'" if n else "*"), ) - def run_state_present(self, idx): + def run_action_present(self, idx): if not self.check_name(idx): return @@ -3352,6 +2026,15 @@ def run_state_present(self, idx): old_content = IfcfgUtil.content_from_file(name) + if not connection.get("type"): + # if the type is not specified, just check that the connection was + # found + if not old_content.get("ifcfg"): + self.log_error( + idx, "Connection not found on system and 'type' not present" + ) + return + ifcfg_all = IfcfgUtil.ifcfg_create( self.connections, idx, lambda msg: self.log_warn(idx, msg), old_content ) @@ -3377,7 +2060,7 @@ def run_state_present(self, idx): idx, "%s ifcfg-rh profile '%s' failed: %s" % (op, name, e) ) - def _run_state_updown(self, idx, do_up): + def _run_action_updown(self, idx, do_up): if not self.check_name(idx): return @@ -3449,11 +2132,11 @@ def _run_state_updown(self, idx, do_up): idx, "call '%s %s' failed with exit status %d" % (cmd, name, rc) ) - def run_state_up(self, idx): - self._run_state_updown(idx, True) + def run_action_up(self, idx): + self._run_action_updown(idx, True) - def run_state_down(self, idx): - self._run_state_updown(idx, False) + def run_action_down(self, idx): + self._run_action_updown(idx, False) ############################################################################### diff --git a/module_utils/network_lsr/__init__.py b/module_utils/network_lsr/__init__.py new file mode 100644 index 0000000..22c717c --- /dev/null +++ b/module_utils/network_lsr/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/python3 -tt +# vim: fileencoding=utf8 +# SPDX-License-Identifier: BSD-3-Clause + + +class MyError(Exception): + pass diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py new file mode 100644 index 0000000..98b584a --- /dev/null +++ b/module_utils/network_lsr/argument_validator.py @@ -0,0 +1,1119 @@ +#!/usr/bin/python3 -tt +# vim: fileencoding=utf8 +# SPDX-License-Identifier: BSD-3-Clause + +import socket + +# pylint: disable=import-error, no-name-in-module +from ansible.module_utils.network_lsr import MyError +from ansible.module_utils.network_lsr.utils import Util + + +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["persistent_state"] == "absent": + c = None + elif connection["persistent_state"] == "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' with invalid '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. + result = set() + for connection in connections: + if "name" not in connection: + continue + if not connection["name"]: + continue + result.add(connection["name"]) + return result + + +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 ArgValidator: + MISSING = object() + DEFAULT_SENTINEL = 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 Exception: # pylint: disable=broad-except + return self.default_value + + def _validate(self, value, name): + raise NotImplementedError() + + def validate(self, value, name=None): + name = name or self.name or "" + validated = self._validate(value, name) + return self._validate_post(value, name, validated) + + # pylint: disable=unused-argument,no-self-use + def _validate_post(self, value, name, result): + return result + + +class ArgValidatorStr(ArgValidator): + def __init__( # pylint: disable=too-many-arguments + 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)) + value = str(value) + if self.enum_values is not None and value 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 value: + raise ValidationError(name, "cannot be empty") + return value + + +class ArgValidatorNum(ArgValidator): + def __init__( # pylint: disable=too-many-arguments + self, + name, + required=False, + val_min=None, + val_max=None, + default_value=ArgValidator.DEFAULT_SENTINEL, + numeric_type=int, + ): + ArgValidator.__init__( + self, + name, + required, + numeric_type(0) + if default_value is ArgValidator.DEFAULT_SENTINEL + 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 Exception: + 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 Exception: + 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: + items = list(value.items()) + except AttributeError: + raise ValidationError(name, "invalid content is not a dictionary") + for (k, v) in items: + 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 _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 Exception: + 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: + 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 Exception: + 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_DictInfiniband(ArgValidatorDict): + def __init__(self): + ArgValidatorDict.__init__( + self, + name="infiniband", + nested=[ + ArgValidatorStr( + "transport_mode", enum_values=["datagram", "connected"] + ), + ArgValidatorNum("p_key", val_min=-1, val_max=0xFFFF, default_value=-1), + ], + default_value=ArgValidator.MISSING, + ) + + def get_default_infiniband(self): + return {"transport_mode": "datagram", "p_key": -1} + + +class ArgValidator_DictVlan(ArgValidatorDict): + def __init__(self): + ArgValidatorDict.__init__( + self, + name="vlan", + nested=[ArgValidatorNum("id", val_min=0, val_max=4094, required=True)], + default_value=ArgValidator.MISSING, + ) + + def get_default_vlan(self): + return {"id": None} + + +class ArgValidator_DictMacvlan(ArgValidatorDict): + + VALID_MODES = ["vepa", "bridge", "private", "passthru", "source"] + + def __init__(self): + ArgValidatorDict.__init__( + self, + name="macvlan", + nested=[ + ArgValidatorStr( + "mode", + enum_values=ArgValidator_DictMacvlan.VALID_MODES, + default_value="bridge", + ), + ArgValidatorBool("promiscuous", default_value=True), + ArgValidatorBool("tap", default_value=False), + ], + default_value=ArgValidator.MISSING, + ) + + def get_default_macvlan(self): + return {"mode": "bridge", "promiscuous": True, "tap": False} + + def _validate_post(self, value, name, result): + if result["promiscuous"] is False and result["mode"] != "passthru": + raise ValidationError( + name, "non promiscuous operation is allowed only in passthru mode" + ) + return result + + +class ArgValidator_DictConnection(ArgValidatorDict): + + VALID_PERSISTENT_STATES = ["absent", "present"] + VALID_STATES = VALID_PERSISTENT_STATES + ["up", "down"] + VALID_TYPES = [ + "ethernet", + "infiniband", + "bridge", + "team", + "bond", + "vlan", + "macvlan", + ] + 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 + ), + ArgValidatorStr( + "persistent_state", + enum_values=ArgValidator_DictConnection.VALID_PERSISTENT_STATES, + ), + ArgValidatorBool("force_state_change", default_value=None), + ArgValidatorNum( + "wait", + val_min=0, + val_max=3600, + numeric_type=float, + default_value=None, + ), + 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", allow_empty=True), + ArgValidatorMac("mac"), + ArgValidatorNum( + "mtu", val_min=0, val_max=0xFFFFFFFF, default_value=None + ), + ArgValidatorStr("zone"), + ArgValidatorBool("check_iface_exists", default_value=True), + ArgValidatorStr("parent"), + ArgValidatorBool("ignore_errors", default_value=None), + ArgValidator_DictIP(), + ArgValidator_DictEthernet(), + ArgValidator_DictBond(), + ArgValidator_DictInfiniband(), + ArgValidator_DictVlan(), + ArgValidator_DictMacvlan(), + # deprecated options: + ArgValidatorStr( + "infiniband_transport_mode", + enum_values=["datagram", "connected"], + default_value=ArgValidator.MISSING, + ), + ArgValidatorNum( + "infiniband_p_key", + val_min=-1, + val_max=0xFFFF, + default_value=ArgValidator.MISSING, + ), + ArgValidatorNum( + "vlan_id", + val_min=0, + val_max=4094, + default_value=ArgValidator.MISSING, + ), + ], + default_value=dict, + all_missing_during_validate=True, + ) + + # valid field based on specified state, used to set defaults and reject + # bad values + self.VALID_FIELDS = [] + + def _validate_post_state(self, value, name, result): + """ + Validate state definitions and create a corresponding list of actions. + """ + actions = [] + state = result.get("state") + if state in self.VALID_PERSISTENT_STATES: + del result["state"] + persistent_state_default = state + state = None + else: + persistent_state_default = None + + persistent_state = result.get("persistent_state", persistent_state_default) + + # default persistent_state to present (not done via default_value in the + # ArgValidatorStr, the value will only be set at the end of + # _validate_post() + if not persistent_state: + persistent_state = "present" + + # If the profile is present, it should be ensured first + if persistent_state == "present": + actions.append(persistent_state) + + # If the profile should be absent at the end, it needs to be present in + # the meantime to allow to (de)activate it + if persistent_state == "absent" and state: + actions.append("present") + + # Change the runtime state if necessary + if state: + actions.append(state) + + # Remove the profile in the end if requested + if persistent_state == "absent": + actions.append(persistent_state) + + result["state"] = state + result["persistent_state"] = persistent_state + result["actions"] = actions + + return result + + def _validate_post_fields(self, value, name, result): + """ + Validate the allowed fields (settings depending on the requested state). + FIXME: Maybe it should check whether "up"/"down" is present in the + actions instead of checking the runtime state from "state" to switch + from state to actions after the state parsing is done. + """ + state = result.get("state") + persistent_state = result.get("persistent_state") + + # minimal settings not related to runtime changes + valid_fields = ["actions", "ignore_errors", "name", "persistent_state", "state"] + + # when type is present, a profile is completely specified (using + # defaults or other settings) + if "type" in result: + valid_fields += list(self.nested.keys()) + + # If there are no runtime changes, "wait" and "force_state_change" do + # not make sense + # FIXME: Maybe this restriction can be removed. Need to make sure that + # defaults for wait or force_state_change do not interfer + if not state: + while "wait" in valid_fields: + valid_fields.remove("wait") + while "force_state_change" in valid_fields: + valid_fields.remove("force_state_change") + else: + valid_fields += ["force_state_change", "wait"] + + # FIXME: Maybe just accept all values, even if they are not + # needed/meaningful in the respective context + 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' and persistent_state '%s'" + % (state, persistent_state), + ) + + if "name" not in result: + if persistent_state == "absent": + result["name"] = "" # set to empty string to mean *absent all others* + else: + raise ValidationError(name, "missing 'name'") + + # FIXME: Seems to be a duplicate check since "wait" will be removed from + # valid_keys when state is considered to be not True + if "wait" in result and not state: + raise ValidationError( + name + ".wait", + "'wait' is not allowed for state '%s'" % (result["state"]), + ) + + result["state"] = state + result["persistent_state"] = persistent_state + + self.VALID_FIELDS = valid_fields + return result + + def _validate_post(self, value, name, result): + result = self._validate_post_state(value, name, result) + result = self._validate_post_fields(value, name, result) + + 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'", + ) + maclen = len(Util.mac_aton(result["mac"])) + if result["type"] == "ethernet" and maclen != 6: + raise ValidationError( + name + ".mac", + "a 'mac' address for type ethernet requires 6 octets " + "but is '%s'" % result["mac"], + ) + if result["type"] == "infiniband" and maclen != 20: + raise ValidationError( + name + ".mac", + "a 'mac' address for type ethernet requires 20 octets " + "but is '%s'" % result["mac"], + ) + + if result["type"] == "infiniband": + if "infiniband" not in result: + result["infiniband"] = self.nested[ + "infiniband" + ].get_default_infiniband() + if "infiniband_transport_mode" in result: + result["infiniband"]["transport_mode"] = result[ + "infiniband_transport_mode" + ] + del result["infiniband_transport_mode"] + if "infiniband_p_key" in result: + result["infiniband"]["p_key"] = result["infiniband_p_key"] + del result["infiniband_p_key"] + else: + if "infiniband_transport_mode" in result: + raise ValidationError( + name + ".infiniband_transport_mode", + "cannot mix deprecated 'infiniband_transport_mode' " + "property with 'infiniband' settings", + ) + if "infiniband_p_key" in result: + raise ValidationError( + name + ".infiniband_p_key", + "cannot mix deprecated 'infiniband_p_key' property " + "with 'infiniband' settings", + ) + if result["infiniband"]["transport_mode"] is None: + result["infiniband"]["transport_mode"] = "datagram" + if result["infiniband"]["p_key"] != -1: + 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", + ) + else: + if "infiniband" in result: + raise ValidationError( + name + ".infiniband", + "'infiniband' settings are only allowed for type 'infiniband'", + ) + if "infiniband_transport_mode" in result: + raise ValidationError( + name + ".infiniband_transport_mode", + "a 'infiniband_transport_mode' property is only " + "allowed for type 'infiniband'", + ) + if "infiniband_p_key" in result: + raise ValidationError( + name + ".infiniband_p_key", + "a 'infiniband_p_key' property is only allowed for " + "type 'infiniband'", + ) + + if "interface_name" in result: + # Ignore empty interface_name + if result["interface_name"] == "": + del result["interface_name"] + elif not Util.ifname_valid(result["interface_name"]): + raise ValidationError( + name + ".interface_name", + "invalid 'interface_name' '%s'" % (result["interface_name"]), + ) + else: + if not result.get("mac"): + if not Util.ifname_valid(result["name"]): + raise ValidationError( + name + ".interface_name", + "'interface_name' as 'name' '%s' is not valid" + % (result["name"]), + ) + result["interface_name"] = result["name"] + + if "interface_name" not in result and result["type"] in [ + "bond", + "bridge", + "macvlan", + "team", + "vlan", + ]: + raise ValidationError( + name + ".interface_name", + "type '%s' requires 'interface_name'" % (result["type"]), + ) + + if result["type"] == "vlan": + if "vlan" not in result: + if "vlan_id" not in result: + raise ValidationError( + name + ".vlan", 'missing "vlan" settings for "type" "vlan"' + ) + result["vlan"] = self.nested["vlan"].get_default_vlan() + result["vlan"]["id"] = result["vlan_id"] + del result["vlan_id"] + else: + if "vlan_id" in result: + raise ValidationError( + name + ".vlan_id", + "don't use the deprecated 'vlan_id' together with the " + "'vlan' settings'", + ) + if "parent" not in result: + raise ValidationError( + name + ".parent", 'missing "parent" for "type" "vlan"' + ) + else: + if "vlan" in result: + raise ValidationError( + name + ".vlan", '"vlan" is only allowed for "type" "vlan"' + ) + 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", "macvlan", "infiniband"]: + raise ValidationError( + name + ".parent", + "'parent' is only allowed for type 'vlan', 'macvlan' 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"]), + ) + + if result["type"] == "macvlan": + if "macvlan" not in result: + result["macvlan"] = self.nested["macvlan"].get_default_macvlan() + else: + if "macvlan" in result: + raise ValidationError( + name + ".macvlan", + "'macvlan' settings are not allowed for 'type' '%s'" + % (result["type"]), + ) + + for k in self.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 "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"] != -1) + ) + ): + try: + ArgUtil.connection_find_master(connection["parent"], connections, idx) + except MyError: + 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: + raise ValidationError.from_connection( + idx, + "profile references a master '%s' which has 'interface_name' " + "missing" % (connection["master"]), + ) diff --git a/module_utils/network_lsr/utils.py b/module_utils/network_lsr/utils.py new file mode 100644 index 0000000..ff16bfd --- /dev/null +++ b/module_utils/network_lsr/utils.py @@ -0,0 +1,282 @@ +#!/usr/bin/python3 -tt +# SPDX-License-Identifier: BSD-3-Clause +# vim: fileencoding=utf8 + +import os +import socket +import sys + +# pylint: disable=import-error, no-name-in-module +from ansible.module_utils.network_lsr import MyError + + +class Util: + + PY3 = sys.version_info[0] == 3 + + STRING_TYPE = str if PY3 else basestring # noqa:F821 + + @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): + # subprocess.check_output is python 2.7. + with open("/dev/null", "wb") as DEVNULL: + import subprocess + + env = os.environ.copy() + env["LANG"] = "C" + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=DEVNULL, env=env) + # FIXME: Can we assume this to always be UTF-8? + out = p.communicate()[0].decode("UTF-8") + 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: + # Installing pygobject in a tox virtualenv does not work out of the + # box + # pylint: disable=import-error + 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 Exception: + 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): + if arg is None or isinstance(arg, bool): + return arg + arg0 = arg + if isinstance(arg, Util.STRING_TYPE): + arg = arg.lower() + + if arg in ["y", "yes", "on", "1", "true", 1, True]: + return True + if arg in ["n", "no", "off", "0", "false", 0, False]: + return False + + raise MyError("value '%s' is not a boolean" % (arg0)) + + @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 Exception: + 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: + raise MyError("invalid address '%s'" % (address)) diff --git a/pylintrc b/pylintrc index 407924c..2f07798 100644 --- a/pylintrc +++ b/pylintrc @@ -16,7 +16,7 @@ ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()) + '/library')" +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()) + '/library'); sys.path.append(os.path.dirname(find_pylintrc()) + '/module_utils'); sys.path.append(os.path.dirname(find_pylintrc()) + '/tests')" # Use multiple processes to speed up Pylint. diff --git a/tasks/main.yml b/tasks/main.yml index 8d162b3..5f33ce9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -6,6 +6,7 @@ # needed for ansible_facts.packages - name: Check which packages are installed package_facts: + no_log: true # Depending on the plugins, checking installed packages might be slow # for example subscription manager might slow this down diff --git a/tests/remove-profile.yml b/tests/remove-profile.yml index 95496aa..a50e848 100644 --- a/tests/remove-profile.yml +++ b/tests/remove-profile.yml @@ -5,6 +5,6 @@ vars: network_connections: - name: "{{ profile }}" - state: absent + persistent_state: absent roles: - linux-system-roles.network diff --git a/tests/roles/linux-system-roles.network/module_utils b/tests/roles/linux-system-roles.network/module_utils new file mode 120000 index 0000000..ad35115 --- /dev/null +++ b/tests/roles/linux-system-roles.network/module_utils @@ -0,0 +1 @@ +../../../module_utils/ \ No newline at end of file diff --git a/tests/test_network_connections.py b/tests/test_network_connections.py index a386044..b420ced 100755 --- a/tests/test_network_connections.py +++ b/tests/test_network_connections.py @@ -11,7 +11,20 @@ TESTS_BASEDIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(1, os.path.join(TESTS_BASEDIR, "..", "library")) +sys.path.insert(1, os.path.join(TESTS_BASEDIR, "..", "module_utils")) +try: + from unittest import mock +except ImportError: # py2 + import mock + +sys.modules["ansible"] = mock.Mock() +sys.modules["ansible.module_utils.basic"] = mock.Mock() +sys.modules["ansible.module_utils"] = mock.Mock() +sys.modules["ansible.module_utils.network_lsr"] = __import__("network_lsr") + +# pylint: disable=import-error +import network_lsr import network_connections as n from network_connections import SysUtil @@ -51,15 +64,47 @@ def pprint(msg, obj): obj.dump() -ARGS_CONNECTIONS = n.ArgValidator_ListConnections() +ARGS_CONNECTIONS = network_lsr.argument_validator.ArgValidator_ListConnections() +VALIDATE_ONE_MODE_INITSCRIPTS = ARGS_CONNECTIONS.VALIDATE_ONE_MODE_INITSCRIPTS class TestValidator(unittest.TestCase): + def setUp(self): + # default values when "type" is specified and state is not + self.default_connection_settings = { + "autoconnect": True, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "ignore_errors": None, + "ip": { + "gateway6": None, + "gateway4": None, + "route_metric4": None, + "auto6": True, + "dhcp4": True, + "address": [], + "route_append_only": False, + "rule_append_only": False, + "route": [], + "route_metric6": None, + "dhcp4_send_hostname": None, + "dns": [], + "dns_search": [], + }, + "mac": None, + "master": None, + "mtu": None, + "name": "5", + "parent": None, + "slave_type": None, + "zone": None, + } + def assertValidationError(self, v, value): self.assertRaises(n.ValidationError, v.validate, value) def assert_nm_connection_routes_expected(self, connection, route_list_expected): - parser = n.ArgValidatorIPRoute("route[?]") + parser = network_lsr.argument_validator.ArgValidatorIPRoute("route[?]") route_list_exp = [parser.validate(r) for r in route_list_expected] route_list_new = itertools.chain( nmutil.setting_ip_config_get_routes( @@ -92,7 +137,7 @@ def do_connections_validate_nm(self, input_connections, **kwargs): if "type" in connection: connection["nm.exists"] = False connection["nm.uuid"] = n.Util.create_uuid() - mode = n.ArgValidator_ListConnections.VALIDATE_ONE_MODE_INITSCRIPTS + mode = VALIDATE_ONE_MODE_INITSCRIPTS for idx, connection in enumerate(connections): try: ARGS_CONNECTIONS.validate_connection_one(mode, connections, idx) @@ -103,7 +148,9 @@ def do_connections_validate_nm(self, input_connections, **kwargs): self.assertTrue(con_new) self.assertTrue(con_new.verify()) if "nm_route_list_current" in kwargs: - parser = n.ArgValidatorIPRoute("route[?]") + parser = network_lsr.argument_validator.ArgValidatorIPRoute( + "route[?]" + ) s4 = con_new.get_setting(NM.SettingIP4Config) s6 = con_new.get_setting(NM.SettingIP6Config) s4.clear_routes() @@ -132,7 +179,7 @@ def do_connections_validate_nm(self, input_connections, **kwargs): ) def do_connections_validate_ifcfg(self, input_connections, **kwargs): - mode = n.ArgValidator_ListConnections.VALIDATE_ONE_MODE_INITSCRIPTS + mode = VALIDATE_ONE_MODE_INITSCRIPTS connections = ARGS_CONNECTIONS.validate(input_connections) for idx, connection in enumerate(connections): try: @@ -165,24 +212,26 @@ def do_connections_validate( def test_validate_str(self): - v = n.ArgValidatorStr("state") + v = network_lsr.argument_validator.ArgValidatorStr("state") self.assertEqual("a", v.validate("a")) self.assertValidationError(v, 1) self.assertValidationError(v, None) - v = n.ArgValidatorStr("state", required=True) + v = network_lsr.argument_validator.ArgValidatorStr("state", required=True) self.assertValidationError(v, None) def test_validate_int(self): - v = n.ArgValidatorNum("state", default_value=None, numeric_type=float) + v = network_lsr.argument_validator.ArgValidatorNum( + "state", default_value=None, numeric_type=float + ) self.assertEqual(1, v.validate(1)) self.assertEqual(1.5, v.validate(1.5)) self.assertEqual(1.5, v.validate("1.5")) self.assertValidationError(v, None) self.assertValidationError(v, "1a") - v = n.ArgValidatorNum("state", default_value=None) + v = network_lsr.argument_validator.ArgValidatorNum("state", default_value=None) self.assertEqual(1, v.validate(1)) self.assertEqual(1, v.validate(1.0)) self.assertEqual(1, v.validate("1")) @@ -191,12 +240,12 @@ def test_validate_int(self): self.assertValidationError(v, 1.5) self.assertValidationError(v, "1.5") - v = n.ArgValidatorNum("state", required=True) + v = network_lsr.argument_validator.ArgValidatorNum("state", required=True) self.assertValidationError(v, None) def test_validate_bool(self): - v = n.ArgValidatorBool("state") + v = network_lsr.argument_validator.ArgValidatorBool("state") self.assertEqual(True, v.validate("yes")) self.assertEqual(True, v.validate("yeS")) self.assertEqual(True, v.validate("Y")) @@ -218,18 +267,22 @@ def test_validate_bool(self): self.assertValidationError(v, "Ye") self.assertValidationError(v, "") self.assertValidationError(v, None) - v = n.ArgValidatorBool("state", required=True) + v = network_lsr.argument_validator.ArgValidatorBool("state", required=True) self.assertValidationError(v, None) def test_validate_dict(self): - v = n.ArgValidatorDict( + v = network_lsr.argument_validator.ArgValidatorDict( "dict", nested=[ - n.ArgValidatorNum("i", required=True), - n.ArgValidatorStr("s", required=False, default_value="s_default"), - n.ArgValidatorStr( - "l", required=False, default_value=n.ArgValidator.MISSING + network_lsr.argument_validator.ArgValidatorNum("i", required=True), + network_lsr.argument_validator.ArgValidatorStr( + "s", required=False, default_value="s_default" + ), + network_lsr.argument_validator.ArgValidatorStr( + "l", + required=False, + default_value=network_lsr.argument_validator.ArgValidator.MISSING, ), ], ) @@ -242,24 +295,27 @@ def test_validate_dict(self): def test_validate_list(self): - v = n.ArgValidatorList("list", nested=n.ArgValidatorNum("i")) + v = network_lsr.argument_validator.ArgValidatorList( + "list", nested=network_lsr.argument_validator.ArgValidatorNum("i") + ) self.assertEqual([1, 5], v.validate(["1", 5])) self.assertValidationError(v, [1, "s"]) - def test_1(self): - + def test_empty(self): self.maxDiff = None - self.do_connections_validate([], []) + def test_ethernet_two_defaults(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "5", - "state": "present", - "type": "ethernet", + "actions": ["present"], "autoconnect": True, - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "ignore_errors": None, + "interface_name": "5", "ip": { "gateway6": None, "gateway4": None, @@ -275,34 +331,40 @@ def test_1(self): "dns": [], "dns_search": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "5", - "check_iface_exists": True, + "mtu": None, + "name": "5", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": None, + "type": "ethernet", + "zone": None, }, { - "name": "5", - "state": "up", - "force_state_change": None, - "wait": None, + "actions": ["present"], "ignore_errors": None, + "name": "5", + "persistent_state": "present", + "state": None, }, ], [{"name": "5", "type": "ethernet"}, {"name": "5"}], ) + + def test_up_ethernet(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "5", - "state": "up", - "type": "ethernet", + "actions": ["present", "up"], "autoconnect": True, - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "5", "ip": { "gateway6": None, "gateway4": None, @@ -318,29 +380,34 @@ def test_1(self): "route_metric6": None, "dhcp4_send_hostname": None, }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "5", - "check_iface_exists": True, - "force_state_change": None, + "mtu": None, + "name": "5", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [{"name": "5", "state": "up", "type": "ethernet"}], ) + + def test_up_ethernet_no_autoconnect(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "5", - "state": "up", - "type": "ethernet", + "actions": ["present", "up"], "autoconnect": False, - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "5", "ip": { "gateway6": None, "gateway4": None, @@ -356,17 +423,17 @@ def test_1(self): "route_metric6": None, "dhcp4_send_hostname": None, }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "5", - "check_iface_exists": True, - "force_state_change": None, + "mtu": None, + "name": "5", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [{"name": "5", "state": "up", "type": "ethernet", "autoconnect": "no"}], @@ -390,19 +457,37 @@ def test_1(self): ], ) + def test_invalid_autoconnect(self): + self.maxDiff = None self.do_connections_check_invalid([{"name": "a", "autoconnect": True}]) + def test_absent(self): + self.maxDiff = None self.do_connections_validate( - [{"name": "5", "state": "absent", "ignore_errors": None}], - [{"name": "5", "state": "absent"}], + [ + { + "actions": ["absent"], + "ignore_errors": None, + "name": "5", + "persistent_state": "absent", + "state": None, + } + ], + [{"name": "5", "persistent_state": "absent"}], ) + def test_up_ethernet_mac_mtu_static_ip(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod1", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": None, "ip": { "dhcp4": False, "route_metric6": None, @@ -424,19 +509,17 @@ def test_1(self): "rule_append_only": False, "route": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "state": "up", - "mtu": 1450, - "check_iface_exists": True, - "force_state_change": None, "mac": "52:54:00:44:9f:ba", - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": None, - "type": "ethernet", + "mtu": 1450, + "name": "prod1", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [ @@ -452,13 +535,19 @@ def test_1(self): ], ) + def test_up_single_v4_dns(self): + self.maxDiff = None # set single IPv4 DNS server self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod1", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "prod1", "ip": { "dhcp4": False, "route_metric6": None, @@ -480,19 +569,17 @@ def test_1(self): "rule_append_only": False, "route": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "state": "up", - "check_iface_exists": True, - "force_state_change": None, - "zone": None, "mac": None, "master": None, "mtu": None, - "ignore_errors": None, - "interface_name": "prod1", - "type": "ethernet", + "name": "prod1", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [ @@ -505,12 +592,19 @@ def test_1(self): } ], ) + + def test_routes(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod1", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": None, "ip": { "dhcp4": False, "auto6": True, @@ -537,24 +631,26 @@ def test_1(self): "gateway4": None, "dns": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "state": "up", - "mtu": 1450, - "check_iface_exists": True, - "force_state_change": None, "mac": "52:54:00:44:9f:ba", - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": None, - "type": "ethernet", + "mtu": 1450, + "name": "prod1", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod.100", - "parent": "prod1", + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "prod.100", "ip": { "dhcp4": False, "route_metric6": None, @@ -589,20 +685,18 @@ def test_1(self): } ], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, - "state": "up", "master": None, + "mtu": None, + "name": "prod.100", + "parent": "prod1", + "persistent_state": "present", "slave_type": None, - "ignore_errors": None, - "interface_name": "prod.100", + "state": "up", "type": "vlan", "vlan": {"id": 100}, "wait": None, + "zone": None, }, ], [ @@ -634,12 +728,18 @@ def test_1(self): ], ) + def test_vlan(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod1", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": None, "ip": { "dhcp4": False, "auto6": True, @@ -666,24 +766,26 @@ def test_1(self): "gateway4": None, "dns": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "state": "up", - "mtu": 1450, - "check_iface_exists": True, - "force_state_change": None, "mac": "52:54:00:44:9f:ba", - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": None, - "type": "ethernet", + "mtu": 1450, + "name": "prod1", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod.100", - "parent": "prod1", + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "prod.100", "ip": { "dhcp4": False, "route_metric6": None, @@ -718,20 +820,18 @@ def test_1(self): } ], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, - "state": "up", "master": None, + "mtu": None, + "name": "prod.100", + "parent": "prod1", + "persistent_state": "present", "slave_type": None, - "ignore_errors": None, - "interface_name": "prod.100", + "state": "up", "type": "vlan", "vlan": {"id": 101}, "wait": None, + "zone": None, }, ], [ @@ -763,12 +863,18 @@ def test_1(self): ], ) + def test_macvlan(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "eth0-parent", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "eth0", "ip": { "dhcp4": False, "auto6": False, @@ -790,24 +896,25 @@ def test_1(self): "gateway4": None, "dns": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "state": "up", - "mtu": 1450, - "check_iface_exists": True, - "force_state_change": None, "mac": "33:24:10:24:2f:b9", - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "eth0", - "type": "ethernet", + "mtu": 1450, + "name": "eth0-parent", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, - "name": "veth0.0", - "parent": "eth0-parent", + "check_iface_exists": True, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "veth0", "ip": { "dhcp4": False, "route_metric6": None, @@ -838,23 +945,25 @@ def test_1(self): ], }, "mac": None, - "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, - "state": "up", + "macvlan": {"mode": "bridge", "promiscuous": True, "tap": False}, "master": None, + "mtu": None, + "name": "veth0.0", + "parent": "eth0-parent", + "persistent_state": "present", "slave_type": None, - "ignore_errors": None, - "interface_name": "veth0", + "state": "up", "type": "macvlan", - "macvlan": {"mode": "bridge", "promiscuous": True, "tap": False}, "wait": None, + "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, - "name": "veth0.1", - "parent": "eth0-parent", + "check_iface_exists": True, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "veth1", "ip": { "dhcp4": False, "route_metric6": None, @@ -885,18 +994,17 @@ def test_1(self): ], }, "mac": None, - "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, - "state": "up", + "macvlan": {"mode": "passthru", "promiscuous": False, "tap": True}, "master": None, + "mtu": None, + "name": "veth0.1", + "parent": "eth0-parent", + "persistent_state": "present", "slave_type": None, - "ignore_errors": None, - "interface_name": "veth1", + "state": "up", "type": "macvlan", - "macvlan": {"mode": "passthru", "promiscuous": False, "tap": True}, "wait": None, + "zone": None, }, ], [ @@ -943,73 +1051,79 @@ def test_1(self): ], ) + def test_bridge_no_dhcp4_auto6(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod2", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "bridge2", "ip": { + "address": [], + "auto6": False, "dhcp4": False, - "route_metric6": None, - "route_metric4": None, - "dns_search": [], "dhcp4_send_hostname": None, - "gateway6": None, - "gateway4": None, - "auto6": False, "dns": [], - "address": [], + "dns_search": [], + "gateway4": None, + "gateway6": None, + "route": [], "route_append_only": False, + "route_metric4": None, + "route_metric6": None, "rule_append_only": False, - "route": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, + "master": None, "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, + "name": "prod2", + "parent": None, + "persistent_state": "present", + "slave_type": None, "state": "up", - "master": None, - "ignore_errors": None, - "interface_name": "bridge2", "type": "bridge", - "slave_type": None, "wait": None, + "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, - "name": "prod2-slave1", - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "eth1", "ip": { - "dhcp4": True, - "auto6": True, "address": [], - "route_append_only": False, - "rule_append_only": False, - "route": [], - "route_metric6": None, - "route_metric4": None, - "dns_search": [], + "auto6": True, + "dhcp4": True, "dhcp4_send_hostname": None, - "gateway6": None, - "gateway4": None, "dns": [], + "dns_search": [], + "gateway4": None, + "gateway6": None, + "route": [], + "route_append_only": False, + "route_metric4": None, + "route_metric6": None, + "rule_append_only": False, }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, + "master": "prod2", "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, + "name": "prod2-slave1", + "parent": None, + "persistent_state": "present", + "slave_type": "bridge", "state": "up", - "master": "prod2", - "ignore_errors": None, - "interface_name": "eth1", "type": "ethernet", - "slave_type": "bridge", "wait": None, + "zone": None, }, ], [ @@ -1030,12 +1144,19 @@ def test_1(self): ], ) + def test_bond(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "bond1", - "parent": None, + "bond": {"mode": "balance-rr", "miimon": None}, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "bond1", "ip": { "dhcp4": True, "route_metric6": None, @@ -1051,31 +1172,35 @@ def test_1(self): "rule_append_only": False, "route": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, + "master": None, "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, + "name": "bond1", + "parent": None, + "persistent_state": "present", + "slave_type": None, "state": "up", - "master": None, - "ignore_errors": None, - "interface_name": "bond1", "type": "bond", - "slave_type": None, - "bond": {"mode": "balance-rr", "miimon": None}, "wait": None, + "zone": None, } ], [{"name": "bond1", "state": "up", "type": "bond"}], ) + def test_bond_active_backup(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, - "name": "bond1", - "parent": None, + "bond": {"mode": "active-backup", "miimon": None}, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "bond1", "ip": { "dhcp4": True, "route_metric6": None, @@ -1091,20 +1216,17 @@ def test_1(self): "rule_append_only": False, "route": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, + "master": None, "mtu": None, - "zone": None, - "check_iface_exists": True, - "force_state_change": None, + "name": "bond1", + "parent": None, + "persistent_state": "present", + "slave_type": None, "state": "up", - "master": None, - "ignore_errors": None, - "interface_name": "bond1", "type": "bond", - "slave_type": None, - "bond": {"mode": "active-backup", "miimon": None}, "wait": None, + "zone": None, } ], [ @@ -1117,13 +1239,21 @@ def test_1(self): ], ) + def test_invalid_values(self): + self.maxDiff = None self.do_connections_check_invalid([{}]) self.do_connections_check_invalid([{"name": "b", "xxx": 5}]) + def test_ethernet_mac_address(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present"], "autoconnect": True, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "ignore_errors": None, "interface_name": None, "ip": { "address": [], @@ -1140,70 +1270,33 @@ def test_1(self): "dns": [], "dns_search": [], }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": "aa:bb:cc:dd:ee:ff", - "mtu": None, - "zone": None, "master": None, - "check_iface_exists": True, + "mtu": None, "name": "5", "parent": None, - "ignore_errors": None, + "persistent_state": "present", "slave_type": None, - "state": "present", + "state": None, "type": "ethernet", + "zone": None, } ], [{"name": "5", "type": "ethernet", "mac": "AA:bb:cC:DD:ee:FF"}], ) + def test_ethernet_speed_settings(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "5", - "state": "up", - "type": "ethernet", + "actions": ["present", "up"], "autoconnect": True, - "parent": None, - "ip": { - "gateway6": None, - "gateway4": None, - "route_metric4": None, - "auto6": True, - "dhcp4": True, - "address": [], - "route_append_only": False, - "rule_append_only": False, - "route": [], - "dns": [], - "dns_search": [], - "route_metric6": None, - "dhcp4_send_hostname": None, - }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, - "mac": None, - "mtu": None, - "zone": None, - "master": None, - "ignore_errors": None, - "interface_name": "5", "check_iface_exists": True, + "ethernet": {"autoneg": False, "duplex": "half", "speed": 400}, "force_state_change": None, - "slave_type": None, - "wait": None, - } - ], - [{"name": "5", "state": "up", "type": "ethernet"}], - ) - - self.do_connections_validate( - [ - { - "name": "5", - "state": "up", - "type": "ethernet", - "autoconnect": True, - "parent": None, + "ignore_errors": None, + "interface_name": "5", "ip": { "gateway6": None, "gateway4": None, @@ -1219,17 +1312,17 @@ def test_1(self): "route_metric6": None, "dhcp4_send_hostname": None, }, - "ethernet": {"autoneg": False, "duplex": "half", "speed": 400}, "mac": None, - "mtu": None, - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "5", - "check_iface_exists": True, - "force_state_change": None, + "mtu": None, + "name": "5", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [ @@ -1262,9 +1355,12 @@ def test_1(self): ], ) + def test_bridge2(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, "check_iface_exists": True, "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, @@ -1291,6 +1387,7 @@ def test_1(self): "mtu": None, "name": "6643-master", "parent": None, + "persistent_state": "present", "slave_type": None, "state": "up", "type": "bridge", @@ -1298,6 +1395,7 @@ def test_1(self): "zone": None, }, { + "actions": ["present", "up"], "autoconnect": True, "check_iface_exists": True, "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, @@ -1307,8 +1405,8 @@ def test_1(self): "ip": { "address": [], "auto6": True, - "dhcp4": True, "dhcp4_send_hostname": None, + "dhcp4": True, "dns": [], "dns_search": [], "gateway4": None, @@ -1324,6 +1422,7 @@ def test_1(self): "mtu": None, "name": "6643", "parent": None, + "persistent_state": "present", "slave_type": "bridge", "state": "up", "type": "ethernet", @@ -1342,9 +1441,12 @@ def test_1(self): ], ) + def test_infiniband(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, "check_iface_exists": True, "force_state_change": None, @@ -1371,6 +1473,7 @@ def test_1(self): "mtu": None, "name": "infiniband.1", "parent": None, + "persistent_state": "present", "slave_type": None, "state": "up", "type": "infiniband", @@ -1406,9 +1509,12 @@ def test_1(self): ], ) + def test_infiniband2(self): + self.maxDiff = None self.do_connections_validate( [ { + "actions": ["present", "up"], "autoconnect": True, "check_iface_exists": True, "force_state_change": None, @@ -1436,6 +1542,7 @@ def test_1(self): "mtu": None, "name": "infiniband.2", "parent": None, + "persistent_state": "present", "slave_type": None, "state": "up", "type": "infiniband", @@ -1477,14 +1584,18 @@ def test_1(self): ], ) + def test_route_metric_prefix(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "555", - "state": "up", - "type": "ethernet", + "actions": ["present", "up"], "autoconnect": True, - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "555", "ip": { "gateway6": None, "gateway4": None, @@ -1515,17 +1626,17 @@ def test_1(self): "route_metric6": None, "dhcp4_send_hostname": None, }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": None, "master": None, - "ignore_errors": None, - "interface_name": "555", - "check_iface_exists": True, - "force_state_change": None, + "mtu": None, + "name": "555", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": None, } ], [ @@ -1563,14 +1674,18 @@ def test_1(self): ], ) + def test_route_v6(self): + self.maxDiff = None self.do_connections_validate( [ { - "name": "e556", - "state": "up", - "type": "ethernet", + "actions": ["present", "up"], "autoconnect": True, - "parent": None, + "check_iface_exists": True, + "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "e556", "ip": { "gateway6": None, "gateway4": None, @@ -1608,17 +1723,17 @@ def test_1(self): "route_metric6": None, "dhcp4_send_hostname": None, }, - "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "mac": None, - "mtu": None, - "zone": "external", "master": None, - "ignore_errors": None, - "interface_name": "e556", - "check_iface_exists": True, - "force_state_change": None, + "mtu": None, + "name": "e556", + "parent": None, + "persistent_state": "present", "slave_type": None, + "state": "up", + "type": "ethernet", "wait": None, + "zone": "external", } ], [ @@ -1688,6 +1803,8 @@ def test_1(self): ], ) + def test_invalid_mac(self): + self.maxDiff = None self.do_connections_check_invalid( [{"name": "b", "type": "ethernet", "mac": "aa:b"}] ) @@ -1716,7 +1833,7 @@ def test_interface_name_ethernet_empty(self): self.assertTrue(connections[0]["interface_name"] is None) def test_interface_name_ethernet_None(self): - """ Check that inerface_name cannot be None """ + """ Check that interface_name cannot be None """ network_connections = [ {"name": "internal_network", "type": "ethernet", "interface_name": None} ] @@ -1730,7 +1847,7 @@ def test_interface_name_ethernet_explicit(self): {"name": "internal", "type": "ethernet", "interface_name": "eth0"} ] connections = ARGS_CONNECTIONS.validate(network_connections) - self.assertEquals(connections[0]["interface_name"], "eth0") + self.assertEqual(connections[0]["interface_name"], "eth0") def test_interface_name_ethernet_invalid_profile(self): """ Require explicit interface_name when the profile name is not a @@ -1764,7 +1881,235 @@ def test_interface_name_bond_empty_interface_name(self): def test_interface_name_bond_profile_as_interface_name(self): network_connections = [{"name": "internal", "type": "bond"}] connections = ARGS_CONNECTIONS.validate(network_connections) - self.assertEquals(connections[0]["interface_name"], "internal") + self.assertEqual(connections[0]["interface_name"], "internal") + + def check_connection(self, connection, expected): + reduced_connection = {} + for setting in expected: + reduced_connection[setting] = connection[setting] + self.assertEqual(reduced_connection, expected) + + def check_partial_connection_zero(self, network_config, expected): + connections = ARGS_CONNECTIONS.validate([network_config]) + self.check_connection(connections[0], expected) + + def check_one_connection_with_defaults( + self, network_config, expected_changed_settings + ): + self.maxDiff = None + expected = self.default_connection_settings + expected.update(expected_changed_settings) + + self.do_connections_validate([expected], [network_config]) + + def test_default_states(self): + self.check_partial_connection_zero( + {"name": "eth0"}, + {"actions": ["present"], "persistent_state": "present", "state": None}, + ) + + def test_invalid_persistent_state_up(self): + network_connections = [{"name": "internal", "persistent_state": "up"}] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_invalid_persistent_state_down(self): + network_connections = [{"name": "internal", "persistent_state": "down"}] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_invalid_state_test(self): + network_connections = [{"name": "internal", "state": "test"}] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_default_states_type(self): + self.check_partial_connection_zero( + {"name": "eth0", "type": "ethernet"}, + {"actions": ["present"], "persistent_state": "present", "state": None}, + ) + + def test_persistent_state_present(self): + self.check_partial_connection_zero( + {"name": "eth0", "persistent_state": "present", "type": "ethernet"}, + {"actions": ["present"], "persistent_state": "present", "state": None}, + ) + + def test_state_present(self): + self.check_partial_connection_zero( + {"name": "eth0", "state": "present", "type": "ethernet"}, + {"actions": ["present"], "persistent_state": "present", "state": None}, + ) + + def test_state_absent(self): + self.check_partial_connection_zero( + {"name": "eth0", "state": "absent"}, + {"actions": ["absent"], "persistent_state": "absent", "state": None}, + ) + + def test_persistent_state_absent(self): + self.check_partial_connection_zero( + {"name": "eth0", "persistent_state": "absent"}, + {"actions": ["absent"], "persistent_state": "absent", "state": None}, + ) + + def test_state_present_up(self): + self.check_partial_connection_zero( + { + "name": "eth0", + "persistent_state": "present", + "state": "up", + "type": "ethernet", + }, + { + "actions": ["present", "up"], + "persistent_state": "present", + "state": "up", + }, + ) + + def test_state_present_down(self): + self.check_partial_connection_zero( + { + "name": "eth0", + "persistent_state": "present", + "state": "down", + "type": "ethernet", + }, + { + "actions": ["present", "down"], + "persistent_state": "present", + "state": "down", + }, + ) + + def test_state_absent_up_no_type(self): + self.check_partial_connection_zero( + {"name": "eth0", "persistent_state": "absent", "state": "up"}, + { + "actions": ["present", "up", "absent"], + "persistent_state": "absent", + "state": "up", + }, + ) + + def test_state_absent_up_type(self): + # if type is specified, present should happen, too + self.check_partial_connection_zero( + { + "name": "eth0", + "persistent_state": "absent", + "state": "up", + "type": "ethernet", + }, + { + "actions": ["present", "up", "absent"], + "persistent_state": "absent", + "state": "up", + }, + ) + + def test_state_absent_down(self): + # if type is specified, present should happen, too + self.check_partial_connection_zero( + {"name": "eth0", "persistent_state": "absent", "state": "down"}, + { + "actions": ["present", "down", "absent"], + "persistent_state": "absent", + "state": "down", + }, + ) + + def test_state_up_no_type(self): + self.check_partial_connection_zero( + {"name": "eth0", "state": "up"}, + { + "actions": ["present", "up"], + "persistent_state": "present", + "state": "up", + }, + ) + + def test_state_up_type(self): + self.check_partial_connection_zero( + {"name": "eth0", "state": "up", "type": "ethernet"}, + { + "actions": ["present", "up"], + "persistent_state": "present", + "state": "up", + }, + ) + + def test_state_down_no_type(self): + self.check_partial_connection_zero( + {"name": "eth0", "state": "down"}, + { + "actions": ["present", "down"], + "persistent_state": "present", + "state": "down", + }, + ) + + def test_full_state_present_no_type(self): + self.maxDiff = None + self.do_connections_validate( + [ + { + "actions": ["present"], + "ignore_errors": None, + "name": "eth0", + "state": None, + "persistent_state": "present", + } + ], + [{"name": "eth0", "persistent_state": "present"}], + ) + + def test_full_state_present_type_defaults(self): + self.check_one_connection_with_defaults( + {"name": "eth0", "type": "ethernet", "persistent_state": "present"}, + { + "actions": ["present"], + "interface_name": "eth0", + "name": "eth0", + "persistent_state": "present", + "state": None, + "type": "ethernet", + }, + ) + + def test_full_state_absent_no_type(self): + self.maxDiff = None + self.do_connections_validate( + [ + { + "actions": ["absent"], + "ignore_errors": None, + "name": "eth0", + "state": None, + "persistent_state": "absent", + } + ], + [{"name": "eth0", "persistent_state": "absent"}], + ) + + def test_full_state_absent_defaults(self): + self.maxDiff = None + self.check_one_connection_with_defaults( + {"name": "eth0", "persistent_state": "absent", "type": "ethernet"}, + { + "actions": ["absent"], + "ignore_errors": None, + "name": "eth0", + "state": None, + "persistent_state": "absent", + "type": "ethernet", + "interface_name": "eth0", + }, + ) @my_test_skipIf(nmutil is None, "no support for NM (libnm via pygobject)") diff --git a/tests/tests_unit.yml b/tests/tests_unit.yml index 203b98b..48e478b 100644 --- a/tests/tests_unit.yml +++ b/tests/tests_unit.yml @@ -3,27 +3,33 @@ - hosts: all name: Setup for test running tasks: - # FIXME: change with_items to loop when test-harness is updated + - name: Install EPEL on enterprise Linux for python2-mock + command: yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_distribution_major_version }}.noarch.rpm + when: + - ansible_distribution in ['RedHat', 'CentOS'] + - ansible_distribution_major_version in ['6', '7'] + - package: name: "{{ item }}" state: present ignore_errors: true - with_items: + loop: - NetworkManager-libnm - python2-gobject-base - python3-gobject-base - python-gobject-base + - python2-mock - hosts: all name: execute python unit tests tasks: - copy: - src: ../library/network_connections.py - dest: /tmp/test-unit-1/ - - - copy: - src: test_network_connections.py + src: "{{ item }}" dest: /tmp/test-unit-1/ + loop: + - ../library/network_connections.py + - test_network_connections.py + - ../module_utils/network_lsr - file: state: directory diff --git a/tox.ini b/tox.ini index 369f890..1c8a0a6 100644 --- a/tox.ini +++ b/tox.ini @@ -8,15 +8,16 @@ basepython = python2.7 deps = py{26,27,36,37}: pytest-cov py{27,36,37}: pytest>=3.5.1 + py{26,27}: mock py26: pytest [base] passenv = * setenv = - PYTHONPATH = {toxinidir}/library + PYTHONPATH = {toxinidir}/library:{toxinidir}/module_utils LC_ALL = C changedir = {toxinidir}/tests -covtarget = network_connections +covtarget = {toxinidir}/library --cov {toxinidir}/module_utils pytesttarget = . [testenv:black] @@ -93,6 +94,7 @@ commands = --errors-only \ {posargs} \ library/network_connections.py \ + module_utils/network_lsr \ tests/test_network_connections.py [testenv:flake8]