Blame cloudinit/net/dhcp.py

Packit Service a04d08
# Copyright (C) 2017 Canonical Ltd.
Packit Service a04d08
#
Packit Service a04d08
# Author: Chad Smith <chad.smith@canonical.com>
Packit Service a04d08
#
Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
import configobj
Packit Service a04d08
import logging
Packit Service a04d08
import os
Packit Service a04d08
import re
Packit Service a04d08
import signal
Packit Service a04d08
import time
Packit Service 9bfd13
from io import StringIO
Packit Service a04d08
Packit Service a04d08
from cloudinit.net import (
Packit Service a04d08
    EphemeralIPv4Network, find_fallback_nic, get_devicelist,
Packit Service a04d08
    has_url_connectivity)
Packit Service a04d08
from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip
Packit Service a04d08
from cloudinit import temp_utils
Packit Service 9bfd13
from cloudinit import subp
Packit Service a04d08
from cloudinit import util
Packit Service a04d08
Packit Service a04d08
LOG = logging.getLogger(__name__)
Packit Service a04d08
Packit Service a04d08
NETWORKD_LEASES_DIR = '/run/systemd/netif/leases'
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class InvalidDHCPLeaseFileError(Exception):
Packit Service a04d08
    """Raised when parsing an empty or invalid dhcp.leases file.
Packit Service a04d08
Packit Service a04d08
    Current uses are DataSourceAzure and DataSourceEc2 during ephemeral
Packit Service a04d08
    boot to scrape metadata.
Packit Service a04d08
    """
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class NoDHCPLeaseError(Exception):
Packit Service a04d08
    """Raised when unable to get a DHCP lease."""
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class EphemeralDHCPv4(object):
Packit Service 9bfd13
    def __init__(self, iface=None, connectivity_url=None, dhcp_log_func=None):
Packit Service a04d08
        self.iface = iface
Packit Service a04d08
        self._ephipv4 = None
Packit Service a04d08
        self.lease = None
Packit Service 9bfd13
        self.dhcp_log_func = dhcp_log_func
Packit Service a04d08
        self.connectivity_url = connectivity_url
Packit Service a04d08
Packit Service a04d08
    def __enter__(self):
Packit Service a04d08
        """Setup sandboxed dhcp context, unless connectivity_url can already be
Packit Service a04d08
        reached."""
Packit Service a04d08
        if self.connectivity_url:
Packit Service a04d08
            if has_url_connectivity(self.connectivity_url):
Packit Service a04d08
                LOG.debug(
Packit Service a04d08
                    'Skip ephemeral DHCP setup, instance has connectivity'
Packit Service a04d08
                    ' to %s', self.connectivity_url)
Packit Service a04d08
                return
Packit Service a04d08
        return self.obtain_lease()
Packit Service a04d08
Packit Service a04d08
    def __exit__(self, excp_type, excp_value, excp_traceback):
Packit Service a04d08
        """Teardown sandboxed dhcp context."""
Packit Service a04d08
        self.clean_network()
Packit Service a04d08
Packit Service a04d08
    def clean_network(self):
Packit Service a04d08
        """Exit _ephipv4 context to teardown of ip configuration performed."""
Packit Service a04d08
        if self.lease:
Packit Service a04d08
            self.lease = None
Packit Service a04d08
        if not self._ephipv4:
Packit Service a04d08
            return
Packit Service a04d08
        self._ephipv4.__exit__(None, None, None)
Packit Service a04d08
Packit Service a04d08
    def obtain_lease(self):
Packit Service a04d08
        """Perform dhcp discovery in a sandboxed environment if possible.
Packit Service a04d08
Packit Service a04d08
        @return: A dict representing dhcp options on the most recent lease
Packit Service a04d08
            obtained from the dhclient discovery if run, otherwise an error
Packit Service a04d08
            is raised.
Packit Service a04d08
Packit Service a04d08
        @raises: NoDHCPLeaseError if no leases could be obtained.
Packit Service a04d08
        """
Packit Service a04d08
        if self.lease:
Packit Service a04d08
            return self.lease
Packit Service a04d08
        try:
Packit Service 9bfd13
            leases = maybe_perform_dhcp_discovery(
Packit Service 9bfd13
                self.iface, self.dhcp_log_func)
Packit Service 9bfd13
        except InvalidDHCPLeaseFileError as e:
Packit Service 9bfd13
            raise NoDHCPLeaseError() from e
Packit Service a04d08
        if not leases:
Packit Service a04d08
            raise NoDHCPLeaseError()
Packit Service a04d08
        self.lease = leases[-1]
Packit Service a04d08
        LOG.debug("Received dhcp lease on %s for %s/%s",
Packit Service a04d08
                  self.lease['interface'], self.lease['fixed-address'],
Packit Service a04d08
                  self.lease['subnet-mask'])
Packit Service a04d08
        nmap = {'interface': 'interface', 'ip': 'fixed-address',
Packit Service a04d08
                'prefix_or_mask': 'subnet-mask',
Packit Service a04d08
                'broadcast': 'broadcast-address',
Packit Service a04d08
                'static_routes': [
Packit Service a04d08
                    'rfc3442-classless-static-routes',
Packit Service a04d08
                    'classless-static-routes'
Packit Service a04d08
                ],
Packit Service a04d08
                'router': 'routers'}
Packit Service a04d08
        kwargs = self.extract_dhcp_options_mapping(nmap)
Packit Service a04d08
        if not kwargs['broadcast']:
Packit Service a04d08
            kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
Packit Service a04d08
        if kwargs['static_routes']:
Packit Service a04d08
            kwargs['static_routes'] = (
Packit Service a04d08
                parse_static_routes(kwargs['static_routes']))
Packit Service a04d08
        if self.connectivity_url:
Packit Service a04d08
            kwargs['connectivity_url'] = self.connectivity_url
Packit Service a04d08
        ephipv4 = EphemeralIPv4Network(**kwargs)
Packit Service a04d08
        ephipv4.__enter__()
Packit Service a04d08
        self._ephipv4 = ephipv4
Packit Service a04d08
        return self.lease
Packit Service a04d08
Packit Service a04d08
    def extract_dhcp_options_mapping(self, nmap):
Packit Service a04d08
        result = {}
Packit Service a04d08
        for internal_reference, lease_option_names in nmap.items():
Packit Service a04d08
            if isinstance(lease_option_names, list):
Packit Service a04d08
                self.get_first_option_value(
Packit Service a04d08
                    internal_reference,
Packit Service a04d08
                    lease_option_names,
Packit Service a04d08
                    result
Packit Service a04d08
                )
Packit Service a04d08
            else:
Packit Service a04d08
                result[internal_reference] = self.lease.get(lease_option_names)
Packit Service a04d08
        return result
Packit Service a04d08
Packit Service a04d08
    def get_first_option_value(self, internal_mapping,
Packit Service a04d08
                               lease_option_names, result):
Packit Service a04d08
        for different_names in lease_option_names:
Packit Service a04d08
            if not result.get(internal_mapping):
Packit Service a04d08
                result[internal_mapping] = self.lease.get(different_names)
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None):
Packit Service a04d08
    """Perform dhcp discovery if nic valid and dhclient command exists.
Packit Service a04d08
Packit Service a04d08
    If the nic is invalid or undiscoverable or dhclient command is not found,
Packit Service a04d08
    skip dhcp_discovery and return an empty dict.
Packit Service a04d08
Packit Service a04d08
    @param nic: Name of the network interface we want to run dhclient on.
Packit Service 9bfd13
    @param dhcp_log_func: A callable accepting the dhclient output and error
Packit Service 9bfd13
        streams.
Packit Service a04d08
    @return: A list of dicts representing dhcp options for each lease obtained
Packit Service a04d08
        from the dhclient discovery if run, otherwise an empty list is
Packit Service a04d08
        returned.
Packit Service a04d08
    """
Packit Service a04d08
    if nic is None:
Packit Service a04d08
        nic = find_fallback_nic()
Packit Service a04d08
        if nic is None:
Packit Service a04d08
            LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.')
Packit Service a04d08
            return []
Packit Service a04d08
    elif nic not in get_devicelist():
Packit Service a04d08
        LOG.debug(
Packit Service a04d08
            'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic)
Packit Service a04d08
        return []
Packit Service 9bfd13
    dhclient_path = subp.which('dhclient')
Packit Service a04d08
    if not dhclient_path:
Packit Service a04d08
        LOG.debug('Skip dhclient configuration: No dhclient command found.')
Packit Service a04d08
        return []
Packit Service a04d08
    with temp_utils.tempdir(rmtree_ignore_errors=True,
Packit Service a04d08
                            prefix='cloud-init-dhcp-',
Packit Service a04d08
                            needs_exe=True) as tdir:
Packit Service a04d08
        # Use /var/tmp because /run/cloud-init/tmp is mounted noexec
Packit Service 9bfd13
        return dhcp_discovery(dhclient_path, nic, tdir, dhcp_log_func)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def parse_dhcp_lease_file(lease_file):
Packit Service a04d08
    """Parse the given dhcp lease file for the most recent lease.
Packit Service a04d08
Packit Service a04d08
    Return a list of dicts of dhcp options. Each dict contains key value pairs
Packit Service a04d08
    a specific lease in order from oldest to newest.
Packit Service a04d08
Packit Service a04d08
    @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile
Packit Service a04d08
        content.
Packit Service a04d08
    """
Packit Service a04d08
    lease_regex = re.compile(r"lease {(?P<lease>[^}]*)}\n")
Packit Service a04d08
    dhcp_leases = []
Packit Service a04d08
    lease_content = util.load_file(lease_file)
Packit Service a04d08
    if len(lease_content) == 0:
Packit Service a04d08
        raise InvalidDHCPLeaseFileError(
Packit Service a04d08
            'Cannot parse empty dhcp lease file {0}'.format(lease_file))
Packit Service a04d08
    for lease in lease_regex.findall(lease_content):
Packit Service a04d08
        lease_options = []
Packit Service a04d08
        for line in lease.split(';'):
Packit Service a04d08
            # Strip newlines, double-quotes and option prefix
Packit Service a04d08
            line = line.strip().replace('"', '').replace('option ', '')
Packit Service a04d08
            if not line:
Packit Service a04d08
                continue
Packit Service a04d08
            lease_options.append(line.split(' ', 1))
Packit Service a04d08
        dhcp_leases.append(dict(lease_options))
Packit Service a04d08
    if not dhcp_leases:
Packit Service a04d08
        raise InvalidDHCPLeaseFileError(
Packit Service a04d08
            'Cannot parse dhcp lease file {0}. No leases found'.format(
Packit Service a04d08
                lease_file))
Packit Service a04d08
    return dhcp_leases
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None):
Packit Service a04d08
    """Run dhclient on the interface without scripts or filesystem artifacts.
Packit Service a04d08
Packit Service a04d08
    @param dhclient_cmd_path: Full path to the dhclient used.
Packit Service a04d08
    @param interface: Name of the network inteface on which to dhclient.
Packit Service a04d08
    @param cleandir: The directory from which to run dhclient as well as store
Packit Service a04d08
        dhcp leases.
Packit Service 9bfd13
    @param dhcp_log_func: A callable accepting the dhclient output and error
Packit Service 9bfd13
        streams.
Packit Service a04d08
Packit Service a04d08
    @return: A list of dicts of representing the dhcp leases parsed from the
Packit Service a04d08
        dhcp.leases file or empty list.
Packit Service a04d08
    """
Packit Service a04d08
    LOG.debug('Performing a dhcp discovery on %s', interface)
Packit Service a04d08
Packit Service a04d08
    # XXX We copy dhclient out of /sbin/dhclient to avoid dealing with strict
Packit Service a04d08
    # app armor profiles which disallow running dhclient -sf <our-script-file>.
Packit Service a04d08
    # We want to avoid running /sbin/dhclient-script because of side-effects in
Packit Service a04d08
    # /etc/resolv.conf any any other vendor specific scripts in
Packit Service a04d08
    # /etc/dhcp/dhclient*hooks.d.
Packit Service a04d08
    sandbox_dhclient_cmd = os.path.join(cleandir, 'dhclient')
Packit Service a04d08
    util.copy(dhclient_cmd_path, sandbox_dhclient_cmd)
Packit Service a04d08
    pid_file = os.path.join(cleandir, 'dhclient.pid')
Packit Service a04d08
    lease_file = os.path.join(cleandir, 'dhcp.leases')
Packit Service a04d08
Packit Service 9bfd13
    # In some cases files in /var/tmp may not be executable, launching dhclient
Packit Service 9bfd13
    # from there will certainly raise 'Permission denied' error. Try launching
Packit Service 9bfd13
    # the original dhclient instead.
Packit Service 9bfd13
    if not os.access(sandbox_dhclient_cmd, os.X_OK):
Packit Service 9bfd13
        sandbox_dhclient_cmd = dhclient_cmd_path
Packit Service 9bfd13
Packit Service a04d08
    # ISC dhclient needs the interface up to send initial discovery packets.
Packit Service a04d08
    # Generally dhclient relies on dhclient-script PREINIT action to bring the
Packit Service a04d08
    # link up before attempting discovery. Since we are using -sf /bin/true,
Packit Service a04d08
    # we need to do that "link up" ourselves first.
Packit Service 9bfd13
    subp.subp(['ip', 'link', 'set', 'dev', interface, 'up'], capture=True)
Packit Service a04d08
    cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,
Packit Service a04d08
           '-pf', pid_file, interface, '-sf', '/bin/true']
Packit Service 9bfd13
    out, err = subp.subp(cmd, capture=True)
Packit Service a04d08
Packit Service a04d08
    # Wait for pid file and lease file to appear, and for the process
Packit Service a04d08
    # named by the pid file to daemonize (have pid 1 as its parent). If we
Packit Service a04d08
    # try to read the lease file before daemonization happens, we might try
Packit Service a04d08
    # to read it before the dhclient has actually written it. We also have
Packit Service a04d08
    # to wait until the dhclient has become a daemon so we can be sure to
Packit Service a04d08
    # kill the correct process, thus freeing cleandir to be deleted back
Packit Service a04d08
    # up the callstack.
Packit Service a04d08
    missing = util.wait_for_files(
Packit Service a04d08
        [pid_file, lease_file], maxwait=5, naplen=0.01)
Packit Service a04d08
    if missing:
Packit Service a04d08
        LOG.warning("dhclient did not produce expected files: %s",
Packit Service a04d08
                    ', '.join(os.path.basename(f) for f in missing))
Packit Service a04d08
        return []
Packit Service a04d08
Packit Service a04d08
    ppid = 'unknown'
Packit Service 9bfd13
    daemonized = False
Packit Service a04d08
    for _ in range(0, 1000):
Packit Service a04d08
        pid_content = util.load_file(pid_file).strip()
Packit Service a04d08
        try:
Packit Service a04d08
            pid = int(pid_content)
Packit Service a04d08
        except ValueError:
Packit Service a04d08
            pass
Packit Service a04d08
        else:
Packit Service a04d08
            ppid = util.get_proc_ppid(pid)
Packit Service a04d08
            if ppid == 1:
Packit Service a04d08
                LOG.debug('killing dhclient with pid=%s', pid)
Packit Service a04d08
                os.kill(pid, signal.SIGKILL)
Packit Service 9bfd13
                daemonized = True
Packit Service 9bfd13
                break
Packit Service a04d08
        time.sleep(0.01)
Packit Service a04d08
Packit Service 9bfd13
    if not daemonized:
Packit Service 9bfd13
        LOG.error(
Packit Service 9bfd13
            'dhclient(pid=%s, parentpid=%s) failed to daemonize after %s '
Packit Service 9bfd13
            'seconds', pid_content, ppid, 0.01 * 1000
Packit Service 9bfd13
        )
Packit Service 9bfd13
    if dhcp_log_func is not None:
Packit Service 9bfd13
        dhcp_log_func(out, err)
Packit Service a04d08
    return parse_dhcp_lease_file(lease_file)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def networkd_parse_lease(content):
Packit Service a04d08
    """Parse a systemd lease file content as in /run/systemd/netif/leases/
Packit Service a04d08
Packit Service a04d08
    Parse this (almost) ini style file even though it says:
Packit Service a04d08
      # This is private data. Do not parse.
Packit Service a04d08
Packit Service a04d08
    Simply return a dictionary of key/values."""
Packit Service a04d08
Packit Service a04d08
    return dict(configobj.ConfigObj(StringIO(content), list_values=False))
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def networkd_load_leases(leases_d=None):
Packit Service a04d08
    """Return a dictionary of dictionaries representing each lease
Packit Service a04d08
    found in lease_d.i
Packit Service a04d08
Packit Service a04d08
    The top level key will be the filename, which is typically the ifindex."""
Packit Service a04d08
Packit Service a04d08
    if leases_d is None:
Packit Service a04d08
        leases_d = NETWORKD_LEASES_DIR
Packit Service a04d08
Packit Service a04d08
    ret = {}
Packit Service a04d08
    if not os.path.isdir(leases_d):
Packit Service a04d08
        return ret
Packit Service a04d08
    for lfile in os.listdir(leases_d):
Packit Service a04d08
        ret[lfile] = networkd_parse_lease(
Packit Service a04d08
            util.load_file(os.path.join(leases_d, lfile)))
Packit Service a04d08
    return ret
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def networkd_get_option_from_leases(keyname, leases_d=None):
Packit Service a04d08
    if leases_d is None:
Packit Service a04d08
        leases_d = NETWORKD_LEASES_DIR
Packit Service a04d08
    leases = networkd_load_leases(leases_d=leases_d)
Packit Service a04d08
    for _ifindex, data in sorted(leases.items()):
Packit Service a04d08
        if data.get(keyname):
Packit Service a04d08
            return data[keyname]
Packit Service a04d08
    return None
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def parse_static_routes(rfc3442):
Packit Service a04d08
    """ parse rfc3442 format and return a list containing tuple of strings.
Packit Service a04d08
Packit Service a04d08
    The tuple is composed of the network_address (including net length) and
Packit Service a04d08
    gateway for a parsed static route.  It can parse two formats of rfc3442,
Packit Service a04d08
    one from dhcpcd and one from dhclient (isc).
Packit Service a04d08
Packit Service a04d08
    @param rfc3442: string in rfc3442 format (isc or dhcpd)
Packit Service a04d08
    @returns: list of tuple(str, str) for all valid parsed routes until the
Packit Service a04d08
              first parsing error.
Packit Service a04d08
Packit Service a04d08
    E.g.
Packit Service a04d08
    sr=parse_static_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1")
Packit Service a04d08
    sr=[
Packit Service a04d08
        ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")
Packit Service a04d08
    ]
Packit Service a04d08
Packit Service a04d08
    sr2 = parse_static_routes("24.191.168.128 192.168.128.1,0 192.168.128.1")
Packit Service a04d08
    sr2 = [
Packit Service a04d08
        ("191.168.128.0/24", "192.168.128.1"), ("0.0.0.0/0", "192.168.128.1")
Packit Service a04d08
    ]
Packit Service a04d08
Packit Service a04d08
    Python version of isc-dhclient's hooks:
Packit Service a04d08
       /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
Packit Service a04d08
    """
Packit Service a04d08
    # raw strings from dhcp lease may end in semi-colon
Packit Service a04d08
    rfc3442 = rfc3442.rstrip(";")
Packit Service a04d08
    tokens = [tok for tok in re.split(r"[, .]", rfc3442) if tok]
Packit Service a04d08
    static_routes = []
Packit Service a04d08
Packit Service a04d08
    def _trunc_error(cidr, required, remain):
Packit Service a04d08
        msg = ("RFC3442 string malformed.  Current route has CIDR of %s "
Packit Service a04d08
               "and requires %s significant octets, but only %s remain. "
Packit Service a04d08
               "Verify DHCP rfc3442-classless-static-routes value: %s"
Packit Service a04d08
               % (cidr, required, remain, rfc3442))
Packit Service a04d08
        LOG.error(msg)
Packit Service a04d08
Packit Service a04d08
    current_idx = 0
Packit Service a04d08
    for idx, tok in enumerate(tokens):
Packit Service a04d08
        if idx < current_idx:
Packit Service a04d08
            continue
Packit Service a04d08
        net_length = int(tok)
Packit Service a04d08
        if net_length in range(25, 33):
Packit Service a04d08
            req_toks = 9
Packit Service a04d08
            if len(tokens[idx:]) < req_toks:
Packit Service a04d08
                _trunc_error(net_length, req_toks, len(tokens[idx:]))
Packit Service a04d08
                return static_routes
Packit Service a04d08
            net_address = ".".join(tokens[idx+1:idx+5])
Packit Service a04d08
            gateway = ".".join(tokens[idx+5:idx+req_toks])
Packit Service a04d08
            current_idx = idx + req_toks
Packit Service a04d08
        elif net_length in range(17, 25):
Packit Service a04d08
            req_toks = 8
Packit Service a04d08
            if len(tokens[idx:]) < req_toks:
Packit Service a04d08
                _trunc_error(net_length, req_toks, len(tokens[idx:]))
Packit Service a04d08
                return static_routes
Packit Service a04d08
            net_address = ".".join(tokens[idx+1:idx+4] + ["0"])
Packit Service a04d08
            gateway = ".".join(tokens[idx+4:idx+req_toks])
Packit Service a04d08
            current_idx = idx + req_toks
Packit Service a04d08
        elif net_length in range(9, 17):
Packit Service a04d08
            req_toks = 7
Packit Service a04d08
            if len(tokens[idx:]) < req_toks:
Packit Service a04d08
                _trunc_error(net_length, req_toks, len(tokens[idx:]))
Packit Service a04d08
                return static_routes
Packit Service a04d08
            net_address = ".".join(tokens[idx+1:idx+3] + ["0", "0"])
Packit Service a04d08
            gateway = ".".join(tokens[idx+3:idx+req_toks])
Packit Service a04d08
            current_idx = idx + req_toks
Packit Service a04d08
        elif net_length in range(1, 9):
Packit Service a04d08
            req_toks = 6
Packit Service a04d08
            if len(tokens[idx:]) < req_toks:
Packit Service a04d08
                _trunc_error(net_length, req_toks, len(tokens[idx:]))
Packit Service a04d08
                return static_routes
Packit Service a04d08
            net_address = ".".join(tokens[idx+1:idx+2] + ["0", "0", "0"])
Packit Service a04d08
            gateway = ".".join(tokens[idx+2:idx+req_toks])
Packit Service a04d08
            current_idx = idx + req_toks
Packit Service a04d08
        elif net_length == 0:
Packit Service a04d08
            req_toks = 5
Packit Service a04d08
            if len(tokens[idx:]) < req_toks:
Packit Service a04d08
                _trunc_error(net_length, req_toks, len(tokens[idx:]))
Packit Service a04d08
                return static_routes
Packit Service a04d08
            net_address = "0.0.0.0"
Packit Service a04d08
            gateway = ".".join(tokens[idx+1:idx+req_toks])
Packit Service a04d08
            current_idx = idx + req_toks
Packit Service a04d08
        else:
Packit Service a04d08
            LOG.error('Parsed invalid net length "%s".  Verify DHCP '
Packit Service a04d08
                      'rfc3442-classless-static-routes value.', net_length)
Packit Service a04d08
            return static_routes
Packit Service a04d08
Packit Service a04d08
        static_routes.append(("%s/%s" % (net_address, net_length), gateway))
Packit Service a04d08
Packit Service a04d08
    return static_routes
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab