diff options
author | Daniil Baturin <daniil@vyos.io> | 2020-03-09 19:24:00 +0200 |
---|---|---|
committer | Daniil Baturin <daniil@vyos.io> | 2020-03-09 19:24:00 +0200 |
commit | 806b944f62e675484a114f69be1fc80c1ec76337 (patch) | |
tree | fc9266a837a38c38dcfb79230727aed26a310aee | |
parent | f4000627dac973e1a2a001f8de2430cbd6a69e03 (diff) | |
parent | 6b4fb1820e740e6c7d63d7aba94fb2e0c7f5eded (diff) | |
download | vyos-1x-806b944f62e675484a114f69be1fc80c1ec76337.tar.gz vyos-1x-806b944f62e675484a114f69be1fc80c1ec76337.zip |
Merge branch 'crux' of github.com:vyos/vyos-1x into crux
-rw-r--r-- | Jenkinsfile | 30 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/dns-dynamic.xml | 57 | ||||
-rw-r--r-- | interface-definitions/pppoe-server.xml | 66 | ||||
-rw-r--r-- | interface-definitions/snmp.xml | 22 | ||||
-rw-r--r-- | op-mode-definitions/dhcp.xml | 36 | ||||
-rw-r--r-- | op-mode-definitions/igmp-proxy.xml | 13 | ||||
-rw-r--r-- | op-mode-definitions/pppoe-server.xml | 2 | ||||
-rwxr-xr-x | src/conf_mode/accel_pppoe.py | 119 | ||||
-rwxr-xr-x | src/conf_mode/igmp_proxy.py | 29 | ||||
-rwxr-xr-x | src/conf_mode/vrrp.py | 15 | ||||
-rwxr-xr-x | src/migration-scripts/quagga/3-to-4 | 76 | ||||
-rwxr-xr-x | src/op_mode/show_dhcp.py | 140 | ||||
-rwxr-xr-x | src/op_mode/show_dhcpv6.py | 61 |
14 files changed, 506 insertions, 161 deletions
diff --git a/Jenkinsfile b/Jenkinsfile index 515f5070e..225f4fce5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -69,11 +69,20 @@ def projectProperties = [ properties(projectProperties) setDescription() +node('Docker') { + stage('Define Agent') { + script { + // create container name on demand + env.DOCKER_IMAGE = "vyos/vyos-build:" + getGitBranchName() + } + } +} + pipeline { agent { docker { - args '--sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006' - image 'vyos/vyos-build:crux' + args "--sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006" + image "${env.DOCKER_IMAGE}" alwaysPull true } } @@ -88,7 +97,8 @@ pipeline { steps { script { dir('build') { - git branch: getGitBranchName(), url: getGitRepoURL() + git branch: getGitBranchName(), + url: getGitRepoURL() } } } @@ -97,7 +107,10 @@ pipeline { steps { script { dir('build') { - sh "dpkg-buildpackage -b -us -uc -tc" + def commitId = sh(returnStdout: true, script: 'git rev-parse --short=11 HEAD').trim() + currentBuild.description = sprintf('Git SHA1: %s', commitId[-11..-1]) + + sh 'dpkg-buildpackage -b -us -uc -tc' } } } @@ -117,9 +130,13 @@ pipeline { sshagent(['SSH-dev.packages.vyos.net']) { // build up some fancy groovy variables so we do not need to write/copy // every option over and over again! + def RELEASE = getGitBranchName() + if (getGitBranchName() == "master") { + RELEASE = 'current' + } - def VYOS_REPO_PATH = '/home/sentrium/web/dev.packages.vyos.net/public_html/repositories/' + getGitBranchName() + '/' - if (getGitBranchName() != "equuleus") + def VYOS_REPO_PATH = '/home/sentrium/web/dev.packages.vyos.net/public_html/repositories/' + RELEASE + '/' + if (getGitBranchName() == "crux") VYOS_REPO_PATH += 'vyos/' def SSH_OPTS = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR' @@ -129,7 +146,6 @@ pipeline { files = findFiles(glob: '*.deb') files.each { PACKAGE -> - def RELEASE = getGitBranchName() def ARCH = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Architecture").trim() def SUBSTRING = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Package").trim() def SSH_DIR = '~/VyOS/' + RELEASE + '/' + ARCH diff --git a/debian/control b/debian/control index 0bde10835..f239ec463 100644 --- a/debian/control +++ b/debian/control @@ -11,6 +11,7 @@ Build-Depends: debhelper (>= 9), python3-nose, python3-coverage, whois, + fakeroot, libvyosconfig0 Standards-Version: 3.9.6 diff --git a/interface-definitions/dns-dynamic.xml b/interface-definitions/dns-dynamic.xml index 8e7e77475..725ebadac 100644 --- a/interface-definitions/dns-dynamic.xml +++ b/interface-definitions/dns-dynamic.xml @@ -78,48 +78,52 @@ </valueHelp> <valueHelp> <format>afraid</format> - <description/> + <description>afraid.org Services</description> </valueHelp> <valueHelp> <format>changeip</format> - <description/> + <description>changeip.com Services</description> </valueHelp> <valueHelp> <format>cloudflare</format> - <description/> + <description>cloudflare.com Services</description> </valueHelp> <valueHelp> <format>dnspark</format> - <description/> + <description>dnspark.com Services</description> </valueHelp> <valueHelp> <format>dslreports</format> - <description/> + <description>dslreports.com Services</description> </valueHelp> <valueHelp> <format>dyndns</format> - <description/> + <description>dyndns.com Services</description> </valueHelp> <valueHelp> <format>easydns</format> - <description/> + <description>easydns.com Services</description> </valueHelp> <valueHelp> <format>namecheap</format> - <description/> + <description>namecheap.com Services</description> </valueHelp> <valueHelp> <format>noip</format> - <description/> + <description>noip.com Services</description> </valueHelp> <valueHelp> <format>sitelutions</format> - <description/> + <description>sitelutions.com Services</description> </valueHelp> <valueHelp> <format>zoneedit</format> - <description/> + <description>zoneedit.com Services</description> </valueHelp> + <constraint> + <regex>(custom|afraid|changeip|cloudflare|dnspark|dslreports|dyndns|easydns|namecheap|noip|sitelutions|zoneedit)</regex> + </constraint> + <constraintErrorMessage>Please choose from the list of allowed services</constraintErrorMessage> </properties> <children> <leafNode name="host-name"> @@ -141,50 +145,53 @@ <leafNode name="protocol"> <properties> <help>ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM]</help> - <valueHelp> - <format>protocol</format> - <description>ddclient protocol</description> - </valueHelp> + <completionHelp> + <list>changeip cloudflare dnspark dslreports1 dyndns2 easydns namecheap noip sitelutions zoneedit1</list> + </completionHelp> <valueHelp> <format>changeip</format> - <description/> + <description>changeip protocol</description> </valueHelp> <valueHelp> <format>cloudflare</format> - <description/> + <description>cloudflare protocol</description> </valueHelp> <valueHelp> <format>dnspark</format> - <description/> + <description>dnspark protocol</description> </valueHelp> <valueHelp> <format>dslreports1</format> - <description/> + <description>dslreports1 protocol</description> </valueHelp> <valueHelp> <format>dyndns2</format> - <description/> + <description>dyndns2 protocol</description> </valueHelp> <valueHelp> <format>easydns</format> - <description/> + <description>easydns protocol</description> </valueHelp> <valueHelp> <format>namecheap</format> - <description/> + <description>namecheap protocol</description> </valueHelp> <valueHelp> <format>noip</format> - <description/> + <description>noip protocol</description> </valueHelp> <valueHelp> <format>sitelutions</format> - <description/> + <description>sitelutions protocol</description> </valueHelp> <valueHelp> <format>zoneedit1</format> - <description/> + <description>zoneedit1 protocol</description> </valueHelp> + <constraint> + <regex>(changeip|cloudflare|dnspark|dslreports1|dyndns2|easydns|namecheap|noip|sitelutions|zoneedit1)</regex> + </constraint> + <constraintErrorMessage>Please choose from the list of allowed protocols</constraintErrorMessage> </properties> </leafNode> <leafNode name="server"> diff --git a/interface-definitions/pppoe-server.xml b/interface-definitions/pppoe-server.xml index ad4522679..18b0e649c 100644 --- a/interface-definitions/pppoe-server.xml +++ b/interface-definitions/pppoe-server.xml @@ -60,6 +60,29 @@ <help>Static client IP address</help> </properties> </leafNode> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> </children> @@ -161,6 +184,29 @@ </leafNode> </children> </node> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="attribute"> + <properties> + <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help> + </properties> + </leafNode> + <leafNode name="vendor"> + <properties> + <help>Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius)</help> + </properties> + </leafNode> + <leafNode name="enable"> + <properties> + <help>Enables Bandwidth shaping via RADIUS</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> </children> </node> </children> @@ -198,7 +244,6 @@ </leafNode> </children> </node> - <node name="client-ipv6-pool"> <properties> <help>Pool of client IPv6 addresses</help> @@ -343,25 +388,6 @@ </leafNode> </children> </node> - <node name="radius"> - <properties> - <help>RADIUS settings</help> - </properties> - <children> - <leafNode name="default-interim-interval"> - <properties> - <help>Default interim accounting interval</help> - <valueHelp> - <format>text</format> - <description>Use local username/password configuration</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 60-10000000"/> - </constraint> - </properties> - </leafNode> - </children> - </node> <leafNode name="service-name"> <properties> <help>Service name</help> diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index 821d1367d..fcc733c6c 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -585,16 +585,24 @@ <tagNode name="extension-name"> <properties> <help>Extension name</help> + <constraint> + <regex>^[a-z0-9\.\-\_]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> </properties> <children> <leafNode name="script"> - <properties> - <help>Script location and name</help> - <completionHelp> - <script>ls /config/user-data</script> - </completionHelp> - </properties> - </leafNode> + <properties> + <help>Script location and name</help> + <completionHelp> + <script>ls /config/user-data</script> + </completionHelp> + <constraint> + <regex>^[a-z0-9\.\-\_\/]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml index c284cf14b..f142cdd0e 100644 --- a/op-mode-definitions/dhcp.xml +++ b/op-mode-definitions/dhcp.xml @@ -9,7 +9,7 @@ <children> <node name="server"> <properties> - <help>Show DHCP information</help> + <help>Show DHCP server information</help> </properties> <children> <node name="leases"> @@ -20,9 +20,30 @@ <children> <tagNode name="pool"> <properties> - <help>Show DHCP leases for a specific pool</help> + <help>Show DHCP server leases for a specific pool</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6</command> + </tagNode> + <tagNode name="sort"> + <properties> + <help>Show DHCP server leases sorted by the specified key</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed sort</script> + </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $4</command> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --sort $6</command> + </tagNode> + <tagNode name="state"> + <properties> + <help>Show DHCP server leases with a specific state (can be multiple, comma-separated)</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed state</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --state $(echo $6 | tr , " ")</command> </tagNode> </children> </node> @@ -35,8 +56,11 @@ <tagNode name="pool"> <properties> <help>Show DHCP server statistics for a specific pool</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script> + </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $4</command> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6</command> </tagNode> </children> </node> @@ -80,12 +104,12 @@ </tagNode> <tagNode name="state"> <properties> - <help>Show DHCPv6 server leases with a specific state</help> + <help>Show DHCPv6 server leases with a specific state (can be multiple, comma-separated)</help> <completionHelp> <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed state</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $6</command> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $(echo $6 | tr , " ")</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/igmp-proxy.xml b/op-mode-definitions/igmp-proxy.xml new file mode 100644 index 000000000..8533138d7 --- /dev/null +++ b/op-mode-definitions/igmp-proxy.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="restart"> + <children> + <node name="igmp-proxy"> + <properties> + <help>Restart the IGMP proxy process</help> + </properties> + <command>sudo systemctl restart igmpproxy.service</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml index 7c0b05484..d8a518573 100644 --- a/op-mode-definitions/pppoe-server.xml +++ b/op-mode-definitions/pppoe-server.xml @@ -11,7 +11,7 @@ <properties> <help>Show active PPPoE server sessions</help> </properties> - <command>/usr/bin/accel-cmd 'show sessions'</command> + <command>/usr/bin/accel-cmd 'show sessions ifname,username,ip,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes'</command> </leafNode> <leafNode name="statistics"> <properties> diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 31f439c68..97e3723f0 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -54,7 +54,7 @@ auth_chap_md5 auth_mschap_v1 auth_mschap_v2 #pppd_compat -#shaper +shaper {% if snmp == 'enable' or snmp == 'enable-ma' %} net-snmp {% endif %} @@ -76,7 +76,7 @@ level=5 {% if snmp == 'enable-ma' %} [snmp] master=1 -{% endif %} +{% endif -%} [client-ip-range] disable @@ -101,24 +101,24 @@ gw-ip-address={{ppp_gw}} {% for prfx in client_ipv6_pool['delegate-prefix']: %} delegate={{prfx}} {% endfor %} -{% endif %} +{% endif -%} {% if dns %} [dns] {% if dns[0] %} dns1={{dns[0]}} -{% endif %} +{% endif -%} {% if dns[1] %} dns2={{dns[1]}} -{% endif %} -{% endif %} +{% endif -%} +{% endif -%} {% if dnsv6 %} [dnsv6] {% for srv in dnsv6: %} dns={{srv}} {% endfor %} -{% endif %} +{% endif -%} {% if wins %} [wins] @@ -127,13 +127,13 @@ wins1={{wins[0]}} {% endif %} {% if wins[1] %} wins2={{wins[1]}} -{% endif %} -{% endif %} +{% endif -%} +{% endif -%} {% if authentication['mode'] == 'local' %} [chap-secrets] chap-secrets=/etc/accel-ppp/pppoe/chap-secrets -{% endif %} +{% endif -%} {% if authentication['mode'] == 'radius' %} [radius] @@ -156,14 +156,23 @@ nas-identifier={{authentication['radiusopt']['nas-id']}} {% endif %} {% if authentication['radiusopt']['nas-ip'] %} nas-ip-address={{authentication['radiusopt']['nas-ip']}} -{% endif %} +{% endif -%} {% if authentication['radiusopt']['dae-srv'] %} dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ {{authentication['radiusopt']['dae-srv']['port']}},\ {{authentication['radiusopt']['dae-srv']['secret']}} -{% endif %} +{% endif -%} gw-ip-address={{ppp_gw}} verbose=1 + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} +{% endif -%} {% endif %} [ppp] @@ -245,11 +254,16 @@ tcp=127.0.0.1:2001 ### pppoe chap secrets chap_secrets_conf = ''' -# username server password acceptable local IP addresses +# username server password acceptable local IP addresses shaper {% for user in authentication['local-users'] %} {% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ +{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} +{% else %} {{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} {% endif %} +{% endif %} {% endfor %} ''' ### @@ -389,9 +403,11 @@ def get_config(): config_data['authentication']['local-users'].update( { usr : { - 'passwd' : '', - 'state' : 'enabled', - 'ip' : '*' + 'passwd' : None, + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None } } ) @@ -401,7 +417,11 @@ def get_config(): config_data['authentication']['local-users'][usr]['state'] = 'disable' if c.exists('authentication local-users username ' + usr + ' static-ip'): config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') - + if c.exists('authentication local-users username ' + usr + ' rate-limit download'): + config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') + if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): + config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') + ### authentication mode radius servers and settings if c.exists('authentication mode radius'): @@ -426,28 +446,42 @@ def get_config(): } ) - #### advanced radius-setting - if c.exists('authentication radius-settings'): - if c.exists('authentication radius-settings acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') - if c.exists('authentication radius-settings max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') - if c.exists('authentication radius-settings timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') - if c.exists('authentication radius-settings nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') - if c.exists('authentication radius-settings nas-ip-address'): - config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') - if c.exists('authentication radius-settings dae-server'): - config_data['authentication']['radiusopt'].update( - { - 'dae-srv' : { - 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), - 'port' : c.return_value('authentication radius-settings dae-server port'), - 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) - } + #### advanced radius-setting + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings dae-server'): + config_data['authentication']['radiusopt'].update( + { + 'dae-srv' : { + 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), + 'port' : c.return_value('authentication radius-settings dae-server port'), + 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) } - ) + } + ) + #### filter-id is the internal accel default if attribute is empty + #### set here as default for visibility which may change in the future + if c.exists('authentication radius-settings rate-limit enable'): + if not c.exists('authentication radius-settings rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : 'Filter-Id' + } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : c.return_value('authentication radius-settings rate-limit attribute') + } + if c.exists('authentication radius-settings rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') + if c.exists('mtu'): config_data['mtu'] = c.return_value('mtu') @@ -496,10 +530,17 @@ def verify(c): if c['authentication']['mode'] == 'local': if not c['authentication']['local-users']: raise ConfigError('pppoe-server authentication local-users required') - + for usr in c['authentication']['local-users']: if not c['authentication']['local-users'][usr]['passwd']: raise ConfigError('user ' + usr + ' requires a password') + ### if up/download is set, check that both have a value + if c['authentication']['local-users'][usr]['upload']: + if not c['authentication']['local-users'][usr]['download']: + raise ConfigError('user ' + usr + ' requires download speed value') + if c['authentication']['local-users'][usr]['download']: + if not c['authentication']['local-users'][usr]['upload']: + raise ConfigError('user ' + usr + ' requires upload speed value') if c['authentication']['mode'] == 'radius': if len(c['authentication']['radiussrv']) == 0: diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index b994369af..cd0704124 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -20,6 +20,7 @@ import sys import os import jinja2 +from netifaces import interfaces from vyos.config import Config from vyos import ConfigError @@ -50,17 +51,17 @@ config_tmpl = """ quickleave {% endif -%} -{% for i in interface %} -# Configuration for {{ i.interface }} ({{ i.role }} interface) -{% if i.role == 'disabled' -%} -phyint {{ i.interface }} disabled +{% for interface in interfaces %} +# Configuration for {{ interface.name }} ({{ interface.role }} interface) +{% if interface.role == 'disabled' -%} +phyint {{ interface.name }} disabled {%- else -%} -phyint {{ i.interface }} {{ i.role }} ratelimit 0 threshold {{ i.threshold }} +phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }} {%- endif -%} -{%- for subnet in i.alt_subnet %} +{%- for subnet in interface.alt_subnet %} altnet {{ subnet }} {%- endfor %} -{%- for subnet in i.whitelist %} +{%- for subnet in interface.whitelist %} whitelist {{ subnet }} {%- endfor %} {% endfor %} @@ -69,7 +70,7 @@ phyint {{ i.interface }} {{ i.role }} ratelimit 0 threshold {{ i.threshold }} default_config_data = { 'disable': False, 'disable_quickleave': False, - 'interface': [], + 'interfaces': [], } def get_config(): @@ -91,7 +92,7 @@ def get_config(): for intf in conf.list_nodes('interface'): conf.set_level('protocols igmp-proxy interface {0}'.format(intf)) interface = { - 'interface': intf, + 'name': intf, 'alt_subnet': [], 'role': 'downstream', 'threshold': '1', @@ -111,7 +112,7 @@ def get_config(): interface['whitelist'] = conf.return_values('whitelist') # Append interface configuration to global configuration list - igmp_proxy['interface'].append(interface) + igmp_proxy['interfaces'].append(interface) return igmp_proxy @@ -125,12 +126,14 @@ def verify(igmp_proxy): return None # at least two interfaces are required, one upstream and one downstream - if len(igmp_proxy['interface']) < 2: + if len(igmp_proxy['interfaces']) < 2: raise ConfigError('Must define an upstream and at least 1 downstream interface!') upstream = 0 - for i in igmp_proxy['interface']: - if "upstream" == i['role']: + for interface in igmp_proxy['interfaces']: + if interface['name'] not in interfaces(): + raise ConfigError('Interface "{}" does not exist'.format(interface['name'])) + if "upstream" == interface['role']: upstream += 1 if upstream == 0: diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index a08493309..04bce9d39 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -27,7 +27,7 @@ import vyos.keepalived from vyos import ConfigError - +daemon_file = "/etc/default/keepalived" config_file = "/etc/keepalived/keepalived.conf" config_tmpl = """ @@ -136,6 +136,14 @@ vrrp_sync_group {{ sync_group.name }} { """ +daemon_tmpl = """ +# Autogenerated by VyOS +# Options to pass to keepalived + +# DAEMON_ARGS are appended to the keepalived command-line +DAEMON_ARGS="--snmp" +""" + def get_config(): vrrp_groups = [] sync_groups = [] @@ -304,9 +312,12 @@ def generate(data): tmpl = jinja2.Template(config_tmpl) config_text = tmpl.render({"groups": vrrp_groups, "sync_groups": sync_groups}) - with open(config_file, 'w') as f: f.write(config_text) + + with open(daemon_file, 'w') as f: + f.write(daemon_tmpl) + return None def apply(data): diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 new file mode 100755 index 000000000..be3528391 --- /dev/null +++ b/src/migration-scripts/quagga/3-to-4 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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/>. +# +# + +# Between 1.2.3 and 1.2.4, FRR added per-neighbor enforce-first-as option. +# Unfortunately they also removed the global enforce-first-as option, +# which broke all old configs that used to have it. +# +# To emulate the effect of the original option, we insert it in every neighbor +# if the config used to have the original global option + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP enforce-first-as option is set + enforce_first_as_path = ['protocols', 'bgp', asn, 'parameters', 'enforce-first-as'] + if config.exists(enforce_first_as_path): + # Delete the obsolete option + config.delete(enforce_first_as_path) + + # Now insert it in every peer + peers = config.list_nodes(['protocols', 'bgp', asn, 'neighbor']) + for p in peers: + config.set(['protocols', 'bgp', asn, 'neighbor', p, 'enforce-first-as']) + else: + # Do nothing + sys.exit(0) + + # Save a new configuration file + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index 4c4ee6355..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["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S") + except: + data["start"] = "" + + try: + data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") + except: + data["end"] = "" + try: - data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + 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["expires"] = "" + 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,26 +99,54 @@ def get_lease_data(lease): return data -def get_leases(leases, state=None, pool=None): +def get_leases(leases, state, pool=None, sort='ip'): + # get leases from file leases = IscDhcpLeases(lease_file).get() - if state is not None: - leases = list(filter(lambda x: x.binding_state == 'active', leases)) + # 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: - 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) - return list(map(get_lease_data, leases)) + # should maybe filter all state=active by lease.valid here? -def show_leases(leases): - headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] + # 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 + # 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 lease: int(ipaddress.ip_address(lease['ip']))) + else: + leases = sorted(leases, key = lambda lease: lease[sort]) + + return leases + +def show_leases(leases): 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, lease_display_fields.values()) - output = tabulate.tabulate(lease_list, headers) - print(output) def get_pool_size(config, pool): @@ -85,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 @@ -101,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 = [] @@ -143,10 +214,10 @@ 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 + use_percentage = round(leases / size * 100) else: use_percentage = 0 @@ -163,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) |