From 89995dcec3a2a6b9112f11cf1c9e4a1bcb4d66d6 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 23 Jun 2019 17:01:12 +0200 Subject: T1470: improve output of "show dhcpv6 server leases" - change DUID to IAID_DUID - format IAID_DUID as colon-separated hex list - implement functions: pool, sort, state - add op-mode definitions for pool, sort, state - add columns: State, Type, Last communication, Pool - implement json output - implement completionHelp function --- src/op_mode/show_dhcpv6.py | 104 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 16 deletions(-) (limited to 'src/op_mode/show_dhcpv6.py') diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index bf73b92ea..f1f5a6a55 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -20,11 +20,42 @@ import argparse import ipaddress import tabulate import sys +import collections from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases lease_file = "/config/dhcpdv6.leases" +pool_key = "shared-networkname" + +lease_display_fields = collections.OrderedDict() +lease_display_fields['ip'] = 'IPv6 address' +lease_display_fields['state'] = 'State' +lease_display_fields['last_comm'] = 'Last communication' +lease_display_fields['expires'] = 'Lease expiration' +lease_display_fields['type'] = 'Type' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['iaid_duid'] = 'IAID_DUID' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +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 get_lease_data(lease): data = {} @@ -35,27 +66,55 @@ def get_lease_data(lease): except: data["expires"] = "" - data["duid"] = lease.host_identifier_string + try: + data["last_comm"] = lease.last_communication.strftime("%Y/%m/%d %H:%M:%S") + except: + data["last_comm"] = "" + + # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...} + # where IAID_DUID is the combined IAID and DUID + data["iaid_duid"] = format_hex_string(lease.host_identifier_string) + + lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"} + data["type"] = lease_types_long[lease.type] + + data["state"] = lease.binding_state data["ip"] = lease.ip + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + return data -def get_leases(leases, state=None): +def get_leases(leases, state, pool=None, sort='ip'): leases = IscDhcpLeases(lease_file).get() - if state is not None: - leases = list(filter(lambda x: x.binding_state == 'active', leases)) + if state != 'all': + leases = list(filter(lambda x: x.binding_state == state, leases)) - return list(map(get_lease_data, leases)) + # filter lease by pool name + if pool is not None: + leases = list(filter(lambda x: in_pool(x, pool), leases)) -def show_leases(leases): - headers = ["IPv6 address", "Lease expiration", "DUID"] + leases = list(map(get_lease_data, leases)) + if sort == 'ip': + leases = sorted(leases, key = lambda k: int(ipaddress.IPv6Address(k['ip']))) + else: + leases = sorted(leases, key = lambda k: k[sort]) + + return leases +def show_leases(leases): lease_list = [] for l in leases: - lease_list.append([l["ip"], l["expires"], l["duid"]]) + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) - output = tabulate.tabulate(lease_list, headers) + output = tabulate.tabulate(lease_list, lease_display_fields.values()) print(output) @@ -63,11 +122,14 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() - group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") - group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") - parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, choices=lease_valid_states, default="active", help="Lease state to show") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() @@ -78,9 +140,19 @@ if __name__ == '__main__': sys.exit(0) if args.leases: - leases = get_leases(lease_file, state='active') - show_leases(leases) + leases = get_leases(lease_file, args.state, args.pool, args.sort) + + if args.json: + print(json.dumps(leases, indent=4)) + else: + show_leases(leases) elif args.statistics: print("DHCPv6 statistics option is not available") + elif args.allowed == 'pool': + print(' '.join(c.list_effective_nodes("service dhcpv6-server shared-network-name"))) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) else: - print("Invalid option") + parser.print_help() -- cgit v1.2.3 From 9f20bee81c0a0f4632aa152297d0fdf89139d6af Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Fri, 19 Jul 2019 02:28:53 +0200 Subject: T1376: improve show_dhcp and show_dhcpv6 --- op-mode-definitions/dhcp.xml | 32 ++++++++-- src/op_mode/show_dhcp.py | 143 +++++++++++++++++++++++++++++++------------ src/op_mode/show_dhcpv6.py | 61 +++++++++++++++--- 3 files changed, 183 insertions(+), 53 deletions(-) (limited to 'src/op_mode/show_dhcpv6.py') diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml index 989c8274a..f142cdd0e 100644 --- a/op-mode-definitions/dhcp.xml +++ b/op-mode-definitions/dhcp.xml @@ -9,7 +9,7 @@ - Show DHCP information + Show DHCP server information @@ -20,10 +20,31 @@ - Show DHCP leases for a specific pool + Show DHCP server leases for a specific pool + + + sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6 + + + Show DHCP server leases sorted by the specified key + + + + + sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --sort $6 + + + + Show DHCP server leases with a specific state (can be multiple, comma-separated) + + + + + sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --state $(echo $6 | tr , " ") + @@ -35,6 +56,9 @@ Show DHCP server statistics for a specific pool + + + sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6 @@ -80,12 +104,12 @@ - Show DHCPv6 server leases with a specific state + Show DHCPv6 server leases with a specific state (can be multiple, comma-separated) - sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $6 + sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $(echo $6 | tr , " ") diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index 652173dc1..c2a05f516 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -20,6 +20,9 @@ import argparse import ipaddress import tabulate import sys +import collections +import os +from datetime import datetime from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases @@ -27,6 +30,18 @@ from isc_dhcp_leases import Lease, IscDhcpLeases lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" +lease_display_fields = collections.OrderedDict() +lease_display_fields['ip'] = 'IP address' +lease_display_fields['hardware_address'] = 'Hardware address' +lease_display_fields['state'] = 'State' +lease_display_fields['start'] = 'Lease start' +lease_display_fields['end'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['hostname'] = 'Hostname' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + def in_pool(lease, pool): if pool_key in lease.sets: if lease.sets[pool_key] == pool: @@ -34,17 +49,47 @@ def in_pool(lease, pool): return False +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) + def get_lease_data(lease): data = {} - # End time may not be present in backup leases + # isc-dhcp lease times are in UTC so we need to convert them to local time to display try: - data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S") except: - data["expires"] = "" + data["start"] = "" + + try: + data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") + except: + data["end"] = "" + + try: + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["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["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" + except: + data["remaining"] = "" + + # currently not used but might come in handy + # todo: parse into datetime string + for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']: + if prop in lease.data: + data[prop] = lease.data[prop] + else: + data[prop] = '' data["hardware_address"] = lease.ethernet data["hostname"] = lease.hostname + + data["state"] = lease.binding_state data["ip"] = lease.ip try: @@ -54,38 +99,53 @@ def get_lease_data(lease): return data -def get_leases(leases, state=None, pool=None): - # define variable for leases - leases_dict = {} - +def get_leases(leases, state, pool=None, sort='ip'): # get leases from file leases = IscDhcpLeases(lease_file).get() - # convert leases list to dictionary to avoid records duplication - it's the fastest and easiest way to do this + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) + + # filter leases by pool name + if pool is not None: + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + sys.exit(0) + + # should maybe filter all state=active by lease.valid here? + + # sort by start time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.start) + + # dedupe by converting to dict + leases_dict = {} for lease in leases: + # dedupe by IP leases_dict[lease.ip] = lease - # filter leases by state - if state is 'active': - leases = list(filter(lambda x: x.active and x.valid, leases_dict.values())) - if state is 'free': - leases = list(filter(lambda x: x.binding_state == 'free', leases_dict.values())) + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) - # filter lease by pool name - if pool is not None: - leases = list(filter(lambda x: in_pool(x, pool), leases)) + # apply output/display sort + if sort == 'ip': + leases = sorted(leases, key = lambda lease: int(ipaddress.ip_address(lease['ip']))) + else: + leases = sorted(leases, key = lambda lease: lease[sort]) - # return sorted leases list - return sorted(list(map(get_lease_data, leases)), key = lambda k: k['ip']) + return leases def show_leases(leases): - headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] - lease_list = [] for l in leases: - lease_list.append([l["ip"], l["hardware_address"], l["expires"], l["pool"], l["hostname"]]) + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) - output = tabulate.tabulate(lease_list, headers) + output = tabulate.tabulate(lease_list, lease_display_fields.values()) print(output) @@ -98,7 +158,7 @@ def get_pool_size(config, pool): start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - size += int(ipaddress.IPv4Address(stop)) - int(ipaddress.IPv4Address(start)) + size += int(ipaddress.ip_address(stop)) - int(ipaddress.ip_address(start)) return size @@ -114,35 +174,33 @@ if __name__ == '__main__': group = parser.add_mutually_exclusive_group() group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") - parser.add_argument("-e", "--expired", action="store_true", help="Show expired leases") - parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() # Do nothing if service is not configured config = Config() if not config.exists_effective('service dhcp-server'): - print("DHCP service is not configured") + print("DHCP service is not configured.") sys.exit(0) + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if os.system('systemctl -q is-active isc-dhcp-server.service') != 0: + print("WARNING: DHCP server is configured but not started. Data may be stale.") + if args.leases: - if args.expired: - if args.pool: - leases = get_leases(lease_file, state='free', pool=args.pool) - else: - leases = get_leases(lease_file, state='free') - else: - if args.pool: - leases = get_leases(lease_file, state='active', pool=args.pool) - else: - leases = get_leases(lease_file, state='active') + leases = get_leases(lease_file, args.state, args.pool, args.sort) if args.json: print(json.dumps(leases, indent=4)) else: show_leases(leases) + elif args.statistics: pools = [] @@ -156,7 +214,7 @@ if __name__ == '__main__': stats = [] for p in pools: size = get_pool_size(config, p) - leases = len(get_leases(lease_file, state='active', pool=args.pool)) + leases = len(get_leases(lease_file, state='active', pool=p)) if size != 0: use_percentage = round(leases / size * 100) @@ -176,5 +234,12 @@ if __name__ == '__main__': print(json.dumps(stats, indent=4)) else: show_pool_stats(stats) + + elif args.allowed == 'pool': + print(' '.join(config.list_effective_nodes("service dhcp-server shared-network-name"))) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) else: - print("Use either --leases or --statistics option") + parser.print_help() diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index f1f5a6a55..1a6ee62e6 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -21,6 +21,8 @@ import ipaddress import tabulate import sys import collections +import os +from datetime import datetime from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases @@ -33,6 +35,7 @@ lease_display_fields['ip'] = 'IPv6 address' lease_display_fields['state'] = 'State' lease_display_fields['last_comm'] = 'Last communication' lease_display_fields['expires'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' lease_display_fields['type'] = 'Type' lease_display_fields['pool'] = 'Pool' lease_display_fields['iaid_duid'] = 'IAID_DUID' @@ -57,20 +60,35 @@ def format_hex_string(in_str): return out_str +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) + def get_lease_data(lease): data = {} - # End time may not be present in backup leases + # isc-dhcp lease times are in UTC so we need to convert them to local time to display try: - data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") except: data["expires"] = "" try: - data["last_comm"] = lease.last_communication.strftime("%Y/%m/%d %H:%M:%S") + data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S") except: data["last_comm"] = "" + try: + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["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["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" + except: + data["remaining"] = "" + # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...} # where IAID_DUID is the combined IAID and DUID data["iaid_duid"] = format_hex_string(lease.host_identifier_string) @@ -91,16 +109,35 @@ def get_lease_data(lease): def get_leases(leases, state, pool=None, sort='ip'): leases = IscDhcpLeases(lease_file).get() - if state != 'all': - leases = list(filter(lambda x: x.binding_state == state, leases)) + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) - # filter lease by pool name + # filter leases by pool name if pool is not None: - leases = list(filter(lambda x: in_pool(x, pool), leases)) + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + sys.exit(0) - leases = list(map(get_lease_data, leases)) + # should maybe filter all state=active by lease.valid here? + + # sort by last_comm time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.last_communication) + + # dedupe by converting to dict + leases_dict = {} + for lease in leases: + # dedupe by IP + leases_dict[lease.ip] = lease + + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) + + # apply output/display sort if sort == 'ip': - leases = sorted(leases, key = lambda k: int(ipaddress.IPv6Address(k['ip']))) + leases = sorted(leases, key = lambda k: int(ipaddress.ip_address(k['ip']))) else: leases = sorted(leases, key = lambda k: k[sort]) @@ -128,7 +165,7 @@ if __name__ == '__main__': parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") - parser.add_argument("-t", "--state", type=str, choices=lease_valid_states, default="active", help="Lease state to show") + parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() @@ -139,6 +176,10 @@ if __name__ == '__main__': print("DHCPv6 service is not configured") sys.exit(0) + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if os.system('systemctl -q is-active isc-dhcpv6-server.service') != 0: + print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") + if args.leases: leases = get_leases(lease_file, args.state, args.pool, args.sort) -- cgit v1.2.3