From 202a9f6b9fe3e984a39094a89c8f8d3cde4bedb3 Mon Sep 17 00:00:00 2001 From: Mike DePaulo Date: Oct 23 2018 19:39:20 +0000 Subject: Merge pull request #2 from tyll/updates Various updates --- diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 09ae449..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -SOURCES/kdump-1.0.0-rc.1.tar.gz -SOURCES/network-d866422.tar.gz -SOURCES/postfix-0.1.tar.gz -SOURCES/selinux-6dd057a.tar.gz -SOURCES/timesync-1.0.0-rc.1.tar.gz diff --git a/.rhel-system-roles.metadata b/.rhel-system-roles.metadata deleted file mode 100644 index 9c5a7c4..0000000 --- a/.rhel-system-roles.metadata +++ /dev/null @@ -1,5 +0,0 @@ -50f9e33a7ea64bd6251a3f091227c55d232a4a08 SOURCES/kdump-1.0.0-rc.1.tar.gz -546b39827090e971dac449a12d3af52f6d1e7ab2 SOURCES/network-d866422.tar.gz -66c82331f4ac9598c506c3999965b4d07dbfe49d SOURCES/postfix-0.1.tar.gz -4dcff70976d75c732a2ebca786090ba59f41cb95 SOURCES/selinux-6dd057a.tar.gz -997b129eae558e84507ca01e4557d527ca43e9b2 SOURCES/timesync-1.0.0-rc.1.tar.gz diff --git a/linux-system-roles.spec b/linux-system-roles.spec index 1f8ee81..c1e7ac6 100644 --- a/linux-system-roles.spec +++ b/linux-system-roles.spec @@ -5,7 +5,7 @@ Name: linux-system-roles %endif Summary: Set of interfaces for unified system management Version: 1.0 -Release: 5%{?dist} +Release: 6%{?dist} #Group: Development/Libraries License: GPLv3+ and MIT and BSD @@ -34,7 +34,7 @@ License: GPLv3+ and MIT and BSD %global rolename3 timesync %global version3 1.0.0-rc.1 -%global commit5 d866422d9d73ed823632a3c56ee8575cd56cad5b +%global commit5 64b2d76de74df2d480394d02aae204beda4d9257 %global shortcommit5 %(c=%{commit5}; echo ${c:0:7}) %global rolename5 network #%%global version5 0.4 @@ -45,9 +45,11 @@ Source2: https://github.com/linux-system-roles/%{rolename2}/archive/%{commit2}.t Source3: https://github.com/linux-system-roles/%{rolename3}/archive/%{version3}.tar.gz#/%{rolename3}-%{version3}.tar.gz Source5: https://github.com/linux-system-roles/%{rolename5}/archive/%{commit5}.tar.gz#/%{rolename5}-%{shortcommit5}.tar.gz +# 2018-10-23: Submitted upstream: https://github.com/linux-system-roles/timesync/pull/25 Source6: timesync-playbook.yml Source7: timesync-pool-playbook.yml +# 2018-10-23: Submitted upstream Source8: md2html.sh %if "%{roleprefix}" != "linux-system-roles." @@ -57,6 +59,7 @@ Patch3: rhel-system-roles-%{rolename3}-prefix.diff Patch5: rhel-system-roles-%{rolename5}-prefix.diff %endif +# Not suitable for upstream, since the files need to be executable there Patch52: network-permissions.diff Url: https://github.com/linux-system-roles/ @@ -224,6 +227,11 @@ rmdir $RPM_BUILD_ROOT%{_datadir}/ansible/roles/%{roleprefix}network/examples %license %{_datadir}/ansible/roles/%{roleprefix}network/LICENSE %changelog +* Tue Oct 23 2018 Till Maas - 1.0-6 +- Update Network system role to latest commit to include Fedora 29 fixes +- Update example timesync example playbooks +- Add comments about upstream status + * Tue Aug 14 2018 Pavel Cahyna - 1.0-4 - Format the READMEs as html, by vdolezal, with changes to use highlight (source-highlight does not understand YAML) diff --git a/rhel-system-roles-kdump-pr16.diff b/rhel-system-roles-kdump-pr16.diff deleted file mode 100644 index f136a9b..0000000 --- a/rhel-system-roles-kdump-pr16.diff +++ /dev/null @@ -1,21 +0,0 @@ -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/rhel-system-roles-network-pr77-pr80.diff b/rhel-system-roles-network-pr77-pr80.diff deleted file mode 100644 index 1571124..0000000 --- a/rhel-system-roles-network-pr77-pr80.diff +++ /dev/null @@ -1,5077 +0,0 @@ -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/rhel-system-roles-network-prefix.diff b/rhel-system-roles-network-prefix.diff deleted file mode 100644 index 3065f21..0000000 --- a/rhel-system-roles-network-prefix.diff +++ /dev/null @@ -1,60 +0,0 @@ -diff --git a/examples/bond-with-vlan.yml b/examples/bond-with-vlan.yml -index 2e6be23..3b7a6dc 100644 ---- a/examples/bond-with-vlan.yml -+++ b/examples/bond-with-vlan.yml -@@ -35,4 +35,4 @@ - - "192.0.2.{{ network_iphost }}/24" - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network -diff --git a/examples/bridge-with-vlan.yml b/examples/bridge-with-vlan.yml -index 037ff8e..83c586d 100644 ---- a/examples/bridge-with-vlan.yml -+++ b/examples/bridge-with-vlan.yml -@@ -33,4 +33,4 @@ - - "192.0.2.{{ network_iphost }}/24" - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network -diff --git a/examples/eth-simple-auto.yml b/examples/eth-simple-auto.yml -index 0ba168a..e4c4a54 100644 ---- a/examples/eth-simple-auto.yml -+++ b/examples/eth-simple-auto.yml -@@ -15,4 +15,4 @@ - mtu: 1450 - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network -diff --git a/examples/eth-with-vlan.yml b/examples/eth-with-vlan.yml -index 63c7432..1c8ee60 100644 ---- a/examples/eth-with-vlan.yml -+++ b/examples/eth-with-vlan.yml -@@ -25,4 +25,4 @@ - - "192.0.2.{{ network_iphost }}/24" - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network -diff --git a/examples/infiniband.yml b/examples/infiniband.yml -index e7197fe..b8710d4 100644 ---- a/examples/infiniband.yml -+++ b/examples/infiniband.yml -@@ -22,4 +22,4 @@ - - 198.51.100.133/30 - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network -diff --git a/examples/macvlan.yml b/examples/macvlan.yml -index 0e6ba1b..14f5dd8 100644 ---- a/examples/macvlan.yml -+++ b/examples/macvlan.yml -@@ -24,4 +24,4 @@ - - 192.168.1.1/24 - - roles: -- - linux-system-roles.network -+ - rhel-system-roles.network diff --git a/rhel-system-roles-postfix-pr5.diff b/rhel-system-roles-postfix-pr5.diff deleted file mode 100644 index 4da6f48..0000000 --- a/rhel-system-roles-postfix-pr5.diff +++ /dev/null @@ -1,40 +0,0 @@ -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/rhel-system-roles-postfix-prefix.diff b/rhel-system-roles-postfix-prefix.diff deleted file mode 100644 index 65ab2a1..0000000 --- a/rhel-system-roles-postfix-prefix.diff +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/README.md b/README.md -index 5950215..a59d72f 100644 ---- a/README.md -+++ b/README.md -@@ -25,7 +25,7 @@ Install and enable postfix. Configure "relay_domains=$mydestination" and - relay_domains: "$mydestination" - relay_host: "example.com" - roles: -- - postfix -+ - linux-system-roles.postfix - ``` - - Install and enable postfix. Do not run 'postfix check' before restarting -@@ -37,7 +37,7 @@ postfix: - vars: - postfix_check: false - roles: -- - postfix -+ - linux-system-roles.postfix - ``` - - Install and enable postfix. Do single backup of main.cf (older backup will be -@@ -51,7 +51,7 @@ rewritten) and configure "relay_host=example.com": - relay_host: "example.com" - postfix_backup: true - roles: -- - postfix -+ - linux-system-roles.postfix - ``` - - Install and enable postfix. Do timestamped backup of main.cf and -@@ -66,7 +66,7 @@ set to true postfix_backup is ignored): - relay_host: "example.com" - postfix_backup_multiple: true - roles: -- - postfix -+ - linux-system-roles.postfix - ``` - - diff --git a/rhel-system-roles-selinux-pr30.diff b/rhel-system-roles-selinux-pr30.diff deleted file mode 100644 index 5df14b9..0000000 --- a/rhel-system-roles-selinux-pr30.diff +++ /dev/null @@ -1,56 +0,0 @@ -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/rhel-system-roles-selinux-prefix.diff b/rhel-system-roles-selinux-prefix.diff deleted file mode 100644 index 8820d6d..0000000 --- a/rhel-system-roles-selinux-prefix.diff +++ /dev/null @@ -1,32 +0,0 @@ -diff --git a/README.md b/README.md -index a0385b0..6efc62d 100644 ---- a/README.md -+++ b/README.md -@@ -41,7 +41,7 @@ This role can be configured using variables as it is described below. - vars: - [ see below ] - roles: -- - role: linux-system-roles.selinux -+ - role: rhel-system-roles.selinux - become: true - ``` - -diff --git a/selinux-playbook.yml b/selinux-playbook.yml -index 78d3953..b2348d5 100644 ---- a/selinux-playbook.yml -+++ b/selinux-playbook.yml -@@ -31,7 +31,7 @@ - - name: execute the role and catch errors - block: - - include_role: -- name: linux-system-roles.selinux -+ name: rhel-system-roles.selinux - rescue: - # Fail if failed for a different reason than selinux_reboot_required. - - name: handle errors -@@ -52,4 +52,4 @@ - - - name: reapply the role - include_role: -- name: linux-system-roles.selinux -+ name: rhel-system-roles.selinux diff --git a/rhel-system-roles-timesync-pr18.diff b/rhel-system-roles-timesync-pr18.diff deleted file mode 100644 index 325d745..0000000 --- a/rhel-system-roles-timesync-pr18.diff +++ /dev/null @@ -1,40 +0,0 @@ -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/rhel-system-roles-timesync-pr19.diff b/rhel-system-roles-timesync-pr19.diff deleted file mode 100644 index b3cb4cb..0000000 --- a/rhel-system-roles-timesync-pr19.diff +++ /dev/null @@ -1,22 +0,0 @@ -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/rhel-system-roles-timesync-prefix.diff b/rhel-system-roles-timesync-prefix.diff deleted file mode 100644 index 02f9bb0..0000000 --- a/rhel-system-roles-timesync-prefix.diff +++ /dev/null @@ -1,29 +0,0 @@ -diff --git a/README.md b/README.md -index 122b725..3866c2e 100644 ---- a/README.md -+++ b/README.md -@@ -70,7 +70,7 @@ Install and configure ntp to synchronize the system clock with three NTP servers - - hostname: baz.example.com - iburst: yes - roles: -- - timesync -+ - rhel-system-roles.timesync - ``` - - Install and configure linuxptp to synchronize the system clock with a -@@ -83,7 +83,7 @@ grandmaster in PTP domain number 0, which is accessible on interface eth0: - - number: 0 - interfaces: [ eth0 ] - roles: -- - timesync -+ - rhel-system-roles.timesync - ``` - - Install and configure chrony and linuxptp to synchronize the system clock with -@@ -110,5 +110,5 @@ synchronization: - transport: UDPv4 - delay: 0.000010 - roles: -- - timesync -+ - rhel-system-roles.timesync - ``` diff --git a/timesync-playbook.yml b/timesync-playbook.yml index 1b1b081..becc670 100644 --- a/timesync-playbook.yml +++ b/timesync-playbook.yml @@ -1,7 +1,7 @@ --- -- hosts: targets +- hosts: "{{ target }}" vars: - ntp_servers: + timesync_ntp_servers: - hostname: 0.rhel.pool.ntp.org iburst: yes - hostname: 1.rhel.pool.ntp.org diff --git a/timesync-pool-playbook.yml b/timesync-pool-playbook.yml index f186a53..fee6b3b 100644 --- a/timesync-pool-playbook.yml +++ b/timesync-pool-playbook.yml @@ -1,7 +1,7 @@ --- -- hosts: targets +- hosts: "{{ targets }}" vars: - ntp_servers: + timesync_ntp_servers: - hostname: 2.rhel.pool.ntp.org pool: yes iburst: yes