diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 951c83693..baac75eda 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -1,547 +1,542 @@
 # Copyright 2023-2025 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 json
 import os
 import socket
 
 from datetime import datetime
 from datetime import timezone
 
 from vyos.template import is_ipv6
 from vyos.template import isc_static_route
 from vyos.template import netmask_from_cidr
 from vyos.utils.dict import dict_search_args
 from vyos.utils.file import file_permissions
 from vyos.utils.process import run
 
 kea4_options = {
     'name_server': 'domain-name-servers',
     'domain_name': 'domain-name',
     'domain_search': 'domain-search',
     'ntp_server': 'ntp-servers',
     'pop_server': 'pop-server',
     'smtp_server': 'smtp-server',
     'time_server': 'time-servers',
     'wins_server': 'netbios-name-servers',
     'default_router': 'routers',
     'server_identifier': 'dhcp-server-identifier',
     'tftp_server_name': 'tftp-server-name',
     'bootfile_size': 'boot-size',
     'time_offset': 'time-offset',
     'wpad_url': 'wpad-url',
     'ipv6_only_preferred': 'v6-only-preferred',
     'captive_portal': 'v4-captive-portal',
 }
 
 kea6_options = {
     'info_refresh_time': 'information-refresh-time',
     'name_server': 'dns-servers',
     'domain_search': 'domain-search',
     'nis_domain': 'nis-domain-name',
     'nis_server': 'nis-servers',
     'nisplus_domain': 'nisp-domain-name',
     'nisplus_server': 'nisp-servers',
     'sntp_server': 'sntp-servers',
     'captive_portal': 'v6-captive-portal',
 }
 
 kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
 
 
 def _format_hex_string(in_str):
     out_str = ''
     # if input is divisible by 2, add : every 2 chars
     if len(in_str) > 0 and len(in_str) % 2 == 0:
         out_str = ':'.join(a + b for a, b in zip(in_str[::2], in_str[1::2]))
     else:
         out_str = in_str
 
     return out_str
 
 
 def _find_list_of_dict_index(lst, key='ip', value=''):
     """
     Find the index entry of list of dict matching the dict value
     Exampe:
         % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
         % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
         % 1
     """
     idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
     return idx
 
 
 def kea_parse_options(config):
     options = []
 
     for node, option_name in kea4_options.items():
         if node not in config:
             continue
 
         value = (
             ', '.join(config[node]) if isinstance(config[node], list) else config[node]
         )
         options.append({'name': option_name, 'data': value})
 
     if 'client_prefix_length' in config:
         options.append(
             {
                 'name': 'subnet-mask',
                 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length']),
             }
         )
 
     if 'ip_forwarding' in config:
         options.append({'name': 'ip-forwarding', 'data': 'true'})
 
     if 'static_route' in config:
         default_route = ''
 
         if 'default_router' in config:
             default_route = isc_static_route('0.0.0.0/0', config['default_router'])
 
         routes = [
             isc_static_route(route, route_options['next_hop'])
             for route, route_options in config['static_route'].items()
         ]
 
         options.append(
             {
                 'name': 'rfc3442-static-route',
                 'data': ', '.join(
                     routes if not default_route else routes + [default_route]
                 ),
             }
         )
         options.append({'name': 'windows-static-route', 'data': ', '.join(routes)})
 
     if 'time_zone' in config:
         with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f:
             tz_string = f.read().split(b'\n')[-2].decode('utf-8')
 
         options.append({'name': 'pcode', 'data': tz_string})
         options.append({'name': 'tcode', 'data': config['time_zone']})
 
     unifi_controller = dict_search_args(
         config, 'vendor_option', 'ubiquiti', 'unifi_controller'
     )
     if unifi_controller:
         options.append(
             {'name': 'unifi-controller', 'data': unifi_controller, 'space': 'ubnt'}
         )
 
     return options
 
 
 def kea_parse_subnet(subnet, config):
     out = {'subnet': subnet, 'id': int(config['subnet_id'])}
 
     if 'option' in config:
         out['option-data'] = kea_parse_options(config['option'])
 
         if 'bootfile_name' in config['option']:
             out['boot-file-name'] = config['option']['bootfile_name']
 
         if 'bootfile_server' in config['option']:
             out['next-server'] = config['option']['bootfile_server']
 
     if 'ignore_client_id' in config:
         out['match-client-id'] = False
 
     if 'lease' in config:
         out['valid-lifetime'] = int(config['lease'])
         out['max-valid-lifetime'] = int(config['lease'])
 
     if 'range' in config:
         pools = []
         for num, range_config in config['range'].items():
             start, stop = range_config['start'], range_config['stop']
             pool = {'pool': f'{start} - {stop}'}
 
             if 'option' in range_config:
                 pool['option-data'] = kea_parse_options(range_config['option'])
 
                 if 'bootfile_name' in range_config['option']:
                     pool['boot-file-name'] = range_config['option']['bootfile_name']
 
                 if 'bootfile_server' in range_config['option']:
                     pool['next-server'] = range_config['option']['bootfile_server']
 
             pools.append(pool)
         out['pools'] = pools
 
     if 'static_mapping' in config:
         reservations = []
         for host, host_config in config['static_mapping'].items():
             if 'disable' in host_config:
                 continue
 
             reservation = {
                 'hostname': host,
             }
 
             if 'mac' in host_config:
                 reservation['hw-address'] = host_config['mac']
 
             if 'duid' in host_config:
                 reservation['duid'] = host_config['duid']
 
             if 'ip_address' in host_config:
                 reservation['ip-address'] = host_config['ip_address']
 
             if 'option' in host_config:
                 reservation['option-data'] = kea_parse_options(host_config['option'])
 
                 if 'bootfile_name' in host_config['option']:
                     reservation['boot-file-name'] = host_config['option'][
                         'bootfile_name'
                     ]
 
                 if 'bootfile_server' in host_config['option']:
                     reservation['next-server'] = host_config['option'][
                         'bootfile_server'
                     ]
 
             reservations.append(reservation)
         out['reservations'] = reservations
 
     return out
 
 
 def kea6_parse_options(config):
     options = []
 
     for node, option_name in kea6_options.items():
         if node not in config:
             continue
 
         value = (
             ', '.join(config[node]) if isinstance(config[node], list) else config[node]
         )
         options.append({'name': option_name, 'data': value})
 
     if 'sip_server' in config:
         sip_servers = config['sip_server']
 
         addrs = []
         hosts = []
 
         for server in sip_servers:
             if is_ipv6(server):
                 addrs.append(server)
             else:
                 hosts.append(server)
 
         if addrs:
             options.append({'name': 'sip-server-addr', 'data': ', '.join(addrs)})
 
         if hosts:
             options.append({'name': 'sip-server-dns', 'data': ', '.join(hosts)})
 
     cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server')
     if cisco_tftp:
         options.append(
             {'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp}
         )
 
     return options
 
 
 def kea6_parse_subnet(subnet, config):
     out = {'subnet': subnet, 'id': int(config['subnet_id'])}
 
     if 'option' in config:
         out['option-data'] = kea6_parse_options(config['option'])
 
     if 'interface' in config:
         out['interface'] = config['interface']
 
     if 'range' in config:
         pools = []
         for num, range_config in config['range'].items():
             pool = {}
 
             if 'prefix' in range_config:
                 pool['pool'] = range_config['prefix']
 
             if 'start' in range_config:
                 start = range_config['start']
                 stop = range_config['stop']
                 pool['pool'] = f'{start} - {stop}'
 
             if 'option' in range_config:
                 pool['option-data'] = kea6_parse_options(range_config['option'])
 
             pools.append(pool)
 
         out['pools'] = pools
 
     if 'prefix_delegation' in config:
         pd_pools = []
 
         if 'prefix' in config['prefix_delegation']:
             for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
                 pd_pool = {
                     'prefix': prefix,
                     'prefix-len': int(pd_conf['prefix_length']),
                     'delegated-len': int(pd_conf['delegated_length']),
                 }
 
                 if 'excluded_prefix' in pd_conf:
                     pd_pool['excluded-prefix'] = pd_conf['excluded_prefix']
                     pd_pool['excluded-prefix-len'] = int(
                         pd_conf['excluded_prefix_length']
                     )
 
                 pd_pools.append(pd_pool)
 
         out['pd-pools'] = pd_pools
 
     if 'lease_time' in config:
         if 'default' in config['lease_time']:
             out['valid-lifetime'] = int(config['lease_time']['default'])
         if 'maximum' in config['lease_time']:
             out['max-valid-lifetime'] = int(config['lease_time']['maximum'])
         if 'minimum' in config['lease_time']:
             out['min-valid-lifetime'] = int(config['lease_time']['minimum'])
 
     if 'static_mapping' in config:
         reservations = []
         for host, host_config in config['static_mapping'].items():
             if 'disable' in host_config:
                 continue
 
             reservation = {'hostname': host}
 
             if 'mac' in host_config:
                 reservation['hw-address'] = host_config['mac']
 
             if 'duid' in host_config:
                 reservation['duid'] = host_config['duid']
 
             if 'ipv6_address' in host_config:
                 reservation['ip-addresses'] = [host_config['ipv6_address']]
 
             if 'ipv6_prefix' in host_config:
                 reservation['prefixes'] = [host_config['ipv6_prefix']]
 
             if 'option' in host_config:
                 reservation['option-data'] = kea6_parse_options(host_config['option'])
 
             reservations.append(reservation)
 
         out['reservations'] = reservations
 
     return out
 
 
 def _ctrl_socket_command(inet, command, args=None):
     path = kea_ctrl_socket.format(inet=inet)
 
     if not os.path.exists(path):
         return None
 
     if file_permissions(path) != '0775':
         run(f'sudo chmod 775 {path}')
 
     with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
         sock.connect(path)
 
         payload = {'command': command}
         if args:
             payload['arguments'] = args
 
         sock.send(bytes(json.dumps(payload), 'utf-8'))
         result = b''
         while True:
             data = sock.recv(4096)
             result += data
             if len(data) < 4096:
                 break
 
         return json.loads(result.decode('utf-8'))
 
 
 def kea_get_leases(inet):
     leases = _ctrl_socket_command(inet, f'lease{inet}-get-all')
 
     if not leases or 'result' not in leases or leases['result'] != 0:
         return []
 
     return leases['arguments']['leases']
 
 
 def kea_delete_lease(inet, ip_address):
     args = {'ip-address': ip_address}
 
     result = _ctrl_socket_command(inet, f'lease{inet}-del', args)
 
     if result and 'result' in result:
         return result['result'] == 0
 
     return False
 
 
 def kea_get_active_config(inet):
     config = _ctrl_socket_command(inet, 'config-get')
 
     if not config or 'result' not in config or config['result'] != 0:
         return None
 
     return config
 
 
 def kea_get_dhcp_pools(config, inet):
     shared_networks = dict_search_args(
         config, 'arguments', f'Dhcp{inet}', 'shared-networks'
     )
     return [network['name'] for network in shared_networks] if shared_networks else []
 
 
 def kea_get_pool_from_subnet_id(config, inet, subnet_id):
     shared_networks = dict_search_args(
         config, 'arguments', f'Dhcp{inet}', 'shared-networks'
     )
 
     if not shared_networks:
         return None
 
     for network in shared_networks:
         if f'subnet{inet}' not in network:
             continue
 
         for subnet in network[f'subnet{inet}']:
             if 'id' in subnet and int(subnet['id']) == int(subnet_id):
                 return network['name']
 
     return None
 
 
 def kea_get_static_mappings(config, inet, pools=[]) -> list:
     """
     Get DHCP static mapping from active Kea DHCPv4 or DHCPv6 configuration
     :return list
     """
     shared_networks = dict_search_args(
         config, 'arguments', f'Dhcp{inet}', 'shared-networks'
     )
 
     mappings = []
 
     if shared_networks:
         for network in shared_networks:
             if f'subnet{inet}' not in network:
                 continue
 
             for p in pools:
                 if network['name'] == p:
                     for subnet in network[f'subnet{inet}']:
                         if 'reservations' in subnet:
                             for reservation in subnet['reservations']:
                                 mapping = {'pool': p, 'subnet': subnet['subnet']}
                                 mapping.update(reservation)
                                 # rename 'ip(v6)-address' to 'ip', inet6 has 'ipv6-address' and inet has 'ip-address'
                                 mapping['ip'] = mapping.pop(
                                     'ipv6-address', mapping.pop('ip-address', None)
                                 )
                                 # rename 'hw-address' to 'mac'
                                 mapping['mac'] = mapping.pop('hw-address', None)
                                 mappings.append(mapping)
 
     return mappings
 
 
 def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list:
     """
     Get DHCP server leases from active Kea DHCPv4 or DHCPv6 configuration
     :return list
     """
     leases = kea_get_leases(inet)
 
     data = []
     for lease in leases:
         lifetime = lease['valid-lft']
-        expiry = lease['cltt'] + lifetime
+        start = lease['cltt']
+        expiry = start + lifetime
 
-        lease['start_timestamp'] = datetime.fromtimestamp(
-            expiry - lifetime, timezone.utc
-        )
-        lease['expire_timestamp'] = (
+        lease['start_time'] = datetime.fromtimestamp(start, timezone.utc)
+        lease['expire_time'] = (
             datetime.fromtimestamp(expiry, timezone.utc) if expiry else None
         )
 
         data_lease = {}
         data_lease['ip'] = lease['ip-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(config, inet, lease['subnet-id'])
             if config
             else '-'
         )
         data_lease['end'] = (
-            lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None
+            lease['expire_time'].timestamp() if lease['expire_time'] else None
         )
         data_lease['origin'] = 'local'  # TODO: Determine remote in HA
         # remove trailing dot in 'hostname' to ensure consistency for `vyos-hostsd-client`
         data_lease['hostname'] = lease.get('hostname', '-').rstrip('.')
 
         if inet == '4':
             data_lease['mac'] = lease['hw-address']
-            data_lease['start'] = lease['start_timestamp'].timestamp()
+            data_lease['start'] = lease['start_time'].timestamp()
 
         if inet == '6':
-            data_lease['last_communication'] = lease['start_timestamp'].timestamp()
+            data_lease['last_communication'] = lease['start_time'].timestamp()
             data_lease['duid'] = _format_hex_string(lease['duid'])
             data_lease['type'] = lease['type']
 
             if lease['type'] == 'IA_PD':
                 prefix_len = lease['prefix-len']
                 data_lease['ip'] += f'/{prefix_len}'
 
         data_lease['remaining'] = '-'
 
-        if lease['valid-lft'] > 0:
-            data_lease['remaining'] = lease['expire_timestamp'] - datetime.now(
-                timezone.utc
-            )
-
-            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]
+        now = datetime.now(timezone.utc)
+        if lease['valid-lft'] > 0 and lease['expire_time'] > now:
+            # 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(lease['expire_time'] - now).split('.')[0]
 
         # Do not add old leases
         if (
             data_lease['remaining']
             and data_lease['pool'] in pools
             and data_lease['state'] != 'free'
             and (not state or state == 'all' 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)
                 if idx is not None:
                     data.pop(idx)
 
     return data
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index b3d7d4dd3..8eed2c6cd 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -1,568 +1,569 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2022-2025 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/>.
 
 import os
 import sys
 import typing
 
 from datetime import datetime
+from datetime import timezone
 from glob import glob
 from ipaddress import ip_address
 from tabulate import tabulate
 
 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_dhcp_pools
 from vyos.kea import kea_get_leases
 from vyos.kea import kea_get_server_leases
 from vyos.kea import kea_get_static_mappings
 from vyos.kea import kea_delete_lease
 from vyos.utils.process import call
 from vyos.utils.process import is_systemd_service_running
 
 time_string = '%a %b %d %H:%M:%S %Z %Y'
 
 config = ConfigTreeQuery()
 lease_valid_states = [
     'all',
     'active',
     'free',
     'expired',
     'released',
     'abandoned',
     'reset',
     'backup',
 ]
 sort_valid_inet = [
     'end',
     'mac',
     'hostname',
     'ip',
     'pool',
     'remaining',
     'start',
     'state',
 ]
 sort_valid_inet6 = [
     'end',
     'duid',
     'ip',
     'last_communication',
     'pool',
     'remaining',
     'state',
     'type',
 ]
 mapping_sort_valid = ['mac', 'ip', 'pool', 'duid']
 
 stale_warn_msg = 'DHCP server is configured but not started. Data may be stale.'
 
 ArgFamily = typing.Literal['inet', 'inet6']
 ArgState = typing.Literal[
     'all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'
 ]
 ArgOrigin = typing.Literal['local', 'remote']
 
 
-def _utc_to_local(utc_dt):
-    return datetime.fromtimestamp(
-        (datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()
-    )
-
-
 def _get_raw_server_leases(
     config, family='inet', pool=None, sorted=None, state=[], origin=None
 ) -> list:
     inet_suffix = '6' if family == 'inet6' else '4'
     pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
 
     mappings = kea_get_server_leases(config, inet_suffix, pools, state, origin)
 
     if sorted:
         if sorted == 'ip':
             mappings.sort(key=lambda x: ip_address(x['ip']))
         else:
             mappings.sort(key=lambda x: x[sorted])
     return mappings
 
 
 def _get_formatted_server_leases(raw_data, family='inet'):
     data_entries = []
     if family == 'inet':
         for lease in raw_data:
             ipaddr = lease.get('ip')
             hw_addr = lease.get('mac')
             state = lease.get('state')
-            start = lease.get('start')
-            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 '-'
+            start = datetime.fromtimestamp(lease.get('start'), timezone.utc)
+            end = (
+                datetime.fromtimestamp(lease.get('end'), timezone.utc)
+                if lease.get('end')
+                else '-'
+            )
             remain = lease.get('remaining')
             pool = lease.get('pool')
             hostname = lease.get('hostname')
             origin = lease.get('origin')
             data_entries.append(
                 [ipaddr, hw_addr, state, start, end, remain, pool, hostname, origin]
             )
 
         headers = [
             'IP Address',
             'MAC address',
             'State',
             'Lease start',
             'Lease expiration',
             'Remaining',
             'Pool',
             'Hostname',
             'Origin',
         ]
 
     if family == 'inet6':
         for lease in raw_data:
             ipaddr = lease.get('ip')
             state = lease.get('state')
-            start = lease.get('last_communication')
-            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')
+            start = datetime.fromtimestamp(
+                lease.get('last_communication'), timezone.utc
+            )
+            end = (
+                datetime.fromtimestamp(lease.get('end'), timezone.utc)
+                if lease.get('end')
+                else '-'
+            )
             remain = lease.get('remaining')
             lease_type = lease.get('type')
             pool = lease.get('pool')
             host_identifier = lease.get('duid')
             data_entries.append(
                 [ipaddr, state, start, end, remain, lease_type, pool, host_identifier]
             )
 
         headers = [
             'IPv6 address',
             'State',
             'Last communication',
             'Lease expiration',
             'Remaining',
             'Type',
             'Pool',
             'DUID',
         ]
 
     output = tabulate(data_entries, headers, numalign='left')
     return output
 
 
 def _get_pool_size(pool, family='inet'):
     v = 'v6' if family == 'inet6' else ''
     base = f'service dhcp{v}-server shared-network-name {pool}'
     size = 0
     subnets = config.list_nodes(f'{base} subnet')
     for subnet in subnets:
         ranges = config.list_nodes(f'{base} subnet {subnet} range')
         for range in ranges:
             if family == 'inet6':
                 start = config.value(f'{base} subnet {subnet} range {range} start')
                 stop = config.value(f'{base} subnet {subnet} range {range} stop')
             else:
                 start = config.value(f'{base} subnet {subnet} range {range} start')
                 stop = config.value(f'{base} subnet {subnet} range {range} stop')
             # Add +1 because both range boundaries are inclusive
             size += int(ip_address(stop)) - int(ip_address(start)) + 1
     return size
 
 
 def _get_raw_server_pool_statistics(config, family='inet', pool=None):
     inet_suffix = '6' if family == 'inet6' else '4'
     pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
 
     stats = []
     for p in pools:
         size = _get_pool_size(family=family, pool=p)
         leases = len(_get_raw_server_leases(config, family=family, pool=p))
         use_percentage = round(leases / size * 100) if size != 0 else 0
         pool_stats = {
             'pool': p,
             'size': size,
             'leases': leases,
             'available': (size - leases),
             'use_percentage': use_percentage,
         }
         stats.append(pool_stats)
     return stats
 
 
 def _get_formatted_server_pool_statistics(pool_data, family='inet'):
     data_entries = []
     for entry in pool_data:
         pool = entry.get('pool')
         size = entry.get('size')
         leases = entry.get('leases')
         available = entry.get('available')
         use_percentage = entry.get('use_percentage')
         use_percentage = f'{use_percentage}%'
         data_entries.append([pool, size, leases, available, use_percentage])
 
     headers = ['Pool', 'Size', 'Leases', 'Available', 'Usage']
     output = tabulate(data_entries, headers, numalign='left')
     return output
 
 
 def _get_raw_server_static_mappings(config, family='inet', pool=None, sorted=None):
     inet_suffix = '6' if family == 'inet6' else '4'
     pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
 
     mappings = kea_get_static_mappings(config, inet_suffix, pools)
 
     if sorted:
         if sorted == 'ip':
             mappings.sort(key=lambda x: ip_address(x['ip']))
         else:
             mappings.sort(key=lambda x: x[sorted])
     return mappings
 
 
 def _get_formatted_server_static_mappings(raw_data, family='inet'):
     data_entries = []
 
     for entry in raw_data:
         pool = entry.get('pool')
         subnet = entry.get('subnet')
         hostname = entry.get('hostname')
         ip_addr = entry.get('ip', 'N/A')
         mac_addr = entry.get('mac', 'N/A')
         duid = entry.get('duid', 'N/A')
         description = entry.get('description', 'N/A')
         data_entries.append(
             [pool, subnet, hostname, ip_addr, mac_addr, duid, description]
         )
 
     headers = [
         'Pool',
         'Subnet',
         'Hostname',
         'IP Address',
         'MAC Address',
         'DUID',
         'Description',
     ]
     output = tabulate(data_entries, headers, numalign='left')
     return output
 
 
 def _verify_server(func):
     """Decorator checks if DHCP(v6) config exists"""
     from functools import wraps
 
     @wraps(func)
     def _wrapper(*args, **kwargs):
         config = ConfigTreeQuery()
         family = kwargs.get('family')
         v = 'v6' if family == 'inet6' else ''
         unconf_message = f'DHCP{v} server is not configured'
         # Check if config does not exist
         if not config.exists(f'service dhcp{v}-server'):
             raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
         return func(*args, **kwargs)
 
     return _wrapper
 
 
 def _verify_client(func):
     """Decorator checks if interface is configured as DHCP client"""
     from functools import wraps
     from vyos.ifconfig import Section
 
     @wraps(func)
     def _wrapper(*args, **kwargs):
         config = ConfigTreeQuery()
         family = kwargs.get('family')
         v = 'v6' if family == 'inet6' else ''
         interface = kwargs.get('interface')
         interface_path = Section.get_config_path(interface)
         unconf_message = f'DHCP{v} client not configured on interface {interface}!'
 
         # Check if config does not exist
         if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
             raise vyos.opmode.UnconfiguredObject(unconf_message)
         return func(*args, **kwargs)
 
     return _wrapper
 
 
 @_verify_server
 def show_server_pool_statistics(
     raw: bool, family: ArgFamily, pool: typing.Optional[str]
 ):
     v = 'v6' if family == 'inet6' else ''
     inet_suffix = '6' if family == 'inet6' else '4'
 
     if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
         Warning(stale_warn_msg)
 
     try:
         active_config = kea_get_active_config(inet_suffix)
     except Exception:
         raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
 
     active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
 
     if pool and active_pools and pool not in active_pools:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
 
     pool_data = _get_raw_server_pool_statistics(active_config, family=family, pool=pool)
     if raw:
         return pool_data
     else:
         return _get_formatted_server_pool_statistics(pool_data, family=family)
 
 
 @_verify_server
 def show_server_leases(
     raw: bool,
     family: ArgFamily,
     pool: typing.Optional[str],
     sorted: typing.Optional[str],
     state: typing.Optional[ArgState],
     origin: typing.Optional[ArgOrigin],
 ):
     v = 'v6' if family == 'inet6' else ''
     inet_suffix = '6' if family == 'inet6' else '4'
 
     if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
         Warning(stale_warn_msg)
 
     try:
         active_config = kea_get_active_config(inet_suffix)
     except Exception:
         raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
 
     active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
 
     if pool and active_pools and pool not in active_pools:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
 
     sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet
     if sorted and sorted not in sort_valid:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
 
     if state and state not in lease_valid_states:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!')
 
     lease_data = _get_raw_server_leases(
         config=active_config,
         family=family,
         pool=pool,
         sorted=sorted,
         state=state,
         origin=origin,
     )
     if raw:
         return lease_data
     else:
         return _get_formatted_server_leases(lease_data, family=family)
 
 
 @_verify_server
 def show_server_static_mappings(
     raw: bool,
     family: ArgFamily,
     pool: typing.Optional[str],
     sorted: typing.Optional[str],
 ):
     v = 'v6' if family == 'inet6' else ''
     inet_suffix = '6' if family == 'inet6' else '4'
 
     if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
         Warning(stale_warn_msg)
 
     try:
         active_config = kea_get_active_config(inet_suffix)
     except Exception:
         raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
 
     active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
 
     if pool and active_pools and pool not in active_pools:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
 
     if sorted and sorted not in mapping_sort_valid:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
 
     static_mappings = _get_raw_server_static_mappings(
         config=active_config, family=family, pool=pool, sorted=sorted
     )
     if raw:
         return static_mappings
     else:
         return _get_formatted_server_static_mappings(static_mappings, family=family)
 
 
 def _lease_valid(inet, address):
     leases = kea_get_leases(inet)
     return any(lease['ip-address'] == address for lease in leases)
 
 
 @_verify_server
 def clear_dhcp_server_lease(family: ArgFamily, address: str):
     v = 'v6' if family == 'inet6' else ''
     inet = '6' if family == 'inet6' else '4'
 
     if not _lease_valid(inet, address):
         print(f'Lease not found on DHCP{v} server')
         return None
 
     if not kea_delete_lease(inet, address):
         print(f'Failed to clear lease for "{address}"')
         return None
 
     print(f'Lease "{address}" has been cleared')
 
 
 def _get_raw_client_leases(family='inet', interface=None):
     from time import mktime
     from datetime import datetime
     from vyos.defaults import directories
     from vyos.utils.network import get_interface_vrf
 
     lease_dir = directories['isc_dhclient_dir']
     lease_files = []
     lease_data = []
 
     if interface:
         tmp = f'{lease_dir}/dhclient_{interface}.lease'
         if os.path.exists(tmp):
             lease_files.append(tmp)
     else:
         # All DHCP leases
         lease_files = glob(f'{lease_dir}/dhclient_*.lease')
 
     for lease in lease_files:
         tmp = {}
         with open(lease, 'r') as f:
             for line in f.readlines():
                 line = line.rstrip()
                 if 'last_update' not in tmp:
                     # ISC dhcp client contains least_update timestamp in human readable
                     # format this makes less sense for an API and also the expiry
                     # timestamp is provided in UNIX time. Convert string (e.g. Sun Jul
                     # 30 18:13:44 CEST 2023) to UNIX time (1690733624)
                     tmp.update(
                         {
                             'last_update': int(
                                 mktime(datetime.strptime(line, time_string).timetuple())
                             )
                         }
                     )
                     continue
 
                 k, v = line.split('=')
                 tmp.update({k: v.replace("'", '')})
 
         if 'interface' in tmp:
             vrf = get_interface_vrf(tmp['interface'])
             if vrf:
                 tmp.update({'vrf': vrf})
 
         lease_data.append(tmp)
 
     return lease_data
 
 
 def _get_formatted_client_leases(lease_data, family):
     from time import localtime
     from time import strftime
 
     from vyos.utils.network import is_intf_addr_assigned
 
     data_entries = []
     for lease in lease_data:
         if not lease.get('new_ip_address'):
             continue
         data_entries.append(['Interface', lease['interface']])
         if 'new_ip_address' in lease:
             tmp = (
                 '[Active]'
                 if is_intf_addr_assigned(lease['interface'], lease['new_ip_address'])
                 else '[Inactive]'
             )
             data_entries.append(['IP address', lease['new_ip_address'], tmp])
         if 'new_subnet_mask' in lease:
             data_entries.append(['Subnet Mask', lease['new_subnet_mask']])
         if 'new_domain_name' in lease:
             data_entries.append(['Domain Name', lease['new_domain_name']])
         if 'new_routers' in lease:
             data_entries.append(['Router', lease['new_routers']])
         if 'new_domain_name_servers' in lease:
             data_entries.append(['Name Server', lease['new_domain_name_servers']])
         if 'new_dhcp_server_identifier' in lease:
             data_entries.append(['DHCP Server', lease['new_dhcp_server_identifier']])
         if 'new_dhcp_lease_time' in lease:
             data_entries.append(['DHCP Server', lease['new_dhcp_lease_time']])
         if 'vrf' in lease:
             data_entries.append(['VRF', lease['vrf']])
         if 'last_update' in lease:
             tmp = strftime(time_string, localtime(int(lease['last_update'])))
             data_entries.append(['Last Update', tmp])
         if 'new_expiry' in lease:
             tmp = strftime(time_string, localtime(int(lease['new_expiry'])))
             data_entries.append(['Expiry', tmp])
 
         # Add empty marker
         data_entries.append([''])
 
     output = tabulate(data_entries, tablefmt='plain')
 
     return output
 
 
 def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[str]):
     lease_data = _get_raw_client_leases(family=family, interface=interface)
     if raw:
         return lease_data
     else:
         return _get_formatted_client_leases(lease_data, family=family)
 
 
 @_verify_client
 def renew_client_lease(raw: bool, family: ArgFamily, interface: str):
     if not raw:
         v = 'v6' if family == 'inet6' else ''
         print(f'Restarting DHCP{v} client on interface {interface}...')
     if family == 'inet6':
         call(f'systemctl restart dhcp6c@{interface}.service')
     else:
         call(f'systemctl restart dhclient@{interface}.service')
 
 
 @_verify_client
 def release_client_lease(raw: bool, family: ArgFamily, interface: str):
     if not raw:
         v = 'v6' if family == 'inet6' else ''
         print(f'Release DHCP{v} client on interface {interface}...')
     if family == 'inet6':
         call(f'systemctl stop dhcp6c@{interface}.service')
     else:
         call(f'systemctl stop dhclient@{interface}.service')
 
 
 if __name__ == '__main__':
     try:
         res = vyos.opmode.run(sys.modules[__name__])
         if res:
             print(res)
     except (ValueError, vyos.opmode.Error) as e:
         print(e)
         sys.exit(1)