diff options
| author | Christian Breunig <christian@breunig.cc> | 2023-12-09 21:35:50 +0100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-09 21:35:50 +0100 | 
| commit | 2778f53cc1f9f03a6c145e45082ca95ba21a1a96 (patch) | |
| tree | f4d1b4e9d811447080223198b0a44cdce1d7ea75 /src | |
| parent | bf096599e4bad8a595257654ec5a0a1c4ae2e15a (diff) | |
| parent | 2787e7915c1225f05f1e07c62f7c4d1ac9dca5ac (diff) | |
| download | vyos-1x-2778f53cc1f9f03a6c145e45082ca95ba21a1a96.tar.gz vyos-1x-2778f53cc1f9f03a6c145e45082ca95ba21a1a96.zip | |
Merge pull request #1960 from sarthurdev/kea
dhcp: T3316: Migrate dhcp/dhcpv6 server to Kea
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/dhcp_server.py | 134 | ||||
| -rwxr-xr-x | src/conf_mode/dhcpv6_server.py | 42 | ||||
| -rwxr-xr-x | src/conf_mode/system-login.py | 2 | ||||
| -rw-r--r-- | src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf | 9 | ||||
| -rw-r--r-- | src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf | 7 | ||||
| -rw-r--r-- | src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf | 7 | ||||
| -rwxr-xr-x | src/migration-scripts/dhcp-server/6-to-7 | 87 | ||||
| -rwxr-xr-x | src/migration-scripts/dhcpv6-server/1-to-2 | 86 | ||||
| -rwxr-xr-x | src/op_mode/clear_dhcp_lease.py | 41 | ||||
| -rwxr-xr-x | src/op_mode/dhcp.py | 120 | ||||
| -rwxr-xr-x | src/system/on-dhcp-event.sh | 14 | ||||
| -rw-r--r-- | src/systemd/isc-dhcp-server6.service | 24 | 
12 files changed, 416 insertions, 157 deletions
| diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index ac7d95632..66f7c8057 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -21,10 +21,16 @@ from ipaddress import ip_network  from netaddr import IPAddress  from netaddr import IPRange  from sys import exit +from time import sleep  from vyos.config import Config +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key  from vyos.template import render  from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file  from vyos.utils.process import call  from vyos.utils.process import run  from vyos.utils.network import is_subnet_connected @@ -33,8 +39,14 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() -config_file = '/run/dhcp-server/dhcpd.conf' -systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf' +ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' +ctrl_socket = '/run/kea/dhcp4-ctrl-socket' +config_file = '/run/kea/kea-dhcp4.conf' +lease_file = '/config/dhcp4.leases' + +ca_cert_file = '/run/kea/kea-failover-ca.pem' +cert_file = '/run/kea/kea-failover.pem' +cert_key_file = '/run/kea/kea-failover-key.pem'  def dhcp_slice_range(exclude_list, range_dict):      """ @@ -130,6 +142,9 @@ def get_config(config=None):                          dhcp['shared_network_name'][network]['subnet'][subnet].update(                                  {'range' : new_range_dict}) +    if dict_search('failover.certificate', dhcp): +        dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)  +      return dhcp  def verify(dhcp): @@ -166,13 +181,6 @@ def verify(dhcp):                      if 'next_hop' not in route_option:                          raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') -            # DHCP failover needs at least one subnet that uses it -            if 'enable_failover' in subnet_config: -                if 'failover' not in dhcp: -                    raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \ -                                      'Failover is not configured globally!') -                failover_ok = True -              # Check if DHCP address range is inside configured subnet declaration              if 'range' in subnet_config:                  networks = [] @@ -249,14 +257,34 @@ def verify(dhcp):          raise ConfigError(f'At least one shared network must be active!')      if 'failover' in dhcp: -        if not failover_ok: -            raise ConfigError('DHCP failover must be enabled for at least one subnet!') -          for key in ['name', 'remote', 'source_address', 'status']:              if key not in dhcp['failover']:                  tmp = key.replace('_', '-')                  raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') +        if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1: +            raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate') + +        if 'certificate' in dhcp['failover']: +            cert_name = dhcp['failover']['certificate'] + +            if cert_name not in dhcp['pki']['certificate']: +                raise ConfigError(f'Invalid certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'): +                raise ConfigError(f'Invalid certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'): +                raise ConfigError(f'Missing private key on certificate specified for DHCP failover') + +        if 'ca_certificate' in dhcp['failover']: +            ca_cert_name = dhcp['failover']['ca_certificate'] +            if ca_cert_name not in dhcp['pki']['ca']: +                raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + +            if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'): +                raise ConfigError(f'Invalid CA certificate specified for DHCP failover') +      for address in (dict_search('listen_address', dhcp) or []):          if is_addr_assigned(address):              listen_ok = True @@ -278,43 +306,71 @@ def generate(dhcp):      if not dhcp or 'disable' in dhcp:          return None -    # Please see: https://vyos.dev/T1129 for quoting of the raw -    # parameters we can pass to ISC DHCPd -    tmp_file = '/tmp/dhcpd.conf' -    render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, -           formater=lambda _: _.replace(""", '"')) -    # XXX: as we have the ability for a user to pass in "raw" options via VyOS -    # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered -    # configuration -    tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}') -    if tmp > 0: -        if os.path.exists(tmp_file): -            os.unlink(tmp_file) -        raise ConfigError('Configuration file errors encountered - check your options!') - -    # Now that we know that the newly rendered configuration is "good" we can -    # render the "real" configuration -    render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, -           formater=lambda _: _.replace(""", '"')) -    render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) - -    # Clean up configuration test file -    if os.path.exists(tmp_file): -        os.unlink(tmp_file) +    dhcp['lease_file'] = lease_file +    dhcp['machine'] = os.uname().machine + +    if not os.path.exists(lease_file): +        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + +    for f in [cert_file, cert_key_file, ca_cert_file]: +        if os.path.exists(f): +            os.unlink(f) + +    if 'failover' in dhcp: +        if 'certificate' in dhcp['failover']: +            cert_name = dhcp['failover']['certificate'] +            cert_data = dhcp['pki']['certificate'][cert_name]['certificate'] +            key_data = dhcp['pki']['certificate'][cert_name]['private']['key'] +            write_file(cert_file, wrap_certificate(cert_data), user='_kea', mode=0o600) +            write_file(cert_key_file, wrap_private_key(key_data), user='_kea', mode=0o600) + +            dhcp['failover']['cert_file'] = cert_file +            dhcp['failover']['cert_key_file'] = cert_key_file + +        if 'ca_certificate' in dhcp['failover']: +            ca_cert_name = dhcp['failover']['ca_certificate'] +            ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate'] +            write_file(ca_cert_file, wrap_certificate(ca_cert_data), user='_kea', mode=0o600) + +            dhcp['failover']['ca_cert_file'] = ca_cert_file + +    render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp) +    render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp)      return None  def apply(dhcp): -    call('systemctl daemon-reload') -    # bail out early - looks like removal from running config +    services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] +      if not dhcp or 'disable' in dhcp: -        call('systemctl stop isc-dhcp-server.service') +        for service in services: +            call(f'systemctl stop {service}.service') +          if os.path.exists(config_file):              os.unlink(config_file)          return None -    call('systemctl restart isc-dhcp-server.service') +    for service in services: +        action = 'restart' + +        if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: +            action = 'stop' + +        if service == 'kea-ctrl-agent' and 'failover' not in dhcp: +            action = 'stop' + +        call(f'systemctl {action} {service}.service') + +    # op-mode needs ctrl socket permission change +    i = 0 +    while not os.path.exists(ctrl_socket): +        if i > 15: +            break +        i += 1 +        sleep(1) +    chmod_775(ctrl_socket) +      return None  if __name__ == '__main__': diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 427001609..73a708ff5 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -19,18 +19,23 @@ import os  from ipaddress import ip_address  from ipaddress import ip_network  from sys import exit +from time import sleep  from vyos.config import Config  from vyos.template import render  from vyos.template import is_ipv6  from vyos.utils.process import call +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file  from vyos.utils.dict import dict_search  from vyos.utils.network import is_subnet_connected  from vyos import ConfigError  from vyos import airbag  airbag.enable() -config_file = '/run/dhcp-server/dhcpdv6.conf' +config_file = '/run/kea/kea-dhcp6.conf' +ctrl_socket = '/run/kea/dhcp6-ctrl-socket' +lease_file = '/config/dhcp6.leases'  def get_config(config=None):      if config: @@ -110,17 +115,20 @@ def verify(dhcpv6):              # Prefix delegation sanity checks              if 'prefix_delegation' in subnet_config: -                if 'start' not in subnet_config['prefix_delegation']: -                    raise ConfigError('prefix-delegation start address not defined!') +                if 'prefix' not in subnet_config['prefix_delegation']: +                    raise ConfigError('prefix-delegation prefix not defined!') -                for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items(): -                    if 'stop' not in prefix_config: -                        raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\ +                for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items(): +                    if 'delegated_length' not in prefix_config: +                        raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\                                            f'must be configured')                      if 'prefix_length' not in prefix_config:                          raise ConfigError('Length of delegated IPv6 prefix must be configured') +                    if prefix_config['prefix_length'] > prefix_config['delegated_length']: +                        raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix') +              # Static mappings don't require anything (but check if IP is in subnet if it's set)              if 'static_mapping' in subnet_config:                  for mapping, mapping_config in subnet_config['static_mapping'].items(): @@ -168,12 +176,18 @@ def generate(dhcpv6):      if not dhcpv6 or 'disable' in dhcpv6:          return None -    render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6) +    dhcpv6['lease_file'] = lease_file +    dhcpv6['machine'] = os.uname().machine + +    if not os.path.exists(lease_file): +        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + +    render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6)      return None  def apply(dhcpv6):      # bail out early - looks like removal from running config -    service_name = 'isc-dhcp-server6.service' +    service_name = 'kea-dhcp6-server.service'      if not dhcpv6 or 'disable' in dhcpv6:          # DHCP server is removed in the commit          call(f'systemctl stop {service_name}') @@ -182,6 +196,16 @@ def apply(dhcpv6):          return None      call(f'systemctl restart {service_name}') + +    # op-mode needs ctrl socket permission change +    i = 0 +    while not os.path.exists(ctrl_socket): +        if i > 15: +            break +        i += 1 +        sleep(1) +    chmod_775(ctrl_socket) +      return None  if __name__ == '__main__': diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index cd85a5066..aeac82462 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -331,7 +331,7 @@ def apply(login):              if tmp: command += f" --home '{tmp}'"              else: command += f" --home '/home/{user}'" -            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}' +            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}'              try:                  cmd(command) diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf new file mode 100644 index 000000000..0f5bf801e --- /dev/null +++ b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf new file mode 100644 index 000000000..682e5bbce --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf diff --git a/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf new file mode 100644 index 000000000..cb33fc057 --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp6 -c /run/kea/kea-dhcp6.conf diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7 new file mode 100755 index 000000000..ccf385a30 --- /dev/null +++ b/src/migration-scripts/dhcp-server/6-to-7 @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T3316: Migrate to Kea +#        - global-parameters will not function +#        - shared-network-parameters will not function +#        - subnet-parameters will not function +#        - static-mapping-parameters will not function +#        - host-decl-name is on by default, option removed +#        - ping-check no longer supported +#        - failover is default enabled on all subnets that exist on failover servers + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['service', 'dhcp-server'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    sys.exit(0) + +if config.exists(base + ['host-decl-name']): +    config.delete(base + ['host-decl-name']) + +if config.exists(base + ['global-parameters']): +    config.delete(base + ['global-parameters']) + +if config.exists(base + ['shared-network-name']): +    for network in config.list_nodes(base + ['shared-network-name']): +        base_network = base + ['shared-network-name', network] + +        if config.exists(base_network + ['ping-check']): +            config.delete(base_network + ['ping-check']) + +        if config.exists(base_network + ['shared-network-parameters']): +            config.delete(base_network +['shared-network-parameters']) + +        if not config.exists(base_network + ['subnet']): +            continue + +        # Run this for every specified 'subnet' +        for subnet in config.list_nodes(base_network + ['subnet']): +            base_subnet = base_network + ['subnet', subnet] + +            if config.exists(base_subnet + ['enable-failover']): +                config.delete(base_subnet + ['enable-failover']) + +            if config.exists(base_subnet + ['ping-check']): +                config.delete(base_subnet + ['ping-check']) + +            if config.exists(base_subnet + ['subnet-parameters']): +                config.delete(base_subnet + ['subnet-parameters']) + +            if config.exists(base_subnet + ['static-mapping']): +                for mapping in config.list_nodes(base_subnet + ['static-mapping']): +                    if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']): +                        config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/dhcpv6-server/1-to-2 b/src/migration-scripts/dhcpv6-server/1-to-2 new file mode 100755 index 000000000..cc5a8900a --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/1-to-2 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T3316: Migrate to Kea +# - Kea was meant to have support for key "prefix-highest" under PD which would allow an address range +#   However this seems to have never been implemented. A conversion to prefix length is needed (where possible). +#   Ref: https://lists.isc.org/pipermail/kea-users/2022-November/003686.html +# - Remove prefix temporary value, convert to multi leafNode (https://kea.readthedocs.io/en/kea-2.2.0/arm/dhcp6-srv.html#dhcpv6-server-limitations) + +import sys +from vyos.configtree import ConfigTree +from vyos.utils.network import ipv6_prefix_length + +if (len(sys.argv) < 1): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +for network in config.list_nodes(base): +    if not config.exists(base + [network, 'subnet']): +        continue + +    for subnet in config.list_nodes(base + [network, 'subnet']): +        # Delete temporary value under address-range prefix, convert tagNode to leafNode multi +        if config.exists(base + [network, 'subnet', subnet, 'address-range', 'prefix']): +            prefix_base = base + [network, 'subnet', subnet, 'address-range', 'prefix'] +            prefixes = config.list_nodes(prefix_base) +             +            config.delete(prefix_base) + +            for prefix in prefixes: +                config.set(prefix_base, value=prefix, replace=False) + +        if config.exists(base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix']): +            prefix_base = base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix'] + +            config.set(prefix_base) +            config.set_tag(prefix_base) + +            for start in config.list_nodes(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']): +                path = base + [network, 'subnet', subnet, 'prefix-delegation', 'start', start] + +                delegated_length = config.return_value(path + ['prefix-length']) +                stop = config.return_value(path + ['stop']) + +                prefix_length = ipv6_prefix_length(start, stop) + +                # This range could not be converted into a simple prefix length and must be skipped +                if not prefix_length: +                    continue + +                config.set(prefix_base + [start, 'delegated-length'], value=delegated_length) +                config.set(prefix_base + [start, 'prefix-length'], value=prefix_length) + +            config.delete(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py index f372d3af0..2c95a2b08 100755 --- a/src/op_mode/clear_dhcp_lease.py +++ b/src/op_mode/clear_dhcp_lease.py @@ -1,20 +1,34 @@  #!/usr/bin/env python3 +# +# Copyright 2023 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 argparse  import re -from isc_dhcp_leases import Lease -from isc_dhcp_leases import IscDhcpLeases -  from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_parse_leases  from vyos.utils.io import ask_yes_no  from vyos.utils.process import call  from vyos.utils.commit import commit_in_progress +# TODO: Update to use Kea control socket command "lease4-del"  config = ConfigTreeQuery()  base = ['service', 'dhcp-server'] -lease_file = '/config/dhcpd.leases' +lease_file = '/config/dhcp4.leases'  def del_lease_ip(address): @@ -25,8 +39,7 @@ def del_lease_ip(address):      """      with open(lease_file, encoding='utf-8') as f:          data = f.read().rstrip() -        lease_config_ip = '{(?P<config>[\s\S]+?)\n}' -        pattern = rf"lease {address} {lease_config_ip}" +        pattern = rf"^{address},[^\n]+\n"          # Delete lease for ip block          data = re.sub(pattern, '', data) @@ -38,15 +51,13 @@ def is_ip_in_leases(address):      """      Return True if address found in the lease file      """ -    leases = IscDhcpLeases(lease_file) +    leases = kea_parse_leases(lease_file)      lease_ips = [] -    for lease in leases.get(): -        lease_ips.append(lease.ip) -    if address not in lease_ips: -        print(f'Address "{address}" not found in "{lease_file}"') -        return False -    return True - +    for lease in leases: +        if address == lease['address']: +            return True +    print(f'Address "{address}" not found in "{lease_file}"') +    return False  if not config.exists(base):      print('DHCP-server not configured!') @@ -75,4 +86,4 @@ if __name__ == '__main__':          exit(1)      else:          del_lease_ip(address) -        call('systemctl restart isc-dhcp-server.service') +        call('systemctl restart kea-dhcp4-server.service') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d6b8aa0b8..bd2c522ca 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -21,7 +21,6 @@ import typing  from datetime import datetime  from glob import glob  from ipaddress import ip_address -from isc_dhcp_leases import IscDhcpLeases  from tabulate import tabulate  import vyos.opmode @@ -29,6 +28,9 @@ import vyos.opmode  from vyos.base import Warning  from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_get_active_config +from vyos.kea import kea_get_pool_from_subnet_id +from vyos.kea import kea_parse_leases  from vyos.utils.dict import dict_search  from vyos.utils.file import read_file  from vyos.utils.process import cmd @@ -77,67 +79,62 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig      Get DHCP server leases      :return list      """ -    lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' +    lease_file = '/config/dhcp6.leases' if family == 'inet6' else '/config/dhcp4.leases'      data = [] -    leases = IscDhcpLeases(lease_file).get() +    leases = kea_parse_leases(lease_file)      if pool is None:          pool = _get_dhcp_pools(family=family) -        aux = False      else:          pool = [pool] -        aux = True - -    ## Search leases for every pool -    for pool_name in pool: -        for lease in leases: -            if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '': -            #if lease.sets.get('shared-networkname', '') == pool_name: -                data_lease = {} -                data_lease['ip'] = lease.ip -                data_lease['state'] = lease.binding_state -                #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server' -                data_lease['pool'] = lease.sets.get('shared-networkname', '') -                data_lease['end'] = lease.end.timestamp() if lease.end else None -                data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote' - -                if family == 'inet': -                    data_lease['mac'] = lease.ethernet -                    data_lease['start'] = lease.start.timestamp() -                    data_lease['hostname'] = lease.hostname - -                if family == 'inet6': -                    data_lease['last_communication'] = lease.last_communication.timestamp() -                    data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) -                    lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} -                    data_lease['type'] = lease_types_long[lease.type] - -                data_lease['remaining'] = '-' - -                if lease.end: -                    data_lease['remaining'] = lease.end - datetime.utcnow() - -                    if data_lease['remaining'].days >= 0: -                        # substraction gives us a timedelta object which can't be formatted with strftime -                        # so we use str(), split gets rid of the microseconds -                        data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] - -                # Do not add old leases -                if data_lease['remaining'] != '' and data_lease['state'] != 'free': -                    if not state or data_lease['state'] in state or state == 'all': -                        if not origin or data_lease['origin'] in origin: -                            if not aux or (aux and data_lease['pool'] == pool_name): -                                data.append(data_lease) - -                # deduplicate -                checked = [] -                for entry in data: -                    addr = entry.get('ip') -                    if addr not in checked: -                        checked.append(addr) -                    else: -                        idx = _find_list_of_dict_index(data, key='ip', value=addr) -                        data.pop(idx) + +    inet_suffix = '6' if family == 'inet6' else '4' +    active_config = kea_get_active_config(inet_suffix) + +    for lease in leases: +        data_lease = {} +        data_lease['ip'] = lease['address'] +        lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'} +        data_lease['state'] = lease_state_long[lease['state']] +        data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-' +        data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None +        data_lease['origin'] = 'local' # TODO: Determine remote in HA + +        if family == 'inet': +            data_lease['mac'] = lease['hwaddr'] +            data_lease['start'] = lease['start_timestamp'] +            data_lease['hostname'] = lease['hostname'] + +        if family == 'inet6': +            data_lease['last_communication'] = lease['start_timestamp'] +            data_lease['iaid_duid'] = _format_hex_string(lease['duid']) +            lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'} +            data_lease['type'] = lease_types_long[lease['lease_type']] + +        data_lease['remaining'] = '-' + +        if lease['expire']: +            data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() + +            if data_lease['remaining'].days >= 0: +                # substraction gives us a timedelta object which can't be formatted with strftime +                # so we use str(), split gets rid of the microseconds +                data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + +        # Do not add old leases +        if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': +            if not state or data_lease['state'] in state: +                data.append(data_lease) + +        # deduplicate +        checked = [] +        for entry in data: +            addr = entry.get('ip') +            if addr not in checked: +                checked.append(addr) +            else: +                idx = _find_list_of_dict_index(data, key='ip', value=addr) +                data.pop(idx)      if sorted:          if sorted == 'ip': @@ -154,7 +151,7 @@ def _get_formatted_server_leases(raw_data, family='inet'):              ipaddr = lease.get('ip')              hw_addr = lease.get('mac')              state = lease.get('state') -            start = lease.get('start') +            start = lease.get('start').timestamp()              start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')              end = lease.get('end')              end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-' @@ -171,7 +168,7 @@ def _get_formatted_server_leases(raw_data, family='inet'):          for lease in raw_data:              ipaddr = lease.get('ip')              state = lease.get('state') -            start = lease.get('last_communication') +            start = lease.get('last_communication').timestamp()              start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')              end = lease.get('end')              end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') @@ -282,10 +279,9 @@ def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str],                         sorted: typing.Optional[str], state: typing.Optional[ArgState],                         origin: typing.Optional[ArgOrigin] ):      # if dhcp server is down, inactive leases may still be shown as active, so warn the user. -    v = '6' if family == 'inet6' else '' -    service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' -    if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): -        Warning(f'{service_name} server is configured but not started. Data may be stale.') +    v = '6' if family == 'inet6' else '4' +    if not is_systemd_service_running(f'kea-dhcp{v}-server.service'): +        Warning('DHCP server is configured but not started. Data may be stale.')      v = 'v6' if family == 'inet6' else ''      if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 49e53d7e1..7b25bf338 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -15,20 +15,20 @@ if [ $# -lt 5 ]; then  fi  action=$1 -client_name=$2 -client_ip=$3 -client_mac=$4 -domain=$5 +client_name=$LEASE4_HOSTNAME +client_ip=$LEASE4_ADDRESS +client_mac=$LEASE4_HWADDR +domain=$(echo "$client_name" | cut -d"." -f2-)  hostsd_client="/usr/bin/vyos-hostsd-client"  case "$action" in -  commit) # add mapping for new lease +  leases4_renew|lease4_recover) # add mapping for new lease      if [ -z "$client_name" ]; then          logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"          client_name=$(echo "client-"$client_mac | tr : -)      fi -    if [ "$domain" == "..YYZ!" ]; then +    if [ -z "$domain" ]; then          client_fqdn_name=$client_name          client_search_expr=$client_name      else @@ -39,7 +39,7 @@ case "$action" in      exit 0      ;; -  release) # delete mapping for released address +  lease4_release|lease4_expire) # delete mapping for released address)      $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply      exit 0      ;; diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service deleted file mode 100644 index 1345c5fc5..000000000 --- a/src/systemd/isc-dhcp-server6.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=ISC DHCP IPv6 server -Documentation=man:dhcpd(8) -RequiresMountsFor=/run -ConditionPathExists=/run/dhcp-server/dhcpdv6.conf -After=vyos-router.service - -[Service] -Type=forking -WorkingDirectory=/run/dhcp-server -RuntimeDirectory=dhcp-server -RuntimeDirectoryPreserve=yes -Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases -PIDFile=/run/dhcp-server/dhcpdv6.pid -ExecStartPre=/bin/sh -ec '\ -touch ${LEASE_FILE}; \ -chown nobody:nogroup ${LEASE_FILE}* ; \ -chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} -Restart=always - -[Install] -WantedBy=multi-user.target | 
