summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configdict.py2
-rw-r--r--python/vyos/configsession.py2
-rw-r--r--python/vyos/configverify.py12
-rw-r--r--python/vyos/ifconfig/__init__.py2
-rw-r--r--python/vyos/ifconfig/dhcp.py131
-rw-r--r--python/vyos/ifconfig/interface.py95
-rw-r--r--python/vyos/template.py163
-rw-r--r--python/vyos/util.py35
8 files changed, 218 insertions, 224 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 126d6195a..010eda45c 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -23,7 +23,6 @@ from enum import Enum
from copy import deepcopy
from vyos import ConfigError
-from vyos.validate import is_member
def retrieve_config(path_hash, base_path, config):
"""
@@ -203,6 +202,7 @@ def get_interface_dict(config, base, ifname=''):
Will return a dictionary with the necessary interface configuration
"""
from vyos.xml import defaults
+ from vyos.validate import is_member
if not ifname:
# determine tagNode instance
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index f2524b37e..0994fd974 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -26,7 +26,7 @@ DISCARD = '/opt/vyatta/sbin/my_discard'
SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
-INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image']
+INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index bb590a514..d1519b0ac 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -29,11 +29,11 @@ def verify_vrf(config):
recurring validation of VRF configuration.
"""
from netifaces import interfaces
- if 'vrf' in config.keys():
+ if 'vrf' in config:
if config['vrf'] not in interfaces():
raise ConfigError('VRF "{vrf}" does not exist'.format(**config))
- if 'is_bridge_member' in config.keys():
+ if 'is_bridge_member' in config:
raise ConfigError(
'Interface "{ifname}" cannot be both a member of VRF "{vrf}" '
'and bridge "{is_bridge_member}"!'.format(**config))
@@ -57,7 +57,7 @@ def verify_bridge_delete(config):
perform recurring validation of IP address assignmenr
when interface also is part of a bridge.
"""
- if 'is_bridge_member' in config.keys():
+ if 'is_bridge_member' in config:
raise ConfigError(
'Interface "{ifname}" cannot be deleted as it is a '
'member of bridge "{is_bridge_member}"!'.format(**config))
@@ -101,20 +101,20 @@ def verify_vlan_config(config):
recurring validation of interface VLANs
"""
# 802.1q VLANs
- for vlan in config.get('vif', {}).keys():
+ for vlan in config.get('vif', {}):
vlan = config['vif'][vlan]
verify_dhcpv6(vlan)
verify_address(vlan)
verify_vrf(vlan)
# 802.1ad (Q-in-Q) VLANs
- for vlan in config.get('vif_s', {}).keys():
+ for vlan in config.get('vif_s', {}):
vlan = config['vif_s'][vlan]
verify_dhcpv6(vlan)
verify_address(vlan)
verify_vrf(vlan)
- for vlan in config.get('vif_s', {}).get('vif_c', {}).keys():
+ for vlan in config.get('vif_s', {}).get('vif_c', {}):
vlan = config['vif_c'][vlan]
verify_dhcpv6(vlan)
verify_address(vlan)
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index a7cdeadd1..9cd8d44c1 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -13,12 +13,10 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
from vyos.ifconfig.section import Section
from vyos.ifconfig.control import Control
from vyos.ifconfig.interface import Interface
from vyos.ifconfig.operational import Operational
-from vyos.ifconfig.dhcp import DHCP
from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.bond import BondIf
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
deleted file mode 100644
index 63224fc0f..000000000
--- a/python/vyos/ifconfig/dhcp.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import jmespath
-
-from vyos.configdict import dict_merge
-from vyos.configverify import verify_dhcpv6
-from vyos.ifconfig.control import Control
-from vyos.template import render
-
-class _DHCPv4 (Control):
- def __init__(self, ifname):
- super().__init__()
- config_base = r'/var/lib/dhcp/dhclient'
- self._conf_file = f'{config_base}_{ifname}.conf'
- self._options_file = f'{config_base}_{ifname}.options'
- self._pid_file = f'{config_base}_{ifname}.pid'
- self._lease_file = f'{config_base}_{ifname}.leases'
- self.options = {'ifname' : ifname}
-
- # replace dhcpv4/v6 with systemd.networkd?
- def set(self):
- """
- Configure interface as DHCP client. The dhclient binary is automatically
- started in background!
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.dhcp.v4.set()
- """
-
- if jmespath.search('dhcp_options.host_name', self.options) == None:
- # read configured system hostname.
- # maybe change to vyos hostd client ???
- hostname = 'vyos'
- with open('/etc/hostname', 'r') as f:
- hostname = f.read().rstrip('\n')
- tmp = {'dhcp_options' : { 'host_name' : hostname}}
- self.options = dict_merge(tmp, self.options)
-
- render(self._options_file, 'dhcp-client/daemon-options.tmpl',
- self.options, trim_blocks=True)
- render(self._conf_file, 'dhcp-client/ipv4.tmpl',
- self.options, trim_blocks=True)
-
- return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options))
-
- def delete(self):
- """
- De-configure interface as DHCP clinet. All auto generated files like
- pid, config and lease will be removed.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.dhcp.v4.delete()
- """
- if not os.path.isfile(self._pid_file):
- self._debug_msg('No DHCP client PID found')
- return None
-
- self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options))
-
- # cleanup old config files
- for file in [self._conf_file, self._options_file, self._pid_file, self._lease_file]:
- if os.path.isfile(file):
- os.remove(file)
-
-class _DHCPv6 (Control):
- def __init__(self, ifname):
- super().__init__()
- self.options = {'ifname' : ifname}
- self._config = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
-
- def set(self):
- """
- Configure interface as DHCPv6 client. The client is automatically
- started in background when address is configured as DHCP.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.dhcp.v6.set()
- """
-
- # better save then sorry .. should be checked in interface script but if you
- # missed it we are safe!
- verify_dhcpv6(self.options)
-
- render(self._config, 'dhcp-client/ipv6.tmpl',
- self.options, trim_blocks=True)
-
- # We must ignore any return codes. This is required to enable DHCPv6-PD
- # for interfaces which are yet not up and running.
- return self._popen('systemctl restart dhcp6c@{ifname}.service'.format(
- **self.options))
-
- def delete(self):
- """
- De-configure interface as DHCPv6 client. All auto generated files like
- pid, config and lease will be removed.
-
- Example:
- >>> from vyos.ifconfig import Interface
- >>> j = Interface('eth0')
- >>> j.dhcp.v6.delete()
- """
- self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options))
-
- # cleanup old config files
- if os.path.isfile(self._config):
- os.remove(self._config)
-
-class DHCP(object):
- def __init__(self, ifname):
- self.v4 = _DHCPv4(ifname)
- self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 214ece8cd..36f258301 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -31,6 +31,8 @@ from netifaces import AF_INET6
from vyos import ConfigError
from vyos.configdict import list_diff
+from vyos.configdict import dict_merge
+from vyos.template import render
from vyos.util import mac2eui64
from vyos.validate import is_ipv4
from vyos.validate import is_ipv6
@@ -43,7 +45,6 @@ from vyos.validate import assert_positive
from vyos.validate import assert_range
from vyos.ifconfig.control import Control
-from vyos.ifconfig.dhcp import DHCP
from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.operational import Operational
from vyos.ifconfig import Section
@@ -216,7 +217,6 @@ class Interface(Control):
# we must have updated config before initialising the Interface
super().__init__(**kargs)
self.ifname = ifname
- self.dhcp = DHCP(ifname)
if not self.exists(ifname):
# Any instance of Interface, such as Interface('eth0')
@@ -722,9 +722,9 @@ class Interface(Control):
# add to interface
if addr == 'dhcp':
- self.dhcp.v4.set()
+ self.set_dhcp(True)
elif addr == 'dhcpv6':
- self.dhcp.v6.set()
+ self.set_dhcpv6(True)
elif not is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr add "{addr}" '
f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
@@ -763,9 +763,9 @@ class Interface(Control):
# remove from interface
if addr == 'dhcp':
- self.dhcp.v4.delete()
+ self.set_dhcp(False)
elif addr == 'dhcpv6':
- self.dhcp.v6.delete()
+ self.set_dhcpv6(False)
elif is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
else:
@@ -784,8 +784,8 @@ class Interface(Control):
Will raise an exception on error.
"""
# stop DHCP(v6) if running
- self.dhcp.v4.delete()
- self.dhcp.v6.delete()
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
# flush all addresses
self._cmd(f'ip addr flush dev "{self.ifname}"')
@@ -809,12 +809,83 @@ class Interface(Control):
return True
+ def set_dhcp(self, enable):
+ """
+ Enable/Disable DHCP client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_base = r'/var/lib/dhcp/dhclient'
+ config_file = f'{config_base}_{ifname}.conf'
+ options_file = f'{config_base}_{ifname}.options'
+ pid_file = f'{config_base}_{ifname}.pid'
+ lease_file = f'{config_base}_{ifname}.leases'
+
+ if enable and 'disable' not in self._config:
+ if jmespath.search('dhcp_options.host_name', self._config) == None:
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ hostname = 'vyos'
+ with open('/etc/hostname', 'r') as f:
+ hostname = f.read().rstrip('\n')
+ tmp = {'dhcp_options' : { 'host_name' : hostname}}
+ self._config = dict_merge(tmp, self._config)
+
+ render(options_file, 'dhcp-client/daemon-options.tmpl',
+ self._config, trim_blocks=True)
+ render(config_file, 'dhcp-client/ipv4.tmpl',
+ self._config, trim_blocks=True)
+
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
+ return self._cmd(f'systemctl restart dhclient@{ifname}.service')
+ else:
+ self._cmd(f'systemctl stop dhclient@{ifname}.service')
+
+ # cleanup old config files
+ for file in [config_file, options_file, pid_file, lease_file]:
+ if os.path.isfile(file):
+ os.remove(file)
+
+
+ def set_dhcpv6(self, enable):
+ """
+ Enable/Disable DHCPv6 client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+
+ if enable and 'disable' not in self._config:
+ render(config_file, 'dhcp-client/ipv6.tmpl',
+ self._config, trim_blocks=True)
+
+ # We must ignore any return codes. This is required to enable DHCPv6-PD
+ # for interfaces which are yet not up and running.
+ return self._popen(f'systemctl restart dhcp6c@{ifname}.service')
+ else:
+ self._popen(f'systemctl stop dhcp6c@{ifname}.service')
+
+ if os.path.isfile(config_file):
+ os.remove(config_file)
+
+
def update(self, config):
""" General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
interface setup code and provide a single point of entry when workin
on any interface. """
+ # Cache the configuration - it will be reused inside e.g. DHCP handler
+ # XXX: maybe pass the option via __init__ in the future and rename this
+ # method to apply()?
+ self._config = config
+
# Update interface description
self.set_alias(config.get('description', ''))
@@ -822,14 +893,6 @@ class Interface(Control):
value = '2' if 'disable_link_detect' in config else '1'
self.set_link_detect(value)
- # DHCP options
- if 'dhcp_options' in config:
- self.dhcp.v4.options = config
-
- # DHCPv6 options
- if 'dhcpv6_options' in config:
- self.dhcp.v6.options = config
-
# Configure assigned interface IP addresses. No longer
# configured addresses will be removed first
new_addr = config.get('address', [])
diff --git a/python/vyos/template.py b/python/vyos/template.py
index d9b0c749d..c88ab04a0 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -13,78 +13,131 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import functools
import os
+from ipaddress import ip_network
from jinja2 import Environment
from jinja2 import FileSystemLoader
from vyos.defaults import directories
from vyos.util import chmod, chown, makedir
-# reuse the same Environment to improve performance
-_templates_env = {
- False: Environment(loader=FileSystemLoader(directories['templates'])),
- True: Environment(loader=FileSystemLoader(directories['templates']), trim_blocks=True),
-}
-_templates_mem = {
- False: {},
- True: {},
-}
-def vyos_address_from_cidr(text):
- """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
- Example:
- 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
- """
- from ipaddress import ip_network
- return ip_network(text).network_address
+# Holds template filters registered via register_filter()
+_FILTERS = {}
-def vyos_netmask_from_cidr(text):
- """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask".
- Example:
- 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff::
- """
- from ipaddress import ip_network
- return ip_network(text).netmask
-def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None):
+# reuse Environments with identical trim_blocks setting to improve performance
+@functools.lru_cache(maxsize=2)
+def _get_environment(trim_blocks):
+ env = Environment(
+ # Don't check if template files were modified upon re-rendering
+ auto_reload=False,
+ # Cache up to this number of templates for quick re-rendering
+ cache_size=100,
+ loader=FileSystemLoader(directories["templates"]),
+ trim_blocks=trim_blocks,
+ )
+ env.filters.update(_FILTERS)
+ return env
+
+
+def register_filter(name, func=None):
+ """Register a function to be available as filter in templates under given name.
+
+ It can also be used as a decorator, see below in this module for examples.
+
+ :raise RuntimeError:
+ when trying to register a filter after a template has been rendered already
+ :raise ValueError: when trying to register a name which was taken already
"""
- render a template from the template directory, it will raise on any errors
- destination: the file where the rendered template must be saved
- template: the path to the template relative to the template folder
- content: the dictionary to use to render the template
-
- This classes cache the renderer, so rendering the same file multiple time
- does not cause as too much overhead. If use everywhere, it could be changed
- and load the template from python environement variables from an import
- python module generated when the debian package is build
- (recovering the load time and overhead caused by having the file out of the code)
+ if func is None:
+ return functools.partial(register_filter, name)
+ if _get_environment.cache_info().currsize:
+ raise RuntimeError(
+ "Filters can only be registered before rendering the first template"
+ )
+ if name in _FILTERS:
+ raise ValueError(f"A filter with name {name!r} was registered already")
+ _FILTERS[name] = func
+ return func
+
+
+def render_to_string(template, content, trim_blocks=False, formater=None):
+ """Render a template from the template directory, raise on any errors.
+
+ :param template: the path to the template relative to the template folder
+ :param content: the dictionary of variables to put into rendering context
+ :param trim_blocks: controls the trim_blocks jinja2 feature
+ :param formater:
+ if given, it has to be a callable the rendered string is passed through
+
+ The parsed template files are cached, so rendering the same file multiple times
+ does not cause as too much overhead.
+ If used everywhere, it could be changed to load the template from Python
+ environment variables from an importable Python module generated when the Debian
+ package is build (recovering the load time and overhead caused by having the
+ file out of the code).
"""
+ template = _get_environment(bool(trim_blocks)).get_template(template)
+ rendered = template.render(content)
+ if formater is not None:
+ rendered = formater(rendered)
+ return rendered
+
+
+def render(
+ destination,
+ template,
+ content,
+ trim_blocks=False,
+ formater=None,
+ permission=None,
+ user=None,
+ group=None,
+):
+ """Render a template from the template directory to a file, raise on any errors.
- # Create the directory if it does not exists
+ :param destination: path to the file to save the rendered template in
+ :param permission: permission bitmask to set for the output file
+ :param user: user to own the output file
+ :param group: group to own the output file
+
+ All other parameters are as for :func:`render_to_string`.
+ """
+ # Create the directory if it does not exist
folder = os.path.dirname(destination)
makedir(folder, user, group)
- # Setup a renderer for the given template
- # This is cached and re-used for performance
- if template not in _templates_mem[trim_blocks]:
- _env = _templates_env[trim_blocks]
- _env.filters['address_from_cidr'] = vyos_address_from_cidr
- _env.filters['netmask_from_cidr'] = vyos_netmask_from_cidr
- _templates_mem[trim_blocks][template] = _env.get_template(template)
+ # As we are opening the file with 'w', we are performing the rendering before
+ # calling open() to not accidentally erase the file if rendering fails
+ rendered = render_to_string(template, content, trim_blocks, formater)
- template = _templates_mem[trim_blocks][template]
+ # Write to file
+ with open(destination, "w") as file:
+ chmod(file.fileno(), permission)
+ chown(file.fileno(), user, group)
+ file.write(rendered)
- # As we are opening the file with 'w', we are performing the rendering
- # before calling open() to not accidentally erase the file if the
- # templating fails
- content = template.render(content)
- if formater:
- content = formater(content)
+##################################
+# Custom template filters follow #
+##################################
- # Write client config file
- with open(destination, 'w') as f:
- f.write(content)
- chmod(destination, permission)
- chown(destination, user, group)
+@register_filter("address_from_cidr")
+def vyos_address_from_cidr(text):
+ """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
+ Example:
+ 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
+ """
+ return ip_network(text).network_address
+
+
+@register_filter("netmask_from_cidr")
+def vyos_netmask_from_cidr(text):
+ """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask".
+ Example:
+ 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff::
+ """
+ return ip_network(text).netmask
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 7078762df..d27a8a3e7 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -240,7 +240,8 @@ def chown(path, user, group):
if user is None or group is None:
return False
- if not os.path.exists(path):
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
return False
uid = getpwnam(user).pw_uid
@@ -250,7 +251,8 @@ def chown(path, user, group):
def chmod(path, bitmask):
- if not os.path.exists(path):
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
return
if bitmask is None:
return
@@ -261,28 +263,25 @@ def chmod_600(path):
""" make file only read/writable by owner """
from stat import S_IRUSR, S_IWUSR
- if os.path.exists(path):
- bitmask = S_IRUSR | S_IWUSR
- os.chmod(path, bitmask)
+ bitmask = S_IRUSR | S_IWUSR
+ chmod(path, bitmask)
def chmod_750(path):
""" make file/directory only executable to user and group """
from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
- if os.path.exists(path):
- bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
- os.chmod(path, bitmask)
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
+ chmod(path, bitmask)
def chmod_755(path):
""" make file executable by all """
from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
- if os.path.exists(path):
- bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
- S_IROTH | S_IXOTH
- os.chmod(path, bitmask)
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
+ S_IROTH | S_IXOTH
+ chmod(path, bitmask)
def makedir(path, user=None, group=None):
@@ -661,3 +660,15 @@ def check_kmod(k_mod):
if not os.path.exists(f'/sys/module/{module}'):
if call(f'modprobe {module}') != 0:
raise ConfigError(f'Loading Kernel module {module} failed')
+
+def find_device_file(device):
+ """ Recurively search /dev for the given device file and return its full path.
+ If no device file was found 'None' is returned """
+ from fnmatch import fnmatch
+
+ for root, dirs, files in os.walk('/dev'):
+ for basename in files:
+ if fnmatch(basename, device):
+ return os.path.join(root, basename)
+
+ return None