Modularize role

Splitting the role in smaller parts helps to keep the overview and to
develop separate tests.
This commit is contained in:
Till Maas 2018-07-30 18:55:08 +02:00
parent d866422d9d
commit 382c34197b
9 changed files with 1522 additions and 1460 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
#!/usr/bin/python3 -tt
# vim: fileencoding=utf8
# SPDX-License-Identifier: BSD-3-Clause
class MyError(Exception):
pass

File diff suppressed because it is too large Load diff

View file

@ -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))

View file

@ -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.

View file

@ -0,0 +1 @@
../../../module_utils/

View file

@ -12,6 +12,18 @@ import unittest
TESTS_BASEDIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(1, os.path.join(TESTS_BASEDIR, "..", "library"))
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,7 +63,8 @@ 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):
@ -59,7 +72,7 @@ class TestValidator(unittest.TestCase):
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 +105,7 @@ class TestValidator(unittest.TestCase):
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 +116,9 @@ class TestValidator(unittest.TestCase):
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 +147,7 @@ class TestValidator(unittest.TestCase):
)
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 +180,26 @@ class TestValidator(unittest.TestCase):
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 +208,12 @@ class TestValidator(unittest.TestCase):
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 +235,22 @@ class TestValidator(unittest.TestCase):
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,16 +263,18 @@ class TestValidator(unittest.TestCase):
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_minimal_ethernet(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -295,6 +318,9 @@ class TestValidator(unittest.TestCase):
],
[{"name": "5", "type": "ethernet"}, {"name": "5"}],
)
def test_up_ethernet(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -333,6 +359,9 @@ class TestValidator(unittest.TestCase):
],
[{"name": "5", "state": "up", "type": "ethernet"}],
)
def test_up_ethernet_no_autoconnect(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -390,13 +419,19 @@ class TestValidator(unittest.TestCase):
],
)
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"}],
)
def test_up_ethernet_mac_mtu_static_ip(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -452,6 +487,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_up_single_v4_dns(self):
self.maxDiff = None
# set single IPv4 DNS server
self.do_connections_validate(
[
@ -505,6 +542,9 @@ class TestValidator(unittest.TestCase):
}
],
)
def test_routes(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -634,6 +674,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_vlan(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -763,6 +805,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_macvlan(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -943,73 +987,75 @@ class TestValidator(unittest.TestCase):
],
)
def test_bridge_no_dhcp4_auto6(self):
self.maxDiff = None
self.do_connections_validate(
[
{
"autoconnect": True,
"name": "prod2",
"parent": None,
"ip": {
"dhcp4": False,
"route_metric6": None,
"route_metric4": None,
"dns_search": [],
"dhcp4_send_hostname": None,
"gateway6": None,
"gateway4": None,
"auto6": False,
"dns": [],
"address": [],
"route_append_only": False,
"rule_append_only": False,
"route": [],
},
"ethernet": {"autoneg": None, "duplex": None, "speed": 0},
"mac": None,
"mtu": None,
"zone": None,
"check_iface_exists": True,
"ethernet": {"autoneg": None, "duplex": None, "speed": 0},
"force_state_change": None,
"state": "up",
"master": None,
"ignore_errors": None,
"interface_name": "bridge2",
"type": "bridge",
"ip": {
"address": [],
"auto6": False,
"dhcp4": False,
"dhcp4_send_hostname": None,
"dns": [],
"dns_search": [],
"gateway4": None,
"gateway6": None,
"route": [],
"route_append_only": False,
"route_metric4": None,
"route_metric6": None,
"rule_append_only": False,
},
"mac": None,
"master": None,
"mtu": None,
"name": "prod2",
"parent": None,
"slave_type": None,
"state": "up",
"type": "bridge",
"wait": None,
"zone": None,
},
{
"autoconnect": True,
"name": "prod2-slave1",
"parent": None,
"ip": {
"dhcp4": True,
"auto6": True,
"address": [],
"route_append_only": False,
"rule_append_only": False,
"route": [],
"route_metric6": None,
"route_metric4": None,
"dns_search": [],
"dhcp4_send_hostname": None,
"gateway6": None,
"gateway4": None,
"dns": [],
},
"ethernet": {"autoneg": None, "duplex": None, "speed": 0},
"mac": None,
"mtu": None,
"zone": None,
"check_iface_exists": True,
"ethernet": {"autoneg": None, "duplex": None, "speed": 0},
"force_state_change": None,
"state": "up",
"master": "prod2",
"ignore_errors": None,
"interface_name": "eth1",
"type": "ethernet",
"ip": {
"address": [],
"auto6": True,
"dhcp4": True,
"dhcp4_send_hostname": None,
"dns": [],
"dns_search": [],
"gateway4": None,
"gateway6": None,
"route": [],
"route_append_only": False,
"route_metric4": None,
"route_metric6": None,
"rule_append_only": False,
},
"mac": None,
"master": "prod2",
"mtu": None,
"name": "prod2-slave1",
"parent": None,
"slave_type": "bridge",
"state": "up",
"type": "ethernet",
"wait": None,
"zone": None,
},
],
[
@ -1030,6 +1076,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_bond(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1070,6 +1118,8 @@ class TestValidator(unittest.TestCase):
[{"name": "bond1", "state": "up", "type": "bond"}],
)
def test_bond_active_backup(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1117,9 +1167,13 @@ class TestValidator(unittest.TestCase):
],
)
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(
[
{
@ -1157,45 +1211,8 @@ class TestValidator(unittest.TestCase):
[{"name": "5", "type": "ethernet", "mac": "AA:bb:cC:DD:ee:FF"}],
)
self.do_connections_validate(
[
{
"name": "5",
"state": "up",
"type": "ethernet",
"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,
"force_state_change": None,
"slave_type": None,
"wait": None,
}
],
[{"name": "5", "state": "up", "type": "ethernet"}],
)
def test_ethernet_speed_settings(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1262,6 +1279,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_bridge2(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1307,8 +1326,8 @@ class TestValidator(unittest.TestCase):
"ip": {
"address": [],
"auto6": True,
"dhcp4": True,
"dhcp4_send_hostname": None,
"dhcp4": True,
"dns": [],
"dns_search": [],
"gateway4": None,
@ -1342,6 +1361,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_infiniband(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1406,6 +1427,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_infiniband2(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1477,6 +1500,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_route_metric_prefix(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1563,6 +1588,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_route_v6(self):
self.maxDiff = None
self.do_connections_validate(
[
{
@ -1688,6 +1715,8 @@ class TestValidator(unittest.TestCase):
],
)
def test_invalid_mac(self):
self.maxDiff = None
self.do_connections_check_invalid(
[{"name": "b", "type": "ethernet", "mac": "aa:b"}]
)

View file

@ -18,12 +18,12 @@
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

View file

@ -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]