From eb799a64cc286f15fd27e1745e4df84ea4ca69d8 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Aug 21 2018 09:21:56 +0000 Subject: import rhel-system-roles-1.0-4.el7 --- diff --git a/SOURCES/rhel-system-roles-kdump-pr16.diff b/SOURCES/rhel-system-roles-kdump-pr16.diff new file mode 100644 index 0000000..f136a9b --- /dev/null +++ b/SOURCES/rhel-system-roles-kdump-pr16.diff @@ -0,0 +1,21 @@ +diff --git a/README.md b/README.md +index d185518..a584c5d 100644 +--- a/README.md ++++ b/README.md +@@ -3,6 +3,16 @@ + + An ansible role which configures kdump. + ++## Warning ++ ++The role replaces the kdump configuration of the managed ++host. Previous settings will be lost, even if they are not specified ++in the role variables. Currently, this includes replacing at least the ++following configuration files: ++ ++* `/etc/sysconfig/kdump` ++* `/etc/kdump.conf` ++ + ## Role Variables + + **kdump_target**: Can be specified to write vmcore to a location that is not in diff --git a/SOURCES/rhel-system-roles-network-pr77-pr80.diff b/SOURCES/rhel-system-roles-network-pr77-pr80.diff new file mode 100644 index 0000000..1571124 --- /dev/null +++ b/SOURCES/rhel-system-roles-network-pr77-pr80.diff @@ -0,0 +1,5077 @@ +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] diff --git a/SOURCES/rhel-system-roles-postfix-pr5.diff b/SOURCES/rhel-system-roles-postfix-pr5.diff new file mode 100644 index 0000000..4da6f48 --- /dev/null +++ b/SOURCES/rhel-system-roles-postfix-pr5.diff @@ -0,0 +1,40 @@ +diff --git a/README.md b/README.md +index 5950215..df64284 100644 +--- a/README.md ++++ b/README.md +@@ -17,7 +17,7 @@ Example Playbook + + Install and enable postfix. Configure "relay_domains=$mydestination" and + +-``` ++```yaml + --- + - hosts: all + vars: +@@ -31,7 +31,7 @@ Install and enable postfix. Configure "relay_domains=$mydestination" and + Install and enable postfix. Do not run 'postfix check' before restarting + postfix: + +-``` ++```yaml + --- + - hosts: all + vars: +@@ -43,7 +43,7 @@ postfix: + Install and enable postfix. Do single backup of main.cf (older backup will be + rewritten) and configure "relay_host=example.com": + +-``` ++```yaml + --- + - hosts: all + vars: +@@ -58,7 +58,7 @@ Install and enable postfix. Do timestamped backup of main.cf and + configure "relay_host=example.com" (if postfix_backup_multiple is + set to true postfix_backup is ignored): + +-``` ++```yaml + --- + - hosts: all + vars: diff --git a/SOURCES/rhel-system-roles-selinux-pr30.diff b/SOURCES/rhel-system-roles-selinux-pr30.diff new file mode 100644 index 0000000..5df14b9 --- /dev/null +++ b/SOURCES/rhel-system-roles-selinux-pr30.diff @@ -0,0 +1,56 @@ +diff --git a/README.md b/README.md +index a0385b0..290cc37 100644 +--- a/README.md ++++ b/README.md +@@ -45,16 +45,18 @@ roles: + become: true + ``` + +-#### purge local modifications using appropriate variable ++#### purge local modifications + +-```yaml +-selinux_booleans_purge: true +-selinux_fcontexts_purge: true +-selinux_ports_purge: true +-selinux_logins_purge: true +-``` ++By default, the modifications specified in `selinux_booleans`, `selinux_fcontexts`, ++`selinux_ports` and `selinux_logins` are applied on top of pre-existing modifications. ++To purge local modifications prior to setting new ones, set following variables to true: ++ ++- SELinux booleans: `selinux_booleans_purge` ++- SELinux file contexts: `selinux_fcontexts_purge` ++- SELinux ports: `selinux_ports_purge` ++- SELinux user mapping: `selinux_logins_purge` + +-#### purge all local modifications using variable ++You can purge all modifications by using shorthand: + + ```yaml + selinux_all_purge: true +@@ -66,6 +68,11 @@ selinux_all_purge: true + selinux_policy: targeted + selinux_state: enforcing + ``` ++Allowed values for `selinux_state` are `disabled`, `enforcing` and `permissive`. ++ ++If `selinux_state` is not set, the SELinux state is not changed. ++If `selinux_policy` is not set and SELinux is to be enabled, it defaults to `targeted`. ++If SELinux is already enabled, the policy is not changed. + + #### set SELinux booleans + +@@ -79,9 +86,11 @@ selinux_booleans: + + ```yaml + selinux_fcontexts: +- - { target: '/tmp/test_dir(/.*)?', setype: 'user_home_dir_t', ftype: 'd' } ++ - { target: '/tmp/test_dir(/.*)?', setype: 'user_home_dir_t', ftype: 'd', state: 'present' } + ``` + ++Individual modifications can be dropped by setting `state` to `absent`. ++ + #### Set SELinux ports + + ```yaml diff --git a/SOURCES/rhel-system-roles-timesync-pr18.diff b/SOURCES/rhel-system-roles-timesync-pr18.diff new file mode 100644 index 0000000..325d745 --- /dev/null +++ b/SOURCES/rhel-system-roles-timesync-pr18.diff @@ -0,0 +1,40 @@ +diff --git a/README.md b/README.md +index 122b725..b499a2d 100644 +--- a/README.md ++++ b/README.md +@@ -11,7 +11,7 @@ Role Variables + + The variables that can be passed to this role are as follows: + +-``` ++```yaml + # List of NTP servers + timesync_ntp_servers: + - hostname: foo.example.com # Hostname or address of the server +@@ -59,7 +59,7 @@ Example Playbook + + Install and configure ntp to synchronize the system clock with three NTP servers: + +-``` ++```yaml + - hosts: targets + vars: + timesync_ntp_servers: +@@ -76,7 +76,7 @@ Install and configure ntp to synchronize the system clock with three NTP servers + Install and configure linuxptp to synchronize the system clock with a + grandmaster in PTP domain number 0, which is accessible on interface eth0: + +-``` ++```yaml + - hosts: targets + vars: + timesync_ptp_domains: +@@ -90,7 +90,7 @@ Install and configure chrony and linuxptp to synchronize the system clock with + multiple NTP servers and PTP domains for a highly accurate and resilient + synchronization: + +-``` ++```yaml + - hosts: targets + vars: + timesync_ntp_servers: diff --git a/SOURCES/rhel-system-roles-timesync-pr19.diff b/SOURCES/rhel-system-roles-timesync-pr19.diff new file mode 100644 index 0000000..b3cb4cb --- /dev/null +++ b/SOURCES/rhel-system-roles-timesync-pr19.diff @@ -0,0 +1,22 @@ +diff --git a/README.md b/README.md +index 122b725..631f5cb 100644 +--- a/README.md ++++ b/README.md +@@ -6,6 +6,17 @@ as an NTP client and/or PTP slave in order to synchronize the system clock with + NTP servers and/or grandmasters in PTP domains. Supported NTP/PTP + implementations are chrony, ntp (the reference implementation) and linuxptp. + ++Warning ++------- ++ ++The role replaces the configuration of the given or detected provider ++service on the managed host. Previous settings will be lost, even if ++they are not specified in the role variables (no attempt is made to ++preserve or merge the previous settings, the configuration files are ++replaced entirely). The only setting which is preserved is the choice ++of provider if `timesync_ntp_provider` is not defined (see the ++description of this variable below). ++ + Role Variables + -------------- + diff --git a/SPECS/rhel-system-roles.spec b/SPECS/rhel-system-roles.spec index e3f9dcd..064db92 100644 --- a/SPECS/rhel-system-roles.spec +++ b/SPECS/rhel-system-roles.spec @@ -1,7 +1,7 @@ Name: rhel-system-roles Summary: Set of interfaces for unified system management Version: 1.0 -Release: 2%{?dist} +Release: 4%{?dist} #Group: Development/Libraries License: GPLv3+ and MIT and BSD @@ -48,44 +48,49 @@ Patch2: rhel-system-roles-%{rolename2}-prefix.diff Patch3: rhel-system-roles-%{rolename3}-prefix.diff Patch5: rhel-system-roles-%{rolename5}-prefix.diff +Patch101: rhel-system-roles-kdump-pr16.diff + +Patch11: rhel-system-roles-postfix-pr5.diff + +Patch21: rhel-system-roles-selinux-pr30.diff + +Patch31: rhel-system-roles-timesync-pr18.diff +Patch32: rhel-system-roles-timesync-pr19.diff + +Patch51: rhel-system-roles-network-pr77-pr80.diff + Url: https://github.com/linux-system-roles/ BuildArch: noarch +Obsoletes: rhel-system-roles-techpreview < 1.0-3 + %description Collection of Ansible roles and modules that provide a stable and consistent configuration interface for managing multiple versions of Red Hat Enterprise Linux. -%package techpreview -Summary: Set of interfaces for unified system management (tech preview) -# to be updated when roles move to/from the main package to this one -Conflicts: rhel-system-roles < 1.0-1 - -%description techpreview -Collection of Ansible roles and modules that provide a consistent -configuration interface for managing multiple versions of Red Hat -Enterprise Linux. - -The roles in this subpackage are available as Technology Preview -and their backward compatibility is not guaranteed. - %prep %setup -qc -a1 -a2 -a3 -a5 cd %{rolename0}-%{version0} -#kdump patches here if necessary +%patch101 -p1 cd .. cd %{rolename1}-%{version1} %patch1 -p1 +%patch11 -p1 cd .. cd %{rolename2}-%{commit2} %patch2 -p1 +%patch21 -p1 cd .. cd %{rolename3}-%{version3} %patch3 -p1 +%patch31 -p1 +%patch32 -p1 cd .. cd %{rolename5}-%{commit5} %patch5 -p1 +%patch51 -p1 cd .. %build @@ -164,25 +169,20 @@ rmdir $RPM_BUILD_ROOT%{_datadir}/ansible/roles/%{rolecompatprefix}network/exampl %dir %{_datadir}/ansible %dir %{_datadir}/ansible/roles %{_datadir}/ansible/roles/%{roleprefix}kdump +%{_datadir}/ansible/roles/%{roleprefix}postfix %{_datadir}/ansible/roles/%{roleprefix}selinux %{_datadir}/ansible/roles/%{roleprefix}timesync %{_datadir}/ansible/roles/%{roleprefix}network %{_datadir}/ansible/roles/%{rolecompatprefix}kdump +%{_datadir}/ansible/roles/%{rolecompatprefix}postfix %{_datadir}/ansible/roles/%{rolecompatprefix}selinux %{_datadir}/ansible/roles/%{rolecompatprefix}timesync %{_datadir}/ansible/roles/%{rolecompatprefix}network -# no examples for kdump yet -#%%doc %%{_pkgdocdir}/kdump/example-*-playbook.yml -%doc %{_pkgdocdir}/selinux/example-*-playbook.yml -%doc %{_pkgdocdir}/timesync/example-*-playbook.yml -%doc %{_pkgdocdir}/network/example-*-playbook.yml - +%doc %{_pkgdocdir}/*/example-*-playbook.yml %doc %{_pkgdocdir}/network/example-inventory -%doc %{_pkgdocdir}/kdump/README.md -%doc %{_pkgdocdir}/selinux/README.md -%doc %{_pkgdocdir}/timesync/README.md -%doc %{_pkgdocdir}/network/README.md +%doc %{_pkgdocdir}/*/README.md %doc %{_datadir}/ansible/roles/%{rolecompatprefix}kdump/README.md +%doc %{_datadir}/ansible/roles/%{rolecompatprefix}postfix/README.md %doc %{_datadir}/ansible/roles/%{rolecompatprefix}selinux/README.md %doc %{_datadir}/ansible/roles/%{rolecompatprefix}timesync/README.md %doc %{_datadir}/ansible/roles/%{rolecompatprefix}network/README.md @@ -191,24 +191,20 @@ rmdir $RPM_BUILD_ROOT%{_datadir}/ansible/roles/%{rolecompatprefix}network/exampl %license %{_pkgdocdir}/*/COPYING %license %{_pkgdocdir}/*/LICENSE %license %{_datadir}/ansible/roles/%{rolecompatprefix}kdump/COPYING +%license %{_datadir}/ansible/roles/%{rolecompatprefix}postfix/COPYING %license %{_datadir}/ansible/roles/%{rolecompatprefix}selinux/COPYING %license %{_datadir}/ansible/roles/%{rolecompatprefix}timesync/COPYING %license %{_datadir}/ansible/roles/%{rolecompatprefix}network/LICENSE -%files techpreview -%dir %{_datadir}/ansible -%dir %{_datadir}/ansible/roles - -%{_datadir}/ansible/roles/%{roleprefix}postfix -%{_datadir}/ansible/roles/%{rolecompatprefix}postfix -# no examples for postfix yet -#%%doc %%{_pkgdocdir}/postfix/example-*-playbook.yml +%changelog +* Thu Aug 16 2018 Pavel Cahyna - 1.0-4 +- Add Obsoletes for the -techpreview subpackage -%doc %{_pkgdocdir}/postfix/README.md -%doc %{_datadir}/ansible/roles/%{rolecompatprefix}postfix/README.md -%license %{_datadir}/ansible/roles/%{rolecompatprefix}postfix/COPYING +* Thu Aug 16 2018 Pavel Cahyna - 1.0-3 +- Add warnings to role READMEs and other doc updates, rhbz#1616018 +- network: split the state setting into state and persistent_state, rhbz#1616014 +- Undo the -techpreview subpackage introduced in 1.0-1, rhbz#1616015 -%changelog * Thu Aug 2 2018 Pavel Cahyna - 1.0-2 - Rebase the network role to the last revision (d866422). Many improvements to tests, introduces autodetection of the current provider