From a05197c756998e42e763783a5e2014663bcdf952 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 21 May 2019 15:06:46 -0700 Subject: [pppoe-server] T1393 - adding ip6 and ip6-pd to 'show pppoe-server sessions' --- op-mode-definitions/pppoe-server.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml index d8a518573..a2366c710 100644 --- a/op-mode-definitions/pppoe-server.xml +++ b/op-mode-definitions/pppoe-server.xml @@ -11,7 +11,7 @@ Show active PPPoE server sessions - /usr/bin/accel-cmd 'show sessions ifname,username,ip,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes' + /usr/bin/accel-cmd 'show sessions ifname,username,ip,ip6,ip6-dp,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes' -- cgit v1.2.3 From 7a27f6f93b58abd2fabc9e80e429e14a70ebd6aa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 30 May 2019 23:30:01 +0200 Subject: [dhcp] T1416: fix DHCP server status view --- op-mode-definitions/dhcp.xml | 4 ++-- src/op_mode/show_dhcp.py | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml index a7d09304e..85403af6f 100644 --- a/op-mode-definitions/dhcp.xml +++ b/op-mode-definitions/dhcp.xml @@ -22,7 +22,7 @@ Show DHCP leases for a specific pool - sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $4 + sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6 @@ -36,7 +36,7 @@ Show DHCP server statistics for a specific pool - sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $4 + sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6 diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index 4c4ee6355..652173dc1 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -55,15 +55,28 @@ def get_lease_data(lease): return data def get_leases(leases, state=None, pool=None): + # define variable for leases + leases_dict = {} + + # get leases from file leases = IscDhcpLeases(lease_file).get() - if state is not None: - leases = list(filter(lambda x: x.binding_state == 'active', leases)) + # convert leases list to dictionary to avoid records duplication - it's the fastest and easiest way to do this + for lease in leases: + 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())) + # filter lease by pool name if pool is not None: leases = list(filter(lambda x: in_pool(x, pool), leases)) - return list(map(get_lease_data, leases)) + # return sorted leases list + return sorted(list(map(get_lease_data, leases)), key = lambda k: k['ip']) def show_leases(leases): headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] @@ -73,7 +86,7 @@ def show_leases(leases): lease_list.append([l["ip"], l["hardware_address"], l["expires"], l["pool"], l["hostname"]]) output = tabulate.tabulate(lease_list, headers) - + print(output) def get_pool_size(config, pool): @@ -146,7 +159,7 @@ if __name__ == '__main__': leases = len(get_leases(lease_file, state='active', pool=args.pool)) if size != 0: - use_percentage = round(leases / size) * 100 + use_percentage = round(leases / size * 100) else: use_percentage = 0 -- cgit v1.2.3 From efb598caafc20db278938ff3787e3674467e0663 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 10:51:52 +0200 Subject: T1438: fix permissions when invoking 'show version' Accessing Kernel DMI data (under /sys/class/dmi) requires elevated permission and thus retrieving a Board Serial/UUID was not possible. version.py is now called via sudo to gether all facts. --- op-mode-definitions/version.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/version.xml b/op-mode-definitions/version.xml index 593785f7a..57931fdff 100644 --- a/op-mode-definitions/version.xml +++ b/op-mode-definitions/version.xml @@ -6,19 +6,19 @@ Show system version information - ${vyos_op_scripts_dir}/version.py + sudo ${vyos_op_scripts_dir}/version.py Show system version and some fun stuff - ${vyos_op_scripts_dir}/version.py --funny + sudo ${vyos_op_scripts_dir}/version.py --funny Show system version and versions of all packages - ${vyos_op_scripts_dir}/version.py --all + sudo ${vyos_op_scripts_dir}/version.py --all -- cgit v1.2.3 From 9370de0ebafa85608e32ed779545e35e532e8009 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 01:24:02 +0200 Subject: bfd: T1137: add 'show protocols bfd peer' command --- op-mode-definitions/show-protocols-bfd.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 op-mode-definitions/show-protocols-bfd.xml (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml new file mode 100644 index 000000000..052e6a700 --- /dev/null +++ b/op-mode-definitions/show-protocols-bfd.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + Show Bidirectional Forwarding Detection (BFD) Status + + /usr/bin/vtysh -c "show bfd peers" + + + + + + + + -- cgit v1.2.3 From 921849da85c634fb34d331c318992697db6449e7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 23 Jun 2019 11:34:30 +0200 Subject: bfd: T1183: support show of individual BFD peer vyos@vyos:~$ show protocols bfd peer BFD Peers: peer 172.18.1.2 interface eth0.701 ID: 3762227714 Remote ID: 3787683864 Status: up Uptime: 24 minute(s), 54 second(s) Diagnostics: ok Remote diagnostics: ok Local timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms Remote timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms peer 172.18.0.2 interface eth0.700 ID: 3132309989 Remote ID: 859733951 Status: up Uptime: 25 minute(s), 24 second(s) Diagnostics: ok Remote diagnostics: ok Local timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms Remote timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms vyos@vyos:~$ show protocols bfd peer Possible completions: Execute the current command 172.18.0.2 Show Bidirectional Forwarding Detection (BFD) peer status 172.18.1.2 vyos@vyos:~$ show protocols bfd peer 1 172.18.0.2 172.18.1.2 vyos@vyos:~$ show protocols bfd peer 172.18.0.2 BFD Peer: peer 172.18.0.2 interface eth0.700 ID: 3132309989 Remote ID: 859733951 Status: up Uptime: 25 minute(s), 29 second(s) Diagnostics: ok Remote diagnostics: ok Local timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms Remote timers: Receive interval: 300ms Transmission interval: 300ms Echo transmission interval: 50ms --- op-mode-definitions/show-protocols-bfd.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml index 052e6a700..3c682d6f7 100644 --- a/op-mode-definitions/show-protocols-bfd.xml +++ b/op-mode-definitions/show-protocols-bfd.xml @@ -8,10 +8,19 @@ - Show Bidirectional Forwarding Detection (BFD) Status + Show all Bidirectional Forwarding Detection (BFD) peer status /usr/bin/vtysh -c "show bfd peers" + + + Show Bidirectional Forwarding Detection (BFD) peer status + + + + + /usr/bin/vtysh -c "show bfd peer $5" + -- cgit v1.2.3 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 --- op-mode-definitions/dhcp.xml | 29 ++++++++++++ src/conf_mode/dhcpv6_server.py | 3 ++ src/op_mode/show_dhcpv6.py | 104 ++++++++++++++++++++++++++++++++++------- 3 files changed, 120 insertions(+), 16 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml index 85403af6f..989c8274a 100644 --- a/op-mode-definitions/dhcp.xml +++ b/op-mode-definitions/dhcp.xml @@ -59,6 +59,35 @@ Show DHCPv6 server leases sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases + + + + Show DHCPv6 server leases for a specific pool + + + + + sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --pool $6 + + + + Show DHCPv6 server leases sorted by the specified key + + + + + sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --sort $6 + + + + Show DHCPv6 server leases with a specific state + + + + + sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $6 + + diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 37da3ef25..f5117de53 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -101,6 +101,9 @@ shared-network {{ network.name }} { {%- endfor %} } {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + } } {%- endif %} {% endfor %} 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 1027d1c622f9d0e6a062fa1119c8bffbf12151fb Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 24 Jun 2019 11:03:43 -0700 Subject: [IPoE] T989 - IPoE implementation * chap-secrets file generation * noauth in accel config as option * local auth with csid implemented * radius implementation * shaper per user implemented * op comands for stats --- interface-definitions/ipoe-server.xml | 279 +++++++++++++++++++++++++++++ op-mode-definitions/ipoe-server.xml | 26 +++ src/conf_mode/ipoe_server.py | 325 ++++++++++++++++++++++++++++++++++ 3 files changed, 630 insertions(+) create mode 100644 interface-definitions/ipoe-server.xml create mode 100644 op-mode-definitions/ipoe-server.xml create mode 100755 src/conf_mode/ipoe_server.py (limited to 'op-mode-definitions') diff --git a/interface-definitions/ipoe-server.xml b/interface-definitions/ipoe-server.xml new file mode 100644 index 000000000..18968a033 --- /dev/null +++ b/interface-definitions/ipoe-server.xml @@ -0,0 +1,279 @@ + + + + + + + Internet Protocol over Ethernet (IPoE) Server + 900 + + + + + Network interface to server IPoE + + + + + + + + Network Layer IPoE serves on + + L2 L3 + + + ^(L2|L3) + + + L2 + client share the same subnet + + + L3 + clients are behind this router + + + + + + Enables clients to share the same network or each client has its own vlan + + shared vlan + + + ^(shared|vlan) + + + shared + Multiple clients share the same network + + + vlan + One VLAN per client + + + + + + Client address pool + + ipv4net + IPv4 address and prefix length + + + + + + + + + DHCP requests will be forwarded + + + + + DHCP Server the request will be redirected to. + + ipv4 + IPv4 address of the DHCP Server + + + + + + + + + address of the relay agent (Relay Agent IP Address) + + + + + + + + + DNS servers offered via internal DHCP + + + + + IP address of the primary DNS server + + + + + + + + IP address of the primary DNS server + + + + + + + + + + Client authentication methods + + + + + Authetication mode + + local radius noauth + + + ^(local|radius|noauth) + + + local + Authentication based on local definition + + + radius + Authentication based on a RADIUS server + + + noauth + Authentication disabled + + + + + + Network interface the client mac will appear on + + + + + + + + Client mac address allowed to receive an IP address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Upload/Download speed limits + + + + + Upload bandwidth limit in kbits/sec + + + + + + + + Download bandwidth limit in kbits/sec + + + + + + + + + + + + + + IP address of RADIUS server + + ipv4 + IP address of RADIUS server + + + + + + Key for accessing the specified server + + + + + Maximum number of simultaneous requests to server (default: unlimited) + + + + + If server doesn't responds mark it as unavailable for this amount of time in seconds + + + + + + + RADIUS settings + + + + + Timeout to wait response from server (seconds) + + + + + Timeout to wait reply for Interim-Update packets. (default 3 seconds) + + + + + Maximum number of tries to send Access-Request/Accounting-Request queries + + + + + Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests. + + + + + Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address. + + + + + IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA) + + + + + IP address for Dynamic Authorization Extension server (DM/CoA) + + + + + Port for Dynamic Authorization Extension server (DM/CoA) + + + + + Secret for Dynamic Authorization Extension server (DM/CoA) + + + + + + + + + + + + + diff --git a/op-mode-definitions/ipoe-server.xml b/op-mode-definitions/ipoe-server.xml new file mode 100644 index 000000000..484201f40 --- /dev/null +++ b/op-mode-definitions/ipoe-server.xml @@ -0,0 +1,26 @@ + + + + + + + show ipoe-server status + + + + + Show active IPoE server sessions + + /usr/bin/accel-cmd '-p 2002 show sessions ifname,called-sid,calling-sid,ip,ip6,ip6-dp,rate-limit,state,uptime,sid' + + + + Show IPoE server statistics + + /usr/bin/accel-cmd '-p 2002 show stat' + + + + + + diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py new file mode 100755 index 000000000..39f0cb279 --- /dev/null +++ b/src/conf_mode/ipoe_server.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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 . +# +# + +import sys +import os +import re +import time +import socket +import subprocess +import jinja2 +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' +ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config' + +pidfile = r'/var/run/accel_ipoe.pid' +cmd_port = r'2002' + +chap_secrets = ipoe_cnf_dir + '/chap-secrets' +## accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid + +ipoe_config = ''' +### generated by ipoe.py ### +[modules] +log_syslog +ippool +ipoe +shaper +{% if auth == 'radius' %} +radius +{% endif -%} +{% if auth == 'local' %} +chap-secrets +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-ipoe,daemon +copy=1 +level=5 + +[ipoe] +verbose=1 +{% for intfc in interfaces %} +interface={{intfc}},\ +shared={{interfaces[intfc]['shared']}},\ +mode={{interfaces[intfc]['mode']}},\ +ifcfg={{interfaces[intfc]['ifcfg']}},\ +range={{interfaces[intfc]['range']}},\ +start={{interfaces[intfc]['sess_start']}} +{% endfor %} +{% if auth == 'noauth' %} +noauth=1 +{% endif %} +{% if auth == 'local' %} +username=ifname +password=csid +{% endif %} + +{% if (dns['server1']) or (dns['server2']) %} +[dns] +{% if dns['server1'] %} +dns1={{dns['server1']}} +{% endif -%} +{% if dns['server2'] %} +dns2={{dns['server2']}} +{% endif -%} +{% endif %} + +{% if auth == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/ipoe/chap-secrets +{% endif %} + +{% if auth == 'radius' %} +[radius] +verbose=1 +{% for srv in radius %} +server={{srv}},{{radius[srv]['secret']}},\ +req-limit={{radius[srv]['req-limit']}},\ +fail-time={{radius[srv]['fail-time']}} +{% endfor %} +{% if radsettings['dae-server']['ip-address'] %} +dae-server={{radsettings['dae-server']['ip-address']}}:{{radsettings['dae-server']['port']}},{{radsettings['dae-server']['secret']}} +{% endif -%} +{% if radsettings['acct-timeout'] %} +acct-timeout={{radsettings['acct-timeout']}} +{% endif -%} +{% if radsettings['max-try'] %} +max-try={{radsettings['max-try']}} +{% endif -%} +{% if radsettings['nas-ip-address'] %} +nas-ip-address={{radsettings['nas-ip-address']}} +{% endif -%} +{% if radsettings['nas-identifier'] %} +nas-identifier={{radsettings['nas-identifier']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2002 +''' + +### pppoe chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses shaper +{% for aifc in auth_if %} +{% for mac in auth_if[aifc] %} +{% if (auth_if[aifc][mac]['up']) and (auth_if[aifc][mac]['down']) %} +{{aifc}}\t*\t{{mac}}\t*\t{{auth_if[aifc][mac]['down']}}/{{auth_if[aifc][mac]['up']}} +{% else %} +{{aifc}}\t*\t{{mac}}\t* +{% endif %} +{% endfor %} +{% endfor %} +''' + +##### Inline functions start #### +### config path creation +if not os.path.exists(ipoe_cnf_dir): + os.makedirs(ipoe_cnf_dir) + sl.syslog(sl.LOG_NOTICE, ipoe_cnf_dir + " created") + +def get_cpu(): + cpu_cnt = 1 + if os.cpu_count() == 1: + cpu_cnt = 1 + else: + cpu_cnt = int(os.cpu_count()/2) + return cpu_cnt + +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", int(cmd_port))) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start pppoe server") + break + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd', '-p', cmd_port, cmd]).decode().strip() + return ret + except: + return 1 + +### chap_secrets file if auth mode local +def gen_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +##### Inline functions end #### + +def get_config(): + c = Config() + if not c.exists('service ipoe-server'): + return None + + config_data = {} + + c.set_level('service ipoe-server') + for intfc in c.list_nodes('interface'): + config_data.update( + { + 'interfaces' : { + intfc : { + 'mode' : 'L2', + 'shared' : '1', + 'sess_start' : 'dhcpv4', ### may need a conifg option, can be dhcpv4 or up for unclassified pkts + 'range' : '', + 'ifcfg' : '1' + } + }, + 'dns' : { + 'server1' : None, + 'server2' : None + }, + 'auth' : 'noauth', + 'auth_if' : {}, + 'radius' : {}, + 'radsettings' : { + 'dae-server' : {} + } + } + ) + + if c.exists('interface ' + intfc + ' network-mode'): + config_data['interfaces'][intfc]['mode'] = c.return_value('interface ' + intfc + ' network-mode') + if c.return_value('interface ' + intfc + ' network') == 'vlan': + config_data['interfaces'][intfc]['shared'] = '0' + if c.exists('interface ' + intfc + ' client-subnet'): + config_data['interfaces'][intfc]['range'] = c.return_value('interface ' + intfc + ' client-subnet') + if c.exists('dns-server server-1'): + config_data['dns']['server1'] = c.return_value('dns-server server-1') + if c.exists('dns-server server-2'): + config_data['dns']['server2'] = c.return_value('dns-server server-2') + if not c.exists('authentication mode noauth'): + config_data['auth'] = c.return_value('authentication mode') + if c.exists('authentication mode local'): + for auth_int in c.list_nodes('authentication interface'): + for mac in c.list_nodes('authentication interface ' + auth_int + ' mac-address'): + config_data['auth_if'][auth_int] = {} + if c.exists('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit'): + config_data['auth_if'][auth_int][mac] = {} + config_data['auth_if'][auth_int][mac]['up'] = c.return_value('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit upload') + config_data['auth_if'][auth_int][mac]['down'] = c.return_value('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit download') + else: + config_data['auth_if'][auth_int][mac] = {} + config_data['auth_if'][auth_int][mac]['up'] = None + config_data['auth_if'][auth_int][mac]['down'] = None + if c.exists('authentication mode radius'): + for rsrv in c.list_nodes('authentication radius-server'): + config_data['radius'][rsrv] = {} + if c.exists('authentication radius-server ' + rsrv + ' secret'): + config_data['radius'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') + if c.exists('authentication radius-server ' + rsrv + ' fail-time'): + config_data['radius'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') + else: + config_data['radius'][rsrv]['fail-time'] = '0' + if c.exists('authentication radius-server ' + rsrv + ' req-limit'): + config_data['radius'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit') + else: + config_data['radius'][rsrv]['req-limit'] = '0' + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings timeout'): + config_data['radsettings']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['radsettings']['nas-ip-address'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings nas-identifier'): + config_data['radsettings']['nas-identifier'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings max-try'): + config_data['radsettings']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings acct-timeout'): + config_data['radsettings']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings dae-server ip-address'): + config_data['radsettings']['dae-server']['ip-address'] = c.return_value('authentication radius-settings dae-server ip-address') + if c.exists('authentication radius-settings dae-server port'): + config_data['radsettings']['dae-server']['port'] = c.return_value('authentication radius-settings dae-server port') + if c.exists('authentication radius-settings dae-server secret'): + config_data['radsettings']['dae-server']['secret'] = c.return_value('authentication radius-settings dae-server secret') + + return config_data + +def generate(c): + if c == None or not c: + return None + + c['thread_cnt'] = get_cpu() + + if c['auth'] == 'local': + gen_chap_secrets(c) + + tmpl = jinja2.Template(ipoe_config, trim_blocks=True) + config_text = tmpl.render(c) + + open(ipoe_cnf,'w').write(config_text) + return c + +def verify(c): + if c == None or not c: + return None + + for intfc in c['interfaces']: + if not c['interfaces'][intfc]['range']: + raise ConfigError("service ipoe-server interface eth2 client-subnet needs a value") + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd', '-c', ipoe_cnf, '-p', pidfile, '-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 5e04457aa33511bbed8fa19d2180afb7c0b49050 Mon Sep 17 00:00:00 2001 From: hagbard Date: Thu, 27 Jun 2019 14:45:46 -0700 Subject: [IPoE] fixed show commands --- op-mode-definitions/ipoe-server.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/ipoe-server.xml b/op-mode-definitions/ipoe-server.xml index 484201f40..ea14e9a5c 100644 --- a/op-mode-definitions/ipoe-server.xml +++ b/op-mode-definitions/ipoe-server.xml @@ -11,13 +11,13 @@ Show active IPoE server sessions - /usr/bin/accel-cmd '-p 2002 show sessions ifname,called-sid,calling-sid,ip,ip6,ip6-dp,rate-limit,state,uptime,sid' + /usr/bin/accel-cmd -p 2002 show sessions ifname,called-sid,calling-sid,ip,ip6,ip6-dp,rate-limit,state,uptime,sid Show IPoE server statistics - /usr/bin/accel-cmd '-p 2002 show stat' + /usr/bin/accel-cmd -p 2002 show stat -- cgit v1.2.3 From ceede38e249d16d16951a7af7b506ff8efeab5c2 Mon Sep 17 00:00:00 2001 From: Eshenko Dmitriy Date: Sat, 13 Jul 2019 20:49:12 +0300 Subject: fix typo Replace PPPoE to PPTP --- op-mode-definitions/pptp-server.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/pptp-server.xml b/op-mode-definitions/pptp-server.xml index 7f141dc53..388063885 100644 --- a/op-mode-definitions/pptp-server.xml +++ b/op-mode-definitions/pptp-server.xml @@ -9,7 +9,7 @@ - Show active PPPoE server sessions + Show active PPTP server sessions /usr/bin/accel-cmd -p 2003 'show sessions' -- 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 'op-mode-definitions') 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