From d654990111894c0f89ca4141862775ddd7a22b76 Mon Sep 17 00:00:00 2001 From: hagbard Date: Sat, 27 Apr 2019 18:40:32 -0700 Subject: [rsyslog] T1358 - typo fixed os.path.exists --- src/conf_mode/syslog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index c8541a4a0..105f35657 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -279,7 +279,7 @@ def verify(c): raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item) def apply(c): - if not os.path.exits('/var/run/rsyslogd.pid'): + if not os.path.exists('/var/run/rsyslogd.pid'): os.system("sudo systemctl start rsyslog >/dev/null") else: os.system("sudo systemctl restart rsyslog >/dev/null") -- cgit v1.2.3 From ae31b619c62eb7a2c086a7960fe4bf042dd79c39 Mon Sep 17 00:00:00 2001 From: UnicronNL Date: Fri, 3 May 2019 20:04:23 +0200 Subject: fix CVE-2019-10906 --- Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5aaef5675..eae5ea9fe 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -22,7 +22,7 @@ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" ], "index": "pypi", - "version": "==2.10" + "version": "==2.10.1" }, "markupsafe": { "hashes": [ -- cgit v1.2.3 From 09df82d3a4ff2e0912992cb5c73d5d9fce7070dd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 5 May 2019 14:55:35 +0200 Subject: [dhcp-server] T103: wrong hostnames in hosts file --- src/conf_mode/dhcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index c8ae2fa91..78927a847 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -186,7 +186,7 @@ shared-network {{ network.name }} { {%- endif -%} {%- for host in subnet.static_mapping %} {% if not host.disabled -%} - host {{ network.name }}_{{ host.name }} { + host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { fixed-address {{ host.ip_address }}; hardware ethernet {{ host.mac_address }}; {%- if host.static_parameters %} -- cgit v1.2.3 From 95b4b1e2ec1fc518e18406da638bdc0e025310d8 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 8 May 2019 00:32:52 +0200 Subject: [VRRP] T1371: add quotes around the health check script string. --- src/conf_mode/vrrp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index bc833b63f..85c6ad580 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -39,7 +39,7 @@ config_tmpl = """ {% if group.health_check_script -%} vrrp_script healthcheck_{{ group.name }} { - script {{ group.health_check_script }} + script "{{ group.health_check_script }}" interval {{ group.health_check_interval }} fall {{ group.health_check_count }} rise 1 -- cgit v1.2.3 From 4c070a73a3fac283f5ab8ca679bc1ac191565ef6 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 8 May 2019 10:48:57 -0500 Subject: T805: Drop config compatibility with Vyatta Core older than 6.5 Rewrite vyatta-config-migrate/migrate/system/6-to-7 in the canonical style and add to vyos-1x migration-scripts. This completes the collection of scripts needed to drop compatability with Vyatta Core older than 6.5. --- src/migration-scripts/system/6-to-7 | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100755 src/migration-scripts/system/6-to-7 diff --git a/src/migration-scripts/system/6-to-7 b/src/migration-scripts/system/6-to-7 new file mode 100755 index 000000000..bf07abf3a --- /dev/null +++ b/src/migration-scripts/system/6-to-7 @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +# Change smp_affinity to smp-affinity + +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) + +update_required = False + +intf_types = config.list_nodes(["interfaces"]) + +for intf_type in intf_types: + intf_type_path = ["interfaces", intf_type] + intfs = config.list_nodes(intf_type_path) + + for intf in intfs: + intf_path = intf_type_path + [intf] + if not config.exists(intf_path + ["smp_affinity"]): + # Nothing to do. + continue + else: + # Rename the node. + old_smp_affinity_path = intf_path + ["smp_affinity"] + config.rename(old_smp_affinity_path, "smp-affinity") + update_required = True + +if update_required: + 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) + + + -- cgit v1.2.3 From a8b5fae5581c03c5037c5fdc840be3e5bf984484 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 13 May 2019 22:00:42 +0200 Subject: T1378: extend version file with Git commit ID The Git commit ID will be crucial for the future when the full VyOS build can be reproduced by the one Git commit ID, thus start recording it in the version file. --- src/op_mode/version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/op_mode/version.py b/src/op_mode/version.py index ce3b3b54f..5aff0f767 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -51,7 +51,8 @@ version_output_tmpl = """ Version: VyOS {{version}} Built by: {{built_by}} Built on: {{built_on}} -Build ID: {{build_id}} +Build UUID: {{build_uuid}} +Build Commit ID: {{build_git}} Architecture: {{system_arch}} Boot via: {{boot_via}} -- cgit v1.2.3 From ae1c37284c9a91e1be934593812d3bd62e706d3b Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 21 May 2019 14:39:08 -0700 Subject: [pppoe-server] T1393 - pppoe IPv6 pool doesn't work --- src/conf_mode/accel_pppoe.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 3b3bf8cac..7a9878a1d 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -47,6 +47,8 @@ pppoe ippool {% if client_ipv6_pool %} ipv6pool +ipv6_nd +ipv6_dhcp {% endif %} chap-secrets auth_pap @@ -85,7 +87,7 @@ disable gw-ip-address={{ppp_gw}} {% if client_ip_pool %} {{client_ip_pool}} -{% endif -%} +{% endif %} {% if client_ip_subnets %} {% for sn in client_ip_subnets %} @@ -101,7 +103,7 @@ gw-ip-address={{ppp_gw}} {% for prfx in client_ipv6_pool['delegate-prefix']: %} delegate={{prfx}} {% endfor %} -{% endif -%} +{% endif %} {% if dns %} [dns] @@ -111,14 +113,14 @@ dns1={{dns[0]}} {% if dns[1] %} dns2={{dns[1]}} {% endif -%} -{% endif -%} +{% endif %} {% if dnsv6 %} -[dnsv6] +[ipv6-dns] {% for srv in dnsv6: %} -dns={{srv}} +{{srv}} {% endfor %} -{% endif -%} +{% endif %} {% if wins %} [wins] @@ -208,6 +210,10 @@ lcp-echo-failure=3 {% if ppp_options['ipv4'] %} ipv4={{ppp_options['ipv4']}} {% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + {% if ppp_options['ipv6'] %} ipv6={{ppp_options['ipv6']}} {% if ppp_options['ipv6-intf-id'] %} @@ -220,6 +226,7 @@ ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} {% endif %} {% endif %} + mtu={{mtu}} [pppoe] -- cgit v1.2.3 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(-) 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 29cb181b69b7156e3f2f29fb1e78999048a81a7f Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Wed, 22 May 2019 13:39:44 +0200 Subject: Create Jenkinsfile current --- Jenkinsfile | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..563ead229 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,119 @@ +pipeline { + agent none + stages { + stage('build-package') { + parallel { + stage('Build package amd64') { + agent { + docker { + label 'jessie-amd64' + args '--privileged --sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006 -v /tmp:/tmp' + image 'higebu/vyos-build:current' + } + + } + steps { + sh '''#!/bin/bash +git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER +cd $BUILD_NUMBER +sudo apt-get -o Acquire::Check-Valid-Until=false update +sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control +dpkg-buildpackage -b -us -uc -tc +mkdir -p /tmp/$GIT_BRANCH/packages/script +mv ../*.deb /tmp/$GIT_BRANCH/packages/''' + } + } + stage('Build package armhf') { + agent { + docker { + label 'jessie-amd64' + image 'vyos-build-armhf:current' + args '--privileged --sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006 -v /tmp:/tmp' + } + + } + steps { + sh '''#!/bin/bash +git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER +cd $BUILD_NUMBER +sudo apt-get -o Acquire::Check-Valid-Until=false update +sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control +dpkg-buildpackage -b -us -uc -tc +mkdir -p /tmp/$GIT_BRANCH/packages/script +mv ../*.deb /tmp/$GIT_BRANCH/packages/''' + } + } + stage('Build package arm64') { + agent { + docker { + label 'jessie-amd64' + args '--privileged --sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006 -v /tmp:/tmp' + image 'vyos-build-arm64:current' + } + + } + steps { + sh '''#!/bin/bash +git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER +cd $BUILD_NUMBER +sudo apt-get -o Acquire::Check-Valid-Until=false update +sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control +dpkg-buildpackage -b -us -uc -tc +mkdir -p /tmp/$GIT_BRANCH/packages/script +mv ../*.deb /tmp/$GIT_BRANCH/packages/''' + } + } + } + } + stage('Deploy packages') { + agent { + node { + label 'jessie-amd64' + } + + } + steps { + sh '''#!/bin/bash +cd /tmp/$GIT_BRANCH/packages/script +/var/lib/vyos-build/pkg-build.sh $GIT_BRANCH''' + } + } + stage('Cleanup') { + parallel { + stage('Cleanup amd64') { + agent { + node { + label 'jessie-amd64' + } + + } + steps { + cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, cleanupMatrixParent: true, deleteDirs: true, disableDeferredWipeout: true) + } + } + stage('Cleanup armhf') { + agent { + node { + label 'jessie-amd64' + } + + } + steps { + cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, cleanupMatrixParent: true, deleteDirs: true, disableDeferredWipeout: true) + } + } + stage('Cleanup arm64') { + agent { + node { + label 'jessie-amd64' + } + + } + steps { + cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, cleanupMatrixParent: true, deleteDirs: true, disableDeferredWipeout: true) + } + } + } + } + } +} -- cgit v1.2.3 From bf0f721432fa05bbc7058a0b43e2acf4ad1f30e3 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Wed, 22 May 2019 13:46:28 +0200 Subject: add tests to Jenkinsfile --- Jenkinsfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 563ead229..aac051799 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,6 +16,10 @@ pipeline { sh '''#!/bin/bash git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER cd $BUILD_NUMBER +sudo pip3 uninstall vyos -y || true +sudo pip3 install -r test-requirements.txt +python3 -m "pylint" src -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > pylint-report.txt || true +make test sudo apt-get -o Acquire::Check-Valid-Until=false update sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control dpkg-buildpackage -b -us -uc -tc @@ -36,6 +40,10 @@ mv ../*.deb /tmp/$GIT_BRANCH/packages/''' sh '''#!/bin/bash git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER cd $BUILD_NUMBER +sudo pip3 uninstall vyos -y || true +sudo pip3 install -r test-requirements.txt +python3 -m "pylint" src -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > pylint-report.txt || true +make test sudo apt-get -o Acquire::Check-Valid-Until=false update sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control dpkg-buildpackage -b -us -uc -tc @@ -56,6 +64,10 @@ mv ../*.deb /tmp/$GIT_BRANCH/packages/''' sh '''#!/bin/bash git clone --single-branch --branch $GIT_BRANCH $GIT_URL $BUILD_NUMBER cd $BUILD_NUMBER +sudo pip3 uninstall vyos -y || true +sudo pip3 install -r test-requirements.txt +python3 -m "pylint" src -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > pylint-report.txt || true +make test sudo apt-get -o Acquire::Check-Valid-Until=false update sudo mk-build-deps -i -r -t \'apt-get --no-install-recommends -yq\' debian/control dpkg-buildpackage -b -us -uc -tc -- cgit v1.2.3 From df231744b98202ec5fbcd236e795df7399747a0e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 23 May 2019 07:38:54 -0500 Subject: T1397: Rewrite the config merge script Add vyos.config.show_config to show working configuration. Add vyos.remote.get_config_remote() for obtaining remote config files. --- python/vyos/config.py | 15 ++++++ python/vyos/remote.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 python/vyos/remote.py diff --git a/python/vyos/config.py b/python/vyos/config.py index bcf04225b..9a5125eb9 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -169,6 +169,21 @@ class Config(object): except VyOSError: return False + def show_config(self, path='', default=None): + """ + Args: + path (str): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + try: + out = self._run(self._make_command('showConfig', path)) + return out + except VyOSError: + return(default) + def is_multi(self, path): """ Args: diff --git a/python/vyos/remote.py b/python/vyos/remote.py new file mode 100644 index 000000000..372780c91 --- /dev/null +++ b/python/vyos/remote.py @@ -0,0 +1,133 @@ +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import sys +import os +import re +import fileinput +import subprocess + + +def check_and_add_host_key(host_name): + """ + Filter host keys and prompt for adding key to known_hosts file, if + needed. + """ + known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME')) + + keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name) + + try: + host_key = subprocess.check_output(keyscan_cmd, shell=True, + stderr=subprocess.DEVNULL, + universal_newlines=True) + except subprocess.CalledProcessError as err: + sys.exit("Can not get RSA host key") + + # libssh2 (jessie; stretch) does not recognize ec host keys, and curl + # will fail with error 51 if present in known_hosts file; limit to rsa. + usable_keys = False + offending_keys = [] + for line in fileinput.input(known_hosts, inplace=True): + if host_name in line and 'ssh-rsa' in line: + if line.split()[-1] != host_key.split()[-1]: + offending_keys.append(line) + continue + else: + usable_keys = True + if host_name in line and not 'ssh-rsa' in line: + continue + + sys.stdout.write(line) + + if usable_keys: + return + + if offending_keys: + print("Host key has changed!") + print("If you trust the host key fingerprint below, continue.") + + fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key) + try: + fingerprint = subprocess.check_output(fingerprint_cmd, shell=True, + stderr=subprocess.DEVNULL, + universal_newlines=True) + except subprocess.CalledProcessError as err: + sys.exit("Can not get RSA host key fingerprint.") + + print("RSA host key fingerprint is {}".format(fingerprint.split()[1])) + response = input("Do you trust this host? [y]/n ") + + if not response or response == 'y': + with open(known_hosts, 'a+') as f: + print("Adding {} to the list of known" + " hosts.".format(host_name)) + f.write(host_key) + else: + sys.exit("Host not trusted") + +def get_remote_config(remote_file): + """ Invoke curl to download remote (config) file. + + Args: + remote file URI: + scp://[:]@/ + sftp://[:]@/ + http:/// + https:/// + ftp://[:]@/ + tftp:/// + """ + request = dict.fromkeys(['protocol', 'host', 'file', 'user', 'passwd']) + protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + or_protocols = '|'.join(protocols) + + request_match = re.match(r'(' + or_protocols + r')://(.*?)(/.*)', + remote_file) + if request_match: + (request['protocol'], request['host'], + request['file']) = request_match.groups() + else: + print("Malformed URI") + sys.exit(1) + + user_match = re.search(r'(.*)@(.*)', request['host']) + if user_match: + request['user'] = user_match.groups()[0] + request['host'] = user_match.groups()[1] + passwd_match = re.search(r'(.*):(.*)', request['user']) + if passwd_match: + # Deprectated in RFC 3986, but maintain for backward compatability. + request['user'] = passwd_match.groups()[0] + request['passwd'] = passwd_match.groups()[1] + + remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file']) + + if request['protocol'] in ('scp', 'sftp'): + check_and_add_host_key(request['host']) + + if request['user'] and not request['passwd']: + curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file) + else: + curl_cmd = 'curl -# {0}'.format(remote_file) + + config_file = None + try: + config_file = subprocess.check_output(curl_cmd, shell=True, + universal_newlines=True) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + + return config_file -- cgit v1.2.3 From 456abc2aa4ae10981c2aec2d2e6d975ef30fb8d6 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 28 May 2019 14:39:39 -0500 Subject: T1397: Rewrite the config merge script Add the script vyos-merge-config.py to separate the merge function from the config load script and remove dependency on XorpConfigParser. --- python/vyos/defaults.py | 3 +- src/helpers/vyos-merge-config.py | 96 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100755 src/helpers/vyos-merge-config.py diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 36185f16a..0603efc42 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -15,7 +15,8 @@ directories = { - "data": "/usr/share/vyos/" + "data": "/usr/share/vyos/", + "config": "/opt/vyatta/etc/config" } cfg_group = 'vyattacfg' diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py new file mode 100755 index 000000000..f0d5d1595 --- /dev/null +++ b/src/helpers/vyos-merge-config.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import sys +import os +import subprocess +import vyos.defaults +import vyos.remote +from vyos.config import Config +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 2): + print("Need config file name to merge.") + print("Usage: merge [config path]") + sys.exit(0) + +file_name = sys.argv[1] + +configdir = vyos.defaults.directories['config'] + +protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + +if any(x in file_name for x in protocols): + config_file = vyos.remote.get_remote_config(file_name) + if not config_file: + sys.exit("No config file by that name.") +else: + canonical_path = "{0}/{1}".format(configdir, file_name) + first_err = None + try: + with open(canonical_path, 'r') as f: + config_file = f.read() + except Exception as err: + first_err = err + try: + with open(file_name, 'r') as f: + config_file = f.read() + except Exception as err: + print(first_err) + print(err) + sys.exit(1) + +path = None +if (len(sys.argv) > 2): + path = " ".join(sys.argv[2:]) + +merge_config_tree = ConfigTree(config_file) + +effective_config = Config() + +output_effective_config = effective_config.show_config() +effective_config_tree = ConfigTree(output_effective_config) + +effective_cmds = effective_config_tree.to_commands() +merge_cmds = merge_config_tree.to_commands() + +effective_cmd_list = effective_cmds.splitlines() +merge_cmd_list = merge_cmds.splitlines() + +effective_cmd_set = set(effective_cmd_list) +add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] + +if path: + if not effective_config.exists(path): + print("path {} does not exist in running config; will use " + "root.".format(path)) + else: + add_cmds = [ cmd for cmd in add_cmds if path in cmd ] + +for cmd in add_cmds: + cmd = "/opt/vyatta/sbin/my_" + cmd + + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + +if effective_config.session_changed(): + print("Merge complete. Use 'commit' to make changes effective.") +else: + print("No configuration changes to commit.") -- 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(-) 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 a0b43b1dfd99e07258156fdd6831e8c7b748858b Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sat, 1 Jun 2019 06:21:53 +0200 Subject: T1422: add a script for querying values in config files. --- src/utils/vyos-config-file-query | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/utils/vyos-config-file-query diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query new file mode 100644 index 000000000..16c4c49bf --- /dev/null +++ b/src/utils/vyos-config-file-query @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 re +import os +import sys +import json +import argparse + +import vyos.configtree + + +arg_parser = argparse.ArgumentParser() +arg_parser.add_argument('-p', '--path', type=str, + help="VyOS config node, e.g. \"system config-management commit-revisions\"", required=True) +arg_parser.add_argument('-f', '--file', type=str, help="VyOS config file, e.g. /config/config.boot", required=True) + +arg_parser.add_argument('-s', '--separator', type=str, default=' ', help="Value separator for the plain format") +arg_parser.add_argument('-j', '--json', action='store_true') + +op_group = arg_parser.add_mutually_exclusive_group(required=True) +op_group.add_argument('--return-value', action='store_true', help="Return a single node value") +op_group.add_argument('--return-values', action='store_true', help="Return all values of a multi-value node") +op_group.add_argument('--list-nodes', action='store_true', help="List children of a node") +op_group.add_argument('--exists', action='store_true', help="Check if a node exists") + +args = arg_parser.parse_args() + + +try: + with open(args.file, 'r') as f: + config_file = f.read() +except OSError as e: + print("Could not read the config file: {0}".format(e)) + sys.exit(1) + +try: + config = vyos.configtree.ConfigTree(config_file) +except Exception as e: + print(e) + sys.exit(1) + + +path = re.split(r'\s+', args.path) +values = None + +if args.exists: + if config.exists(path): + sys.exit(0) + else: + sys.exit(1) +elif args.return_value: + try: + values = [config.return_value(path)] + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.return_values: + try: + values = config.return_values(path) + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.list_nodes: + values = config.list_nodes(path) + if not values: + values = [] +else: + # Can't happen + print("Operation required") + sys.exit(1) + + +if values: + if args.json: + print(json.dumps(values)) + else: + if len(values) == 1: + print("one") + print(values[0]) + else: + # XXX: assuming values never contain quotes + values = list(map(lambda s: "\'{0}\'".format(s), values)) + values_str = args.separator.join(values) + print(values_str) + +sys.exit(0) -- cgit v1.2.3 From 0b946bb9342d7784d9a271d02c05479419201dfb Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sat, 1 Jun 2019 06:35:27 +0200 Subject: T1422: fix wrong file mode. --- src/utils/vyos-config-file-query | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/utils/vyos-config-file-query diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query old mode 100644 new mode 100755 -- cgit v1.2.3 From 2d13d5741953a82ba1b232dd3e1c9efb98ec43a6 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 3 Jun 2019 10:37:35 -0500 Subject: T1423: Create known_hosts file if not present In the recent rewrite of the config merge script, support for merging remote config files checks and adds the host key in known_hosts; however, this function fails if known_hosts is not present. Fix. --- python/vyos/remote.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/vyos/remote.py b/python/vyos/remote.py index 372780c91..49936ec08 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -26,6 +26,9 @@ def check_and_add_host_key(host_name): needed. """ known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME')) + if not os.path.exists(known_hosts): + mode = 0o600 + os.mknod(known_hosts, 0o600) keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name) -- cgit v1.2.3 From 0cdf63668d5df74d58d8eb5a155cdf2d4693c9cf Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Tue, 4 Jun 2019 15:22:39 +0200 Subject: T1379: Deprecated functions in /sbin/dhclient-script --- src/conf_mode/dns_forwarding.py | 64 +++++--- src/conf_mode/host_name.py | 321 +++++++++++++++++++++------------------- 2 files changed, 210 insertions(+), 175 deletions(-) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 135f6fec0..7559a0af6 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -19,12 +19,18 @@ import sys import os -import netifaces +import argparse import jinja2 +import netifaces from vyos.config import Config from vyos import ConfigError + +parser = argparse.ArgumentParser() +parser.add_argument("--dhclient", action="store_true", + help="Started from dhclient-script") + config_file = r'/etc/powerdns/recursor.conf' # XXX: pdns recursor doesn't like whitespace near entry separators, @@ -84,31 +90,36 @@ default_config_data = { 'name_servers': [], 'negative_ttl': 3600, 'domains': [], - 'dnssec' : 'process-no-validate' + 'dnssec': 'process-no-validate' } # borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! def get_resolvers(file): - resolvers = [] try: with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'nameserver' in line: - resolvers.append(line.split()[1]) + lines = [line.split('#', 1)[0].rstrip() + for line in resolvconf.readlines()] + resolvers = [line.split()[1] + for line in lines if 'nameserver' in line] return resolvers except IOError: return [] -def get_config(): + +def get_config(arguments): dns = default_config_data conf = Config() + + if arguments.dhclient: + conf.exists = conf.exists_effective + conf.return_value = conf.return_effective_value + conf.return_values = conf.return_effective_values + if not conf.exists('service dns forwarding'): return None - else: - conf.set_level('service dns forwarding') + + conf.set_level('service dns forwarding') if conf.exists('cache-size'): cache_size = conf.return_value('cache-size') @@ -139,7 +150,8 @@ def get_config(): system_name_servers = [] system_name_servers = conf.return_values('name-server') if not system_name_servers: - print("DNS forwarding warning: No name-servers set under 'system name-server'\n") + print( + "DNS forwarding warning: No name-servers set under 'system name-server'\n") else: dns['name_servers'] = dns['name_servers'] + system_name_servers conf.set_level('service dns forwarding') @@ -171,9 +183,10 @@ def get_config(): try: addrs = netifaces.ifaddresses(interface) except ValueError: - print("WARNING: interface {0} does not exist".format(interface)) + print( + "WARNING: interface {0} does not exist".format(interface)) continue - + if netifaces.AF_INET in addrs.keys(): for ip4 in addrs[netifaces.AF_INET]: listen4.append(ip4['addr']) @@ -183,7 +196,8 @@ def get_config(): listen6.append(ip6['addr']) if (not listen4) and (not (listen6)): - print("WARNING: interface {0} has no configured addresses".format(interface)) + print( + "WARNING: interface {0} has no configured addresses".format(interface)) dns['listen_on'] = dns['listen_on'] + listen4 + listen6 @@ -195,31 +209,37 @@ def get_config(): interfaces = [] interfaces = conf.return_values('dhcp') for interface in interfaces: - dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface)) + dhcp_resolvers = get_resolvers( + "/etc/resolv.conf.dhclient-new-{0}".format(interface)) if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers return dns + def bracketize_ipv6_addrs(addrs): """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] + def verify(dns): # bail out early - looks like removal from running config if dns is None: return None if not dns['listen_on']: - raise ConfigError("Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + raise ConfigError( + "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") if dns['domains']: for domain in dns['domains']: if not domain['servers']: - raise ConfigError('Error: No server configured for domain {0}'.format(domain['name'])) + raise ConfigError( + 'Error: No server configured for domain {0}'.format(domain['name'])) return None + def generate(dns): # bail out early - looks like removal from running config if dns is None: @@ -232,19 +252,21 @@ def generate(dns): f.write(config_text) return None + def apply(dns): if dns is not None: os.system("systemctl restart pdns-recursor") else: # DNS forwarding is removed in the commit os.system("systemctl stop pdns-recursor") - os.unlink(config_file) + if os.path.isfile(config_file): + os.unlink(config_file) - return None if __name__ == '__main__': + args = parser.parse_args() try: - c = get_config() + c = get_config(args) verify(c) generate(c) apply(c) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 81a52e87f..621ccd7e0 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -23,14 +23,19 @@ conf-mode script for 'system host-name' and 'system domain-name'. import os import re import sys -import subprocess import copy -import jinja2 import glob +import argparse +import jinja2 from vyos.config import Config from vyos import ConfigError + +parser = argparse.ArgumentParser() +parser.add_argument("--dhclient", action="store_true", + help="Started from dhclient-script") + config_file_hosts = '/etc/hosts' config_file_resolv = '/etc/resolv.conf' @@ -72,32 +77,6 @@ search {{ domain_search | join(" ") }} """ -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - resolvers = [] - try: - with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'nameserver' in line: - resolvers.append(line.split()[1]) - return resolvers - except IOError: - return [] - -def get_dhcp_search_doms(file): - search_doms = [] - try: - with open(file, 'r') as resolvconf: - for line in resolvconf.readlines(): - line = line.split('#',1)[0]; - line = line.rstrip(); - if 'search' in line: - return re.sub('^search','',line).lstrip().split() - except IOError: - return [] - default_config_data = { 'hostname': 'vyos', 'domain_name': '', @@ -106,158 +85,192 @@ default_config_data = { 'no_dhcp_ns': False } -def get_config(): - conf = Config() - hosts = copy.deepcopy(default_config_data) +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolv = {} + try: + with open(file, 'r') as resolvconf: + lines = [line.split('#', 1)[0].rstrip() + for line in resolvconf.readlines()] + resolvers = [line.split()[1] + for line in lines if 'nameserver' in line] + domains = [line.split()[1] for line in lines if 'search' in line] + resolv['resolvers'] = resolvers + resolv['domains'] = domains + return resolv + except IOError: + return [] - if conf.exists("system host-name"): - hosts['hostname'] = conf.return_value("system host-name") - if conf.exists("system domain-name"): - hosts['domain_name'] = conf.return_value("system domain-name") - hosts['domain_search'].append(hosts['domain_name']) +def get_config(arguments): + conf = Config() + hosts = copy.deepcopy(default_config_data) - for search in conf.return_values("system domain-search domain"): - hosts['domain_search'].append(search) + if arguments.dhclient: + conf.exists = conf.exists_effective + conf.return_value = conf.return_effective_value + conf.return_values = conf.return_effective_values - if conf.exists("system name-server"): - hosts['nameserver'] = conf.return_values("system name-server") + if conf.exists("system host-name"): + hosts['hostname'] = conf.return_value("system host-name") - if conf.exists("system disable-dhcp-nameservers"): - hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') + if conf.exists("system domain-name"): + hosts['domain_name'] = conf.return_value("system domain-name") + hosts['domain_search'].append(hosts['domain_name']) - ## system static-host-mapping - hosts['static_host_mapping'] = { 'hostnames' : {}} + for search in conf.return_values("system domain-search domain"): + hosts['domain_search'].append(search) - if conf.exists('system static-host-mapping host-name'): - for hn in conf.list_nodes('system static-host-mapping host-name'): - hosts['static_host_mapping']['hostnames'][hn] = { - 'ipaddr' : conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), - 'alias' : '' - } - - if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): - a = conf.return_values('system static-host-mapping host-name ' + hn + ' alias') - hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join( conf.return_values('system static-host-mapping host-name ' + hn + ' alias') ) + if conf.exists("system name-server"): + hosts['nameserver'] = conf.return_values("system name-server") - return hosts + if conf.exists("system disable-dhcp-nameservers"): + hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') -def verify(config): - if config is None: - return None + # system static-host-mapping + hosts['static_host_mapping'] = {'hostnames': {}} - # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" - hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") - if not hostname_regex.match(config['hostname']): - raise ConfigError('Invalid host name ' + config["hostname"]) + if conf.exists('system static-host-mapping host-name'): + for hn in conf.list_nodes('system static-host-mapping host-name'): + hosts['static_host_mapping']['hostnames'][hn] = { + 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), + 'alias': '' + } - # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" - length = len(config['hostname']) - if length < 1 or length > 63: - raise ConfigError('Invalid host-name length, must be less than 63 characters') + if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): + a = conf.return_values( + 'system static-host-mapping host-name ' + hn + ' alias') + hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a) - # The search list is currently limited to six domains with a total of 256 characters. - # https://linux.die.net/man/5/resolv.conf - if len(config['domain_search']) > 6: - raise ConfigError('The search list is currently limited to six domains') + return hosts - tmp = ' '.join(config['domain_search']) - if len(tmp) > 256: - raise ConfigError('The search list is currently limited to 256 characters') - # static mappings alias hostname - if config['static_host_mapping']['hostnames']: - for hn in config['static_host_mapping']['hostnames']: - if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: - raise ConfigError('IP address required for ' + hn) - for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): - if not hostname_regex.match(hn_alias) and len (hn_alias) !=0: - raise ConfigError('Invalid hostname alias ' + hn_alias) +def verify(config): + if config is None: + return None + + # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" + hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") + if not hostname_regex.match(config['hostname']): + raise ConfigError('Invalid host name ' + config["hostname"]) + + # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" + length = len(config['hostname']) + if length < 1 or length > 63: + raise ConfigError( + 'Invalid host-name length, must be less than 63 characters') + + # The search list is currently limited to six domains with a total of 256 characters. + # https://linux.die.net/man/5/resolv.conf + if len(config['domain_search']) > 6: + raise ConfigError( + 'The search list is currently limited to six domains') + + tmp = ' '.join(config['domain_search']) + if len(tmp) > 256: + raise ConfigError( + 'The search list is currently limited to 256 characters') + + # static mappings alias hostname + if config['static_host_mapping']['hostnames']: + for hn in config['static_host_mapping']['hostnames']: + if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: + raise ConfigError('IP address required for ' + hn) + for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): + if not hostname_regex.match(hn_alias) and len(hn_alias) != 0: + raise ConfigError('Invalid hostname alias ' + hn_alias) + + return None - return None def generate(config): - if config is None: + if config is None: + return None + + # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers + # received via dhclient should not be added into the final 'resolv.conf'. + # + # We iterate over every resolver file and retrieve the received nameservers + # for later adjustment of the system nameservers + dhcp_ns = [] + dhcp_sd = [] + for file in glob.glob('/etc/resolv.conf.dhclient-new*'): + for key, value in get_resolvers(file).items(): + ns = [r for r in value if key == 'resolvers'] + dhcp_ns.extend(ns) + sd = [d for d in value if key == 'domains'] + dhcp_sd.extend(sd) + + if not config['no_dhcp_ns']: + config['nameserver'] += dhcp_ns + config['domain_search'] += dhcp_sd + + # We have third party scripts altering /etc/hosts, too. + # One example are the DHCP hostname update scripts thus we need to cache in + # every modification first - so changing domain-name, domain-search or hostname + # during runtime works + old_hosts = "" + with open(config_file_hosts, 'r') as f: + # Skips text before the beginning of our marker. + # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts + for line in f: + if line.strip() == '### modifications from other scripts should be added below': + break + + for line in f: + # This additional line.strip() filters empty lines + if line.strip(): + old_hosts += line + + # Add an additional newline + old_hosts += '\n' + + tmpl = jinja2.Template(config_tmpl_hosts) + config_text = tmpl.render(config) + + with open(config_file_hosts, 'w') as f: + f.write(config_text) + f.write(old_hosts) + + tmpl = jinja2.Template(config_tmpl_resolv) + config_text = tmpl.render(config) + with open(config_file_resolv, 'w') as f: + f.write(config_text) + return None - # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers - # received via dhclient should not be added into the final 'resolv.conf'. - # - # We iterate over every resolver file and retrieve the received nameservers - # for later adjustment of the system nameservers - dhcp_ns = [] - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - for r in get_resolvers(file): - dhcp_ns.append(r) - - if not config['no_dhcp_ns']: - config['nameserver'] += dhcp_ns - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - config['domain_search'] = get_dhcp_search_doms(file) - - # We have third party scripts altering /etc/hosts, too. - # One example are the DHCP hostname update scripts thus we need to cache in - # every modification first - so changing domain-name, domain-search or hostname - # during runtime works - old_hosts = "" - with open(config_file_hosts, 'r') as f: - # Skips text before the beginning of our marker. - # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts - for line in f: - if line.strip() == '### modifications from other scripts should be added below': - break - - for line in f: - # This additional line.strip() filters empty lines - if line.strip(): - old_hosts += line - - # Add an additional newline - old_hosts += '\n' - - tmpl = jinja2.Template(config_tmpl_hosts) - config_text = tmpl.render(config) - - with open(config_file_hosts, 'w') as f: - f.write(config_text) - f.write(old_hosts) - - tmpl = jinja2.Template(config_tmpl_resolv) - config_text = tmpl.render(config) - with open(config_file_resolv, 'w') as f: - f.write(config_text) - - return None def apply(config): - if config is None: - return None + if config is None: + return None - fqdn = config['hostname'] - if config['domain_name']: - fqdn += '.' + config['domain_name'] + fqdn = config['hostname'] + if config['domain_name']: + fqdn += '.' + config['domain_name'] - os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) + os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) - # Restart services that use the hostname - os.system("systemctl restart rsyslog.service") + # Restart services that use the hostname + os.system("systemctl restart rsyslog.service") - # If SNMP is running, restart it too - if os.system("pgrep snmpd > /dev/null") == 0: - os.system("systemctl restart snmpd.service") + # If SNMP is running, restart it too + if os.system("pgrep snmpd > /dev/null") == 0: + os.system("systemctl restart snmpd.service") - # restart pdns if it is used - if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: - os.system("/etc/init.d/pdns-recursor restart >/dev/null") + # restart pdns if it is used + if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: + os.system("/etc/init.d/pdns-recursor restart >/dev/null") + + return None - return None if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + args = parser.parse_args() + try: + c = get_config(args) + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 1483288278060c62904ba1c9984aae841600e2f5 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 5 Jun 2019 08:45:36 -0500 Subject: T1422: Remove extraneous print statement. --- src/utils/vyos-config-file-query | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query index 16c4c49bf..a10c7e9b3 100755 --- a/src/utils/vyos-config-file-query +++ b/src/utils/vyos-config-file-query @@ -90,7 +90,6 @@ if values: print(json.dumps(values)) else: if len(values) == 1: - print("one") print(values[0]) else: # XXX: assuming values never contain quotes -- cgit v1.2.3 From 6763170830010c8cea2f17daee5f46b9203dab56 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 30 May 2019 13:46:08 -0500 Subject: T1334: Migration script runner rewrite Python script and support code to replace the vyatta_config_migrate.pl script. --- python/vyos/defaults.py | 6 +- python/vyos/formatversions.py | 109 +++++++++++++++++++++ python/vyos/migrator.py | 190 ++++++++++++++++++++++++++++++++++++ python/vyos/systemversions.py | 39 ++++++++ src/helpers/run-config-migration.py | 83 ++++++++++++++++ src/helpers/system-versions-foot.py | 39 ++++++++ 6 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 python/vyos/formatversions.py create mode 100644 python/vyos/migrator.py create mode 100644 python/vyos/systemversions.py create mode 100755 src/helpers/run-config-migration.py create mode 100755 src/helpers/system-versions-foot.py diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 0603efc42..da363b8e1 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -16,7 +16,11 @@ directories = { "data": "/usr/share/vyos/", - "config": "/opt/vyatta/etc/config" + "config": "/opt/vyatta/etc/config", + "current": "/opt/vyatta/etc/config-migrate/current", + "migrate": "/opt/vyatta/etc/config-migrate/migrate", } cfg_group = 'vyattacfg' + +cfg_vintage = 'vyatta' diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py new file mode 100644 index 000000000..29117a5d3 --- /dev/null +++ b/python/vyos/formatversions.py @@ -0,0 +1,109 @@ +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +import sys +import os +import re +import fileinput + +def read_vyatta_versions(config_file): + config_file_versions = {} + + with open(config_file, 'r') as config_file_handle: + for config_line in config_file_handle: + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line): + if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line): + raise ValueError("malformed configuration string: " + "{}".format(config_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): + config_file_versions[pair[0]] = int(pair[1]) + + + return config_file_versions + +def read_vyos_versions(config_file): + config_file_versions = {} + + with open(config_file, 'r') as config_file_handle: + for config_line in config_file_handle: + if re.match(r'// vyos-config-version:.+', config_line): + if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line): + raise ValueError("malformed configuration string: " + "{}".format(config_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): + config_file_versions[pair[0]] = int(pair[1]) + + return config_file_versions + +def remove_versions(config_file): + """ + Remove old version string. + """ + for line in fileinput.input(config_file, inplace=True): + if re.match(r'/\* Warning:.+ \*/$', line): + continue + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line): + continue + if re.match(r'/\* Release version:.+ \*/$', line): + continue + if re.match('// vyos-config-version:.+', line): + continue + if re.match('// Warning:.+', line): + continue + if re.match('// Release version:.+', line): + continue + sys.stdout.write(line) + +def format_versions_string(config_versions): + cfg_keys = list(config_versions.keys()) + cfg_keys.sort() + + component_version_strings = [] + + for key in cfg_keys: + cfg_vers = config_versions[key] + component_version_strings.append('{}@{}'.format(key, cfg_vers)) + + separator = ":" + component_version_string = separator.join(component_version_strings) + + return component_version_string + +def write_vyatta_versions_foot(config_file, component_version_string, + os_version_string): + if config_file: + with open(config_file, 'a') as config_file_handle: + config_file_handle.write('/* Warning: Do not remove the following line. */\n') + config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) + config_file_handle.write('/* Release version: {} */\n'.format(os_version_string)) + else: + sys.stdout.write('/* Warning: Do not remove the following line. */\n') + sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) + sys.stdout.write('/* Release version: {} */\n'.format(os_version_string)) + +def write_vyos_versions_foot(config_file, component_version_string, + os_version_string): + if config_file: + with open(config_file, 'a') as config_file_handle: + config_file_handle.write('// Warning: Do not remove the following line.\n') + config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string)) + config_file_handle.write('// Release version: {}\n'.format(os_version_string)) + else: + sys.stdout.write('// Warning: Do not remove the following line.\n') + sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string)) + sys.stdout.write('// Release version: {}\n'.format(os_version_string)) + diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py new file mode 100644 index 000000000..2d4bc7ffc --- /dev/null +++ b/python/vyos/migrator.py @@ -0,0 +1,190 @@ +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +import sys +import os +import subprocess +import vyos.version +import vyos.defaults +import vyos.systemversions as systemversions +import vyos.formatversions as formatversions + +class MigratorError(Exception): + pass + +class Migrator(object): + def __init__(self, config_file, force=False, set_vintage=None): + self._config_file = config_file + self._force = force + self._set_vintage = set_vintage + self._config_file_vintage = None + self._changed = False + + def read_config_file_versions(self): + """ + Get component versions from config file footer and set vintage; + return empty dictionary if config string is missing. + """ + cfg_file = self._config_file + component_versions = {} + + cfg_versions = formatversions.read_vyatta_versions(cfg_file) + + if cfg_versions: + self._config_file_vintage = 'vyatta' + component_versions = cfg_versions + + cfg_versions = formatversions.read_vyos_versions(cfg_file) + + if cfg_versions: + self._config_file_vintage = 'vyos' + component_versions = cfg_versions + + return component_versions + + def update_vintage(self): + old_vintage = self._config_file_vintage + + if self._set_vintage: + self._config_file_vintage = self._set_vintage + + if not self._config_file_vintage: + self._config_file_vintage = vyos.defaults.cfg_vintage + + if self._config_file_vintage not in ['vyatta', 'vyos']: + raise MigratorError("Unknown vintage.") + + if self._config_file_vintage == old_vintage: + return False + else: + return True + + def run_migration_scripts(self, config_file_versions, system_versions): + """ + Run migration scripts iteratively, until config file version equals + system component version. + """ + cfg_versions = config_file_versions + sys_versions = system_versions + + sys_keys = list(sys_versions.keys()) + sys_keys.sort() + + rev_versions = {} + + for key in sys_keys: + sys_ver = sys_versions[key] + if key in cfg_versions: + cfg_ver = cfg_versions[key] + else: + cfg_ver = 0 + + migrate_script_dir = os.path.join( + vyos.defaults.directories['migrate'], key) + + while cfg_ver < sys_ver: + next_ver = cfg_ver + 1 + + migrate_script = os.path.join(migrate_script_dir, + '{}-to-{}'.format(cfg_ver, next_ver)) + + try: + subprocess.check_output([migrate_script, + self._config_file]) + except FileNotFoundError: + pass + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + sys.exit(1) + + cfg_ver = next_ver + + rev_versions[key] = cfg_ver + + return rev_versions + + def write_config_file_versions(self, cfg_versions): + """ + Write new versions string. + """ + versions_string = formatversions.format_versions_string(cfg_versions) + + os_version_string = vyos.version.get_version() + + if self._config_file_vintage == 'vyatta': + formatversions.write_vyatta_versions_foot(self._config_file, + versions_string, + os_version_string) + + if self._config_file_vintage == 'vyos': + formatversions.write_vyos_versions_foot(self._config_file, + versions_string, + os_version_string) + + def run(self): + """ + Gather component versions from config file and system. + Run migration scripts. + Update vintage ('vyatta' or 'vyos'), if needed. + If changed, remove old versions string from config file, and + write new versions string. + """ + cfg_file = self._config_file + + cfg_versions = self.read_config_file_versions() + if self._force: + # This will force calling all migration scripts: + cfg_versions = {} + + sys_versions = systemversions.get_system_versions() + + rev_versions = self.run_migration_scripts(cfg_versions, sys_versions) + + if rev_versions != cfg_versions: + self._changed = True + + if self.update_vintage(): + self._changed = True + + if not self._changed: + return + + formatversions.remove_versions(cfg_file) + + self.write_config_file_versions(rev_versions) + + +class VirtualMigrator(Migrator): + def __init__(self, config_file, vintage='vyos'): + super().__init__(config_file, set_vintage = vintage) + + def run(self): + cfg_file = self._config_file + + cfg_versions = self.read_config_file_versions() + if not cfg_versions: + raise MigratorError("Config file has no version information;" + " virtual migration not possible.") + + if self.update_vintage(): + self._changed = True + + if not self._changed: + return + + formatversions.remove_versions(cfg_file) + + self.write_config_file_versions(cfg_versions) + diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py new file mode 100644 index 000000000..9b3f4f413 --- /dev/null +++ b/python/vyos/systemversions.py @@ -0,0 +1,39 @@ +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +import os +import re +import sys +import vyos.defaults + +def get_system_versions(): + """ + Get component versions from running system; critical failure if + unable to read migration directory. + """ + system_versions = {} + + try: + version_info = os.listdir(vyos.defaults.directories['current']) + except OSError as err: + print("OS error: {}".format(err)) + sys.exit(1) + + for info in version_info: + if re.match(r'[\w,-]+@\d+', info): + pair = info.split('@') + system_versions[pair[0]] = int(pair[1]) + + return system_versions diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py new file mode 100755 index 000000000..a57a19cdf --- /dev/null +++ b/src/helpers/run-config-migration.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +import os +import sys +import argparse +import datetime +import subprocess +from vyos.migrator import Migrator, VirtualMigrator + +def main(): + argparser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter) + argparser.add_argument('config_file', type=str, + help="configuration file to migrate") + argparser.add_argument('--force', action='store_true', + help="Force calling of all migration scripts.") + argparser.add_argument('--set-vintage', type=str, + choices=['vyatta', 'vyos'], + help="Set the format for the config version footer in config" + " file:\n" + "set to 'vyatta':\n" + "(for '/* === vyatta-config-version ... */' format)\n" + "or 'vyos':\n" + "(for '// vyos-config-version ...' format).") + argparser.add_argument('--virtual', action='store_true', + help="Update the format of the trailing comments in" + " config file,\nfrom 'vyatta' to 'vyos'; no migration" + " scripts are run.") + args = argparser.parse_args() + + config_file_name = args.config_file + force_on = args.force + vintage = args.set_vintage + virtual = args.virtual + + if not os.access(config_file_name, os.R_OK): + print("Read error: {}.".format(config_file_name)) + sys.exit(1) + + if not os.access(config_file_name, os.W_OK): + print("Write error: {}.".format(config_file_name)) + sys.exit(1) + + separator = "." + backup_file_name = separator.join([config_file_name, + '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), + 'pre-migration']) + + try: + subprocess.check_call(['cp', '-p', config_file_name, + backup_file_name]) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) + sys.exit(1) + + if not virtual: + migration = Migrator(config_file_name, force=force_on, + set_vintage=vintage) + else: + migration = VirtualMigrator(config_file_name) + + migration.run() + + if not migration._changed: + os.remove(backup_file_name) + +if __name__ == '__main__': + main() diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py new file mode 100755 index 000000000..c33e41d79 --- /dev/null +++ b/src/helpers/system-versions-foot.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . + +import sys +import vyos.formatversions as formatversions +import vyos.systemversions as systemversions +import vyos.defaults +import vyos.version + +sys_versions = systemversions.get_system_versions() + +component_string = formatversions.format_versions_string(sys_versions) + +os_version_string = vyos.version.get_version() + +sys.stdout.write("\n\n") +if vyos.defaults.cfg_vintage == 'vyos': + formatversions.write_vyos_versions_foot(None, component_string, + os_version_string) +elif vyos.defaults.cfg_vintage == 'vyatta': + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) +else: + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) -- cgit v1.2.3 From 2a289fda0de755b019d133622340c14a4a723f0f Mon Sep 17 00:00:00 2001 From: Matthias Fetzer Date: Mon, 10 Jun 2019 13:59:19 +0200 Subject: [wireguard] T1428: Add handling of fwmark setting (#70) [wireguard] T1428: correct handling of the fwmark option --- src/conf_mode/wireguard.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py index e893dba47..b6c1e189b 100755 --- a/src/conf_mode/wireguard.py +++ b/src/conf_mode/wireguard.py @@ -63,6 +63,7 @@ def get_config(): 'lport' : '', 'status' : 'exists', 'state' : 'enabled', + 'fwmark' : 0x00, 'mtu' : '1420', 'peer' : {} } @@ -95,6 +96,9 @@ def get_config(): ### listen port if c.exists(cnf + ' port'): config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') + ### fwmark + if c.exists(cnf + ' fwmark'): + config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') ### description if c.exists(cnf + ' description'): config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') @@ -296,6 +300,10 @@ def configure_interface(c, intf): if c['interfaces'][intf]['lport']: wg_config['port'] = c['interfaces'][intf]['lport'] + ## fwmark + if c['interfaces'][intf]['fwmark']: + wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] + ## endpoint if c['interfaces'][intf]['peer'][p]['endpoint']: wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] @@ -314,6 +322,7 @@ def configure_interface(c, intf): ### assemble wg command cmd = "sudo wg set " + intf cmd += " listen-port " + str(wg_config['port']) + cmd += " fwmark " + str(wg_config['fwmark']) cmd += " private-key " + wg_config['private-key'] cmd += " peer " + wg_config['pubkey'] cmd += " preshared-key " + wg_config['psk'] -- cgit v1.2.3 From 5e7cf2bb32ca5860d59d3a3c1fce9e3bba2236a2 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 12 Jun 2019 08:03:29 +0200 Subject: T1432: initial implementation of the config write API. --- python/vyos/configsession.py | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 python/vyos/configsession.py diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py new file mode 100644 index 000000000..4e9d30fa7 --- /dev/null +++ b/python/vyos/configsession.py @@ -0,0 +1,94 @@ +# configsession -- the write API for the VyOS running config +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import re +import subprocess + +CLI_SHELL_API = '/bin/cli-shell-api' +SET = '/opt/vyatta/sbin/my_set' +DELETE = '/opt/vyatta/sbin/my_delete' +COMMENT = '/opt/vyatta/sbin/my_comment' +COMMIT = '/opt/vyatta/sbin/my_commit' + +APP = "vyos-api" + + +class ConfigSessionError(Exception): + pass + + +class ConfigSession(object): + """ + The write API of VyOS. + """ + def __init__(self, session_id, app=APP): + """ + Creates a new config session. + + Args: + session_id (str): Session identifier + app (str): Application name, purely informational + + Note: + The session identifier MUST be globally unique within the system. + The best practice is to only have one ConfigSession object per process + and used the PID for the session identifier. + """ + + env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)]) + + # Extract actual variables from the chunk of shell it outputs + # XXX: it's better to extend cli-shell-api to provide easily readable output + env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode()) + + session_env = os.environ + for k, v in env_list: + session_env[k] = v + + self.__session_env = session_env + self.__session_env["COMMIT_VIA"] = app + + self.__run_command([CLI_SHELL_API, 'setupSession']) + + def __run_command(self, cmd_list): + p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env) + result = p.wait() + output = p.stdout.read().decode() + if result != 0: + raise VyOSAPIError(output) + + def set(self, path, value=None): + if not value: + value = [] + else: + value = [value] + self.__run_command([SET] + path + value) + + def delete(self, path, value=None): + if not value: + value = [] + else: + value = [value] + self.__run_command([DELETE] + path + value) + + def comment(self, path, value=None): + if not value: + value = [""] + else: + value = [value] + self.__run_command([COMMENT] + path + value) + + def commit(self): + self.__run_command([COMMIT]) -- cgit v1.2.3 From d24f18c25ec159ace25107fc04e50ac67ffe26ca Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 12 Jun 2019 10:10:21 +0200 Subject: T1431: add dependency on python3-bottle to have something to run the HTTP API with. --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index bf213352e..c8946e991 100644 --- a/debian/control +++ b/debian/control @@ -27,6 +27,7 @@ Depends: python3, python3-isc-dhcp-leases, python3-hurry.filesize, python3-vici (>= 5.7.2), + python3-bottle, ipaddrcheck, tcpdump, tshark, -- cgit v1.2.3 From 6f42122bc4b8ae8a287f0350eba4d8cd2f5f9649 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 12 Jun 2019 10:43:08 +0200 Subject: T1432: correct the ConfigSessionError exception name. --- python/vyos/configsession.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 4e9d30fa7..b989d3be5 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -67,7 +67,7 @@ class ConfigSession(object): result = p.wait() output = p.stdout.read().decode() if result != 0: - raise VyOSAPIError(output) + raise ConfigSessionError(output) def set(self, path, value=None): if not value: -- cgit v1.2.3 From aa88d5192338e2263516de791665f819a00b5c36 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 12 Jun 2019 11:29:07 -0500 Subject: T1397: escape backslashes in output passed to configtree The ouput of config.show_config (cli-shell-api showConfig) does not escape backslashes, whereas configtree expects escaped backslashes. Values containing unescaped backslashes consequently lead to a parsing error; cf. T1001. --- src/helpers/vyos-merge-config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index f0d5d1595..bb2919de2 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -64,6 +64,10 @@ merge_config_tree = ConfigTree(config_file) effective_config = Config() output_effective_config = effective_config.show_config() +# showConfig (called by config.show_config() does not escape +# backslashes, which configtree expects; cf. T1001. +output_effective_config = output_effective_config.replace("\\", "\\\\") + effective_config_tree = ConfigTree(output_effective_config) effective_cmds = effective_config_tree.to_commands() -- cgit v1.2.3 From 441f95d499f42f57b3a15d78aec826f794fab59f Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 12 Jun 2019 12:04:34 -0500 Subject: T1397: use revised migration method --- python/vyos/migrator.py | 2 ++ src/helpers/vyos-merge-config.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py index 2d4bc7ffc..59d68f0f7 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -165,6 +165,8 @@ class Migrator(object): self.write_config_file_versions(rev_versions) + def config_changed(self): + return self._changed class VirtualMigrator(Migrator): def __init__(self, config_file, vintage='vyos'): diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index bb2919de2..e97a1c08d 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -18,8 +18,10 @@ import sys import os import subprocess +import tempfile import vyos.defaults import vyos.remote +import vyos.migrator from vyos.config import Config from vyos.configtree import ConfigTree @@ -59,6 +61,16 @@ path = None if (len(sys.argv) > 2): path = " ".join(sys.argv[2:]) +with tempfile.NamedTemporaryFile() as file_to_migrate: + with open(file_to_migrate.name, 'w') as fd: + fd.write(config_file) + + migration = vyos.migrator.Migrator(file_to_migrate.name) + migration.run() + if migration.config_changed(): + with open(file_to_migrate.name, 'r') as fd: + config_file = fd.read() + merge_config_tree = ConfigTree(config_file) effective_config = Config() -- cgit v1.2.3 From bd6f3f6534a93f8a8c64e06967c24d4c3827c517 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 12 Jun 2019 12:45:00 -0500 Subject: T1397: check for path argument in both effective and merge config The merge config script restores the ability to restrict changes to a specified path. In the initial implementation, the path was checked for validity only with respect to the effective config; fix to allow valid paths from merge config as well. --- src/helpers/vyos-merge-config.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index e97a1c08d..e9a14ae98 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -57,10 +57,6 @@ else: print(err) sys.exit(1) -path = None -if (len(sys.argv) > 2): - path = " ".join(sys.argv[2:]) - with tempfile.NamedTemporaryFile() as file_to_migrate: with open(file_to_migrate.name, 'w') as fd: fd.write(config_file) @@ -91,12 +87,19 @@ merge_cmd_list = merge_cmds.splitlines() effective_cmd_set = set(effective_cmd_list) add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] -if path: - if not effective_config.exists(path): - print("path {} does not exist in running config; will use " - "root.".format(path)) +path = None +if (len(sys.argv) > 2): + path = sys.argv[2:] + if (not effective_config_tree.exists(path) and not + merge_config_tree.exists(path)): + print("path {} does not exist in either effective or merge" + " config; will use root.".format(path)) + path = None else: - add_cmds = [ cmd for cmd in add_cmds if path in cmd ] + path = " ".join(path) + +if path: + add_cmds = [ cmd for cmd in add_cmds if path in cmd ] for cmd in add_cmds: cmd = "/opt/vyatta/sbin/my_" + cmd -- cgit v1.2.3 From 29df430906c830146e6cc9b7edda9be836a01837 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 13 Jun 2019 03:10:08 +0200 Subject: T1431: make it possible to obtain session environment and run vyos.config functions under it. This is required for programs running outside a CLI session, like the future API daemon. --- python/vyos/config.py | 11 +++++++++-- python/vyos/configsession.py | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/python/vyos/config.py b/python/vyos/config.py index 9a5125eb9..96e9631e0 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -87,9 +87,13 @@ class Config(object): the only state it keeps is relative *config path* for convenient access to config subtrees. """ - def __init__(self): + def __init__(self, session_env=None): self._cli_shell_api = "/bin/cli-shell-api" self._level = "" + if session_env: + self.__session_env = session_env + else: + self.__session_env = None def _make_command(self, op, path): args = path.split() @@ -97,7 +101,10 @@ class Config(object): return cmd def _run(self, cmd): - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + if self.__session_env: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) + else: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) out = p.stdout.read() p.wait() if p.returncode != 0: diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index b989d3be5..c84d80a77 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -69,6 +69,9 @@ class ConfigSession(object): if result != 0: raise ConfigSessionError(output) + def get_session_env(self): + return self.__session_env + def set(self, path, value=None): if not value: value = [] -- cgit v1.2.3 From 3b9bfe322fd4a7d652b25b28cbcd4825fee0ea4b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 02:02:25 +0200 Subject: DHCPDv6 T1433: fix wrong lease file name A wrong lease file caused the show command to fail: vyos@vyos:~$ show dhcpv6 server leases Traceback (most recent call last): File "/usr/libexec/vyos/op_mode/show_dhcpv6.py", line 81, in leases = get_leases(lease_file, state='active') File "/usr/libexec/vyos/op_mode/show_dhcpv6.py", line 44, in get_leases leases = IscDhcpLeases(lease_file).get() File "/usr/lib/python3/dist-packages/isc_dhcp_leases/iscdhcpleases.py", line 110, in get with open(self.filename) as lease_file: FileNotFoundError: [Errno 2] No such file or directory: '/config/dhcpdv6.leases' --- src/conf_mode/dhcpv6_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index bb3e6e90d..8199c6a53 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -28,7 +28,7 @@ from vyos.config import Config from vyos import ConfigError config_file = r'/etc/dhcp/dhcpd6.conf' -lease_file = r'/config/dhcpd6.leases' +lease_file = r'/config/dhcpdv6.leases' daemon_config_file = r'/etc/default/isc-dhcpv6-server' # Please be careful if you edit the template. -- cgit v1.2.3 From adaa9b78e2fb0c7da58ca6c09934b3e3cff44795 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 02:03:57 +0200 Subject: DHCPDv6 T1433: rename daemon configuration file ... to have the same pattern as the DHCPDv6 lease file --- src/conf_mode/dhcpv6_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 8199c6a53..5430097de 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -27,7 +27,7 @@ import vyos.validate from vyos.config import Config from vyos import ConfigError -config_file = r'/etc/dhcp/dhcpd6.conf' +config_file = r'/etc/dhcp/dhcpdv6.conf' lease_file = r'/config/dhcpdv6.leases' daemon_config_file = r'/etc/default/isc-dhcpv6-server' -- cgit v1.2.3 From 6b5f0dd5e59b4e0b0170a087a50bfd61bc5f14ac Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 16 Jun 2019 10:51:07 +0200 Subject: T1432: add a discard function to vyos.configsession --- python/vyos/configsession.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index c84d80a77..a27470eeb 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -21,7 +21,9 @@ SET = '/opt/vyatta/sbin/my_set' DELETE = '/opt/vyatta/sbin/my_delete' COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' +DISCARD = '/opt/vyatta/sbin/my_discard' +# Default "commit via" string APP = "vyos-api" @@ -95,3 +97,6 @@ class ConfigSession(object): def commit(self): self.__run_command([COMMIT]) + + def discard(self): + self.__run_command([DISCARD]) -- 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(-) 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 303e8cb27560ade4bf0c9e6b9bc453c2f00fe799 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 16 Jun 2019 11:19:34 +0200 Subject: T1432: add a finalizer to vyos.configsession to avoid leaking sessions. --- python/vyos/configsession.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index a27470eeb..39a9713e0 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -14,6 +14,7 @@ import os import re +import sys import subprocess CLI_SHELL_API = '/bin/cli-shell-api' @@ -50,6 +51,7 @@ class ConfigSession(object): """ env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)]) + self.__session_id = session_id # Extract actual variables from the chunk of shell it outputs # XXX: it's better to extend cli-shell-api to provide easily readable output @@ -64,6 +66,14 @@ class ConfigSession(object): self.__run_command([CLI_SHELL_API, 'setupSession']) + def __del__(self): + try: + output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip() + if output: + print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr) + except Exception as e: + print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr) + def __run_command(self, cmd_list): p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env) result = p.wait() -- cgit v1.2.3 From 685b1e0d050c7883303733d710327161fe046b60 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 15:46:19 +0200 Subject: T849: move BGP peer-group node to ipv4 address family To have a consitent IPv4/IPv6 CLI a lot of BGP neighbor nodes have been migrated. The IPv4 peer-group has been forgotten, leaving a non consistent CLI. Previously: ----------- neighbor 2001:DB8:FFFF::1 { address-family { ipv6-unicast { peer-group iBGP } } peer-group iBGP } Now: ---- neighbor 2001:DB8:FFFF::1 { address-family { ipv6-unicast { peer-group iBGP } } address-family { ipv4-unicast { peer-group iBGP } } } --- src/migration-scripts/quagga/3-to-4 | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 src/migration-scripts/quagga/3-to-4 diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 new file mode 100755 index 000000000..b8ba8351b --- /dev/null +++ b/src/migration-scripts/quagga/3-to-4 @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 +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) + +def migrate_neighbor(config, neighbor_path, neighbor): + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + # Move peer-group + if config.exists(neighbor_path + [neighbor, 'peer-group']): + peer_group = config.return_value(neighbor_path + [neighbor, 'peer-group']) + config.set(neighbor_path + [neighbor] + af_path + ['peer-group'], value=peer_group) + config.delete(neighbor_path + [neighbor, 'peer-group']) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Just to avoid writing it so many times + af_path = ['address-family', 'ipv4-unicast'] + + # 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] + bgp_path = ['protocols', 'bgp', asn] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + ## Move global IPv4-specific BGP options to "address-family ipv4-unicast" + + ## Migrate neighbor options + neighbor_path = ['protocols', 'bgp', asn, 'neighbor'] + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + migrate_neighbor(config, neighbor_path, neighbor) + + 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) -- cgit v1.2.3 From c3898928e88856a96e343461fe047e3288cf881d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 17:03:28 +0200 Subject: Revert "T849: move BGP peer-group node to ipv4 address family" This reverts commit 685b1e0d050c7883303733d710327161fe046b60. --- src/migration-scripts/quagga/3-to-4 | 75 ------------------------------------- 1 file changed, 75 deletions(-) delete mode 100755 src/migration-scripts/quagga/3-to-4 diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 deleted file mode 100755 index b8ba8351b..000000000 --- a/src/migration-scripts/quagga/3-to-4 +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 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 -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) - -def migrate_neighbor(config, neighbor_path, neighbor): - if config.exists(neighbor_path): - neighbors = config.list_nodes(neighbor_path) - for neighbor in neighbors: - # Move peer-group - if config.exists(neighbor_path + [neighbor, 'peer-group']): - peer_group = config.return_value(neighbor_path + [neighbor, 'peer-group']) - config.set(neighbor_path + [neighbor] + af_path + ['peer-group'], value=peer_group) - config.delete(neighbor_path + [neighbor, 'peer-group']) - -if not config.exists(['protocols', 'bgp']): - # Nothing to do - sys.exit(0) -else: - # Just to avoid writing it so many times - af_path = ['address-family', 'ipv4-unicast'] - - # 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] - bgp_path = ['protocols', 'bgp', asn] - else: - # There's actually no BGP, just its empty shell - sys.exit(0) - - ## Move global IPv4-specific BGP options to "address-family ipv4-unicast" - - ## Migrate neighbor options - neighbor_path = ['protocols', 'bgp', asn, 'neighbor'] - if config.exists(neighbor_path): - neighbors = config.list_nodes(neighbor_path) - for neighbor in neighbors: - migrate_neighbor(config, neighbor_path, neighbor) - - 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) -- cgit v1.2.3 From 9bf7d03ff7342e7f87710df6bcc15beceed9582c Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 16 Jun 2019 21:02:25 +0200 Subject: T1432: inject VyOS-specific environment variables into the session environment. They are widely referenced by command templates, but a process started as a service doesn't automatically get them. --- python/vyos/configsession.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 39a9713e0..78f332d66 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -25,7 +25,42 @@ COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' # Default "commit via" string -APP = "vyos-api" +APP = "vyos-http-api" + +# When started as a service rather than from a user shell, +# the process lacks the VyOS-specific environment that comes +# from bash configs, so we have to inject it +# XXX: maybe it's better to do via a systemd environment file +def inject_vyos_env(env): + env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg' + env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin' + env['vyatta_bindir']= '/opt/vyatta/bin' + env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' + env['vyatta_configdir'] = '/opt/vyatta/config' + env['vyatta_datadir'] = '/opt/vyatta/share' + env['vyatta_datarootdir'] = '/opt/vyatta/share' + env['vyatta_libdir'] = '/opt/vyatta/lib' + env['vyatta_libexecdir'] = '/opt/vyatta/libexec' + env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates' + env['vyatta_prefix'] = '/opt/vyatta' + env['vyatta_sbindir'] = '/opt/vyatta/sbin' + env['vyatta_sysconfdir'] = '/opt/vyatta/etc' + env['vyos_bin_dir'] = '/usr/bin' + env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' + env['vyos_completion_dir'] = '/usr/libexec/vyos/completion' + env['vyos_configdir'] = '/opt/vyatta/config' + env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode' + env['vyos_datadir'] = '/opt/vyatta/share' + env['vyos_datarootdir']= '/opt/vyatta/share' + env['vyos_libdir'] = '/opt/vyatta/lib' + env['vyos_libexec_dir'] = '/usr/libexec/vyos' + env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode' + env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates' + env['vyos_prefix'] = '/opt/vyatta' + env['vyos_sbin_dir'] = '/usr/sbin' + env['vyos_validators_dir'] = '/usr/libexec/vyos/validators' + + return env class ConfigSessionError(Exception): @@ -58,6 +93,7 @@ class ConfigSession(object): env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode()) session_env = os.environ + session_env = inject_vyos_env(session_env) for k, v in env_list: session_env[k] = v -- cgit v1.2.3 From fc2cc0f9b660408d5fc0cffcaffc33bfbc8ca5f2 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 16 Jun 2019 21:26:38 +0200 Subject: T1431: initial implementation of the HTTP API. --- debian/rules | 5 + src/services/vyos-http-api-server | 203 ++++++++++++++++++++++++++++++++++++++ src/systemd/vyos-http-api.service | 16 +++ 3 files changed, 224 insertions(+) create mode 100755 src/services/vyos-http-api-server create mode 100644 src/systemd/vyos-http-api.service diff --git a/debian/rules b/debian/rules index ff2d205ba..b06117922 100755 --- a/debian/rules +++ b/debian/rules @@ -10,6 +10,7 @@ VYOS_OP_TMPL_DIR := /opt/vyatta/share/vyatta-op/templates MIGRATION_SCRIPTS_DIR := /opt/vyatta/etc/config-migrate/migrate/ SYSTEM_SCRIPTS_DIR := usr/libexec/vyos/system +SERVICES_DIR := usr/libexec/vyos/services %: dh $@ --with python3, --with quilt @@ -53,6 +54,10 @@ override_dh_auto_install: mkdir -p $(DIR)/$(SYSTEM_SCRIPTS_DIR) cp -r src/system/* $(DIR)/$(SYSTEM_SCRIPTS_DIR) + # Install system services + mkdir -p $(DIR)/$(SERVICES_DIR) + cp -r src/services/* $(DIR)/$(SERVICES_DIR) + # Install configuration command definitions mkdir -p $(DIR)/$(VYOS_CFG_TMPL_DIR) cp -r templates-cfg/* $(DIR)/$(VYOS_CFG_TMPL_DIR) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server new file mode 100755 index 000000000..32f8adc73 --- /dev/null +++ b/src/services/vyos-http-api-server @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 os +import sys +import grp +import json +import traceback +import threading + +import vyos.config + +import bottle + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.config import VyOSError + + +DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' + +CFG_GROUP = 'vyattacfg' + +app = bottle.default_app() + +# Giant lock! +lock = threading.Lock() + +def load_server_config(): + with open(DEFAULT_CONFIG_FILE) as f: + config = json.load(f) + return config + +def check_auth(key_list, key): + id = None + for k in key_list: + if k['key'] == key: + id = k['id'] + return id + +def error(code, msg): + bottle.response.status = code + resp = {"success": False, "error": msg, "data": None} + return json.dumps(resp) + +def success(data): + resp = {"success": True, "data": data, "error": None} + return json.dumps(resp) + +@app.route('/configure', method='POST') +def configure(): + session = app.config['vyos_session'] + config = app.config['vyos_config'] + api_keys = app.config['vyos_keys'] + + key = bottle.request.forms.get("key") + id = check_auth(api_keys, key) + if not id: + return error(401, "Valid API key is required") + + strict_field = bottle.request.forms.get("strict") + if strict_field == "true": + strict = True + else: + strict = False + + commands = bottle.request.forms.get("data") + if not commands: + return error(400, "Non-empty data field is required") + else: + try: + commands = json.loads(commands) + except Exception as e: + return error(400, "Failed to parse JSON: {0}".format(e)) + + # We don't want multiple people/apps to be able to commit at once, + # or modify the shared session while someone else is doing the same, + # so the lock is really global + lock.acquire() + + status = 200 + error_msg = None + try: + for c in commands: + op = c['op'] + path = c['path'] + value = c['value'] + + # Account for null values + if not value: + value = "" + + # For vyos.config calls + cfg_path = " ".join(path + [value]).strip() + + if op == 'set': + # XXX: it would be nice to do a strict check for "path already exists", + # but there's probably no way to do that + session.set(path, value=value) + elif op == 'delete': + if strict and not config.exists(cfg_path): + raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path)) + session.delete(path, value=value) + elif op == 'comment': + session.comment(path, value=value) + else: + raise ConfigSessionError("\"{0}\" is not a valid operation".format(op)) + # end for + session.commit() + print("Configuration modified via HTTP API using key \"{0}\"".format(id)) + except ConfigSessionError as e: + session.discard() + status = 400 + if app.config['vyos_debug']: + print(traceback.format_exc(), file=sys.stderr) + error_msg = str(e) + except Exception as e: + session.discard() + print(traceback.format_exc(), file=sys.stderr) + status = 500 + + # Don't give the details away to the outer world + error_msg = "An internal error occured. Check the logs for details." + + lock.release() + if status != 200: + return error(status, error_msg) + else: + return success(None) + +@app.route('/retrieve', method='POST') +def get_value(): + config = app.config['vyos_config'] + + api_keys = app.config['vyos_keys'] + + key = bottle.request.forms.get("key") + id = check_auth(api_keys, key) + if not id: + return error(401, "Valid API key is required") + + command = bottle.request.forms.get("data") + command = json.loads(command) + + op = command['op'] + path = " ".join(command['path']) + + try: + if op == 'returnValue': + res = config.return_value(path) + elif op == 'returnValues': + res = config.return_values(path) + elif op == 'exists': + res = config.exists(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except VyOSError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +if __name__ == '__main__': + # systemd's user and group options don't work, do it by hand here, + # else no one else will be able to commit + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + # Need to set file permissions to 775 too so that every vyattacfg group member + # has write access to the running config + os.umask(0o002) + + try: + server_config = load_server_config() + except Exception as e: + print("Failed to load the HTTP API server config: {0}".format(e)) + + session = ConfigSession(os.getpid()) + env = session.get_session_env() + config = vyos.config.Config(session_env=env) + + app.config['vyos_session'] = session + app.config['vyos_config'] = config + app.config['vyos_keys'] = server_config['api_keys'] + app.config['vyos_debug'] = server_config['debug'] + + bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service new file mode 100644 index 000000000..f0665e3d5 --- /dev/null +++ b/src/systemd/vyos-http-api.service @@ -0,0 +1,16 @@ +[Unit] +Description=VyOS HTTP API service +After=auditd.service systemd-user-sessions.service time-sync.target + +[Service] +ExecStart=/usr/libexec/vyos/services/vyos-http-api-server +ExecReload=/bin/kill -TERM $MAINPID +KillMode=process + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +WantedBy=multi-user.target + -- cgit v1.2.3 From b04a9791226f7953cfa740804ec0d43745605f49 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 16 Jun 2019 15:27:03 +0200 Subject: T1439: remove quotes around dhcp6.client-id --- src/conf_mode/dhcpv6_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 5430097de..aa9c35fa1 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -94,7 +94,7 @@ shared-network {{ network.name }} { {%- for host in subnet.static_mapping %} {% if not host.disabled -%} host {{ network.name }}_{{ host.name }} { - host-identifier option dhcp6.client-id "{{ host.client_identifier }}"; + host-identifier option dhcp6.client-id {{ host.client_identifier }}; fixed-address6 {{ host.ipv6_address }}; } {%- endif %} -- cgit v1.2.3 From f875731a710eb7fadefd5a73d65e53915477dce8 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 16 Jun 2019 17:07:16 +0200 Subject: dhcpv6-server: Add name constraint, clarify help, fix typos --- interface-definitions/dhcpv6-server.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml index e63eb2242..bf049cf7a 100644 --- a/interface-definitions/dhcpv6-server.xml +++ b/interface-definitions/dhcpv6-server.xml @@ -268,7 +268,7 @@ - IPv6 address of an SNTP Server for client to use + IPv6 address of an SNTP server for client to use @@ -278,25 +278,29 @@ Name of static mapping + + ^[-_a-zA-Z0-9.]+$ + + Invalid static-mapping name - Option to disable static-mapping + Option to disable static mapping - Client identifier for this static mapping + Client identifier (DUID) for this static mapping [REQUIRED] - Client IPv5 address for this static mapping + Client IPv6 address for this static mapping [REQUIRED] ipv6 - IPv6 address for this tatic mapping + IPv6 address for this static mapping [REQUIRED] -- cgit v1.2.3 From 87df87e3983e120ad171ae9dc2966309fc14fcd8 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 16 Jun 2019 19:22:49 +0200 Subject: T1439: add dhcpv6-client-id validator --- interface-definitions/dhcpv6-server.xml | 9 ++++++++- src/validators/dhcpv6-client-id | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100755 src/validators/dhcpv6-client-id diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml index bf049cf7a..b1e5c8d6c 100644 --- a/interface-definitions/dhcpv6-server.xml +++ b/interface-definitions/dhcpv6-server.xml @@ -281,7 +281,7 @@ ^[-_a-zA-Z0-9.]+$ - Invalid static-mapping name + Invalid static-mapping name. May only contain letters, numbers and .-_ @@ -293,6 +293,13 @@ Client identifier (DUID) for this static mapping [REQUIRED] + + h[[:h]...] + DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id) + + + + diff --git a/src/validators/dhcpv6-client-id b/src/validators/dhcpv6-client-id new file mode 100755 index 000000000..a8c3e60b6 --- /dev/null +++ b/src/validators/dhcpv6-client-id @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 re + +if len(sys.argv) == 2: + pattern = "^([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})$" + if re.match(pattern, sys.argv[1]): + sys.exit(0) + else: + sys.exit(1) + -- cgit v1.2.3 From 6a6634b02d73cc93cd7368cf2290940b57fae9c7 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 16 Jun 2019 21:59:01 +0200 Subject: T1439: move DUID validator to regex --- interface-definitions/dhcpv6-server.xml | 2 +- src/validators/dhcpv6-client-id | 28 ---------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100755 src/validators/dhcpv6-client-id diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml index b1e5c8d6c..09ffe67ed 100644 --- a/interface-definitions/dhcpv6-server.xml +++ b/interface-definitions/dhcpv6-server.xml @@ -298,7 +298,7 @@ DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id) - + ^([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})$ diff --git a/src/validators/dhcpv6-client-id b/src/validators/dhcpv6-client-id deleted file mode 100755 index a8c3e60b6..000000000 --- a/src/validators/dhcpv6-client-id +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 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 re - -if len(sys.argv) == 2: - pattern = "^([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})$" - if re.match(pattern, sys.argv[1]): - sys.exit(0) - else: - sys.exit(1) - -- cgit v1.2.3 From 03c09b1b0d7dfdab9fc87bc7b017455c45141ced Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sun, 16 Jun 2019 22:52:05 +0200 Subject: T1439: remove beginning and end anchors, they are implied with re.fullmatch --- interface-definitions/dhcpv6-server.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml index 09ffe67ed..e18a58608 100644 --- a/interface-definitions/dhcpv6-server.xml +++ b/interface-definitions/dhcpv6-server.xml @@ -279,7 +279,7 @@ Name of static mapping - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ Invalid static-mapping name. May only contain letters, numbers and .-_ @@ -298,7 +298,7 @@ DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id) - ^([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})$ + ([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2}) -- cgit v1.2.3 From 2c12af330c51468927df3e85ca23aaf9098ff4f8 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 14:04:58 +0200 Subject: T1431: Fix vyos-http-server logging to journald. --- src/systemd/vyos-http-api.service | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service index f0665e3d5..53322a84f 100644 --- a/src/systemd/vyos-http-api.service +++ b/src/systemd/vyos-http-api.service @@ -3,10 +3,12 @@ Description=VyOS HTTP API service After=auditd.service systemd-user-sessions.service time-sync.target [Service] -ExecStart=/usr/libexec/vyos/services/vyos-http-api-server -ExecReload=/bin/kill -TERM $MAINPID +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server KillMode=process +SyslogIdentifier=vyos-http-api +SyslogFacility=daemon + # Does't work but leave it here User=root Group=vyattacfg -- cgit v1.2.3 From 8d70134d1adba4d787476ded970ee40ab18d1622 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 14:37:09 +0200 Subject: T1431: release the lock even if discard() caused an exception. It may be better to crash the process in that situation. --- src/services/vyos-http-api-server | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 32f8adc73..7b9e3d671 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -135,8 +135,9 @@ def configure(): # Don't give the details away to the outer world error_msg = "An internal error occured. Check the logs for details." + finally: + lock.release() - lock.release() if status != 200: return error(status, error_msg) else: -- cgit v1.2.3 From 9b4447f039655b9e24187302b9c4d9bb2116aa99 Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 17 Jun 2019 08:38:37 -0700 Subject: [pppoe-server] T1408 - improve verify() function to enable IPv6 only deployments --- src/conf_mode/accel_pppoe.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 7a9878a1d..9d2efb0bc 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -83,17 +83,19 @@ master=1 [client-ip-range] disable +{% if ppp_gw %} [ip-pool] gw-ip-address={{ppp_gw}} {% if client_ip_pool %} {{client_ip_pool}} -{% endif %} +{% endif -%} {% if client_ip_subnets %} {% for sn in client_ip_subnets %} {{sn}} {% endfor %} {% endif %} +{% endif -%} {% if client_ipv6_pool %} [ipv6-pool] @@ -550,18 +552,14 @@ def verify(c): if c['authentication']['radiussrv'][rsrv]['secret'] == None: raise ConfigError('radius server ' + rsrv + ' needs a secret configured') - ### local ippool and gateway settings - - if not c['ppp_gw']: - raise ConfigError('pppoe-server local-ip required') - - if not c['client_ip_subnets'] and not c['client_ip_pool']: - print ("Warning: No pppoe client IP pool defined") + ### local ippool and gateway settings config checks - ### activate as soon as it is clear what to do migrate or depricate. - #if c['client_ip_pool']: - # print ("Warning: client-ip-pool (start|stop) is depricated, please use client-ip-pool subnet") - # sl.syslog(sl.LOG_NOTICE, "client-ip-pool start stop is depricated, please use client-ip-pool subnet") + if c['client_ip_subnets'] or c['client_ip_pool']: + if not c['ppp_gw']: + raise ConfigError('pppoe-server local-ip required') + + if c['ppp_gw'] and not c['client_ip_subnets'] and not c['client_ip_pool']: + print ("Warning: No pppoe client IPv4 pool defined") def generate(c): if c == None: -- cgit v1.2.3 From 62822413b20a82c4738897c16d3120c04fbf27d6 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 19:58:29 +0200 Subject: [HTTP API] T1431: make systemd restart the HTTP API service on failure. --- src/systemd/vyos-http-api.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service index 53322a84f..509af4816 100644 --- a/src/systemd/vyos-http-api.service +++ b/src/systemd/vyos-http-api.service @@ -9,6 +9,8 @@ KillMode=process SyslogIdentifier=vyos-http-api SyslogFacility=daemon +Restart=on-failure + # Does't work but leave it here User=root Group=vyattacfg -- cgit v1.2.3 From ac2b3fbede362348b94c3e759ff90a15f0fef62a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 19:59:16 +0200 Subject: [HTTP API] T1431: make the value field optional and add better validation. --- src/services/vyos-http-api-server | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 7b9e3d671..45723010a 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -96,16 +96,36 @@ def configure(): error_msg = None try: for c in commands: + # Missing op or path is a show stopper + if not ('op' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) + if not ('path' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) + # Missing value is fine, substitute for empty string + if not ('value' in c): + value = "" + op = c['op'] path = c['path'] value = c['value'] - # Account for null values + # Type checking + if not isinstance(path, list): + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) + + if not isinstance(value, str): + raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c))) + + # Account for the case when value field is present and set to null if not value: value = "" - # For vyos.config calls - cfg_path = " ".join(path + [value]).strip() + # For vyos.configsessios calls that have no separate value arguments, + # and for type checking too + try: + cfg_path = " ".join(path + [value]).strip() + except TypeError: + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c))) if op == 'set': # XXX: it would be nice to do a strict check for "path already exists", -- cgit v1.2.3 From 0f354688d7bd63b63fb91faf17a38c77fb05f660 Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 17 Jun 2019 11:10:33 -0700 Subject: [syslog/hostname.py] T1394 - syslog systemd and host_name.py race condition - checking if the hostname has changed, otherwise the script and systemd try to restart rsyslogd at the same time, at the end it's not started at all. --- src/conf_mode/host_name.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 621ccd7e0..0d03fd41c 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -248,10 +248,15 @@ def apply(config): if config['domain_name']: fqdn += '.' + config['domain_name'] + # rsyslog runs into a race condition at boot time with systemd + # restart rsyslog only if the hostname changed. + hn = subprocess.check_output(['hostnamectl','--static']).decode().strip() + os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) # Restart services that use the hostname - os.system("systemctl restart rsyslog.service") + if hn != fqdn: + os.system("systemctl restart rsyslog.service") # If SNMP is running, restart it too if os.system("pgrep snmpd > /dev/null") == 0: -- cgit v1.2.3 From 7f06879361999e3b3aab6f66bb267841d958bfdb Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 20:12:24 +0200 Subject: [HTTP API] T1431: allow sending a single command, and make sure commands are dicts. --- src/services/vyos-http-api-server | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 45723010a..301c083a1 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -87,6 +87,10 @@ def configure(): except Exception as e: return error(400, "Failed to parse JSON: {0}".format(e)) + # Allow users to pass just one command + if not isinstance(commands, list): + commands = [commands] + # We don't want multiple people/apps to be able to commit at once, # or modify the shared session while someone else is doing the same, # so the lock is really global @@ -96,6 +100,10 @@ def configure(): error_msg = None try: for c in commands: + # What we've got may not even be a dict + if not isinstance(c, dict): + raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c))) + # Missing op or path is a show stopper if not ('op' in c): raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) -- cgit v1.2.3 From 1d40561bbd3aac552c8585d09d8436884aabdee7 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 20:19:21 +0200 Subject: [HTTP API] T1431: make the value field optional. --- src/services/vyos-http-api-server | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 301c083a1..834c06b4d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -109,13 +109,15 @@ def configure(): raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) if not ('path' in c): raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) + # Missing value is fine, substitute for empty string - if not ('value' in c): + if 'value' in c: + value = c['value'] + else: value = "" op = c['op'] path = c['path'] - value = c['value'] # Type checking if not isinstance(path, list): -- cgit v1.2.3 From 73021645d1d1fa0e851bae7e003982f9ee491e84 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 21:07:41 +0200 Subject: [HTTP API] T1431: disallow empty config paths. --- src/services/vyos-http-api-server | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 834c06b4d..e11eb6d52 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -119,6 +119,9 @@ def configure(): op = c['op'] path = c['path'] + if not path: + raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c))) + # Type checking if not isinstance(path, list): raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) -- cgit v1.2.3 From 0037287a8e6a7ddd6d4a8101804d9cc0b8b3e70f Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Tue, 18 Jun 2019 16:08:08 +0200 Subject: [ config ] T1447: Python subprocess called without import in host_name.py --- src/conf_mode/host_name.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 0d03fd41c..13b2b98ae 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -25,6 +25,7 @@ import re import sys import copy import glob +import subprocess import argparse import jinja2 @@ -250,13 +251,13 @@ def apply(config): # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. - hn = subprocess.check_output(['hostnamectl','--static']).decode().strip() + hn = subprocess.check_output(['hostnamectl', '--static']).decode().strip() os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) # Restart services that use the hostname if hn != fqdn: - os.system("systemctl restart rsyslog.service") + os.system("systemctl restart rsyslog.service") # If SNMP is running, restart it too if os.system("pgrep snmpd > /dev/null") == 0: -- cgit v1.2.3 From 85c9e0200a4619f0388b7fd7ba9a03f4be933ef5 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 18 Jun 2019 15:07:41 -0700 Subject: [pppoe-server] T1452 - add vendor option to shaper --- interface-definitions/pppoe-server.xml | 5 +++++ src/conf_mode/accel_pppoe.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/interface-definitions/pppoe-server.xml b/interface-definitions/pppoe-server.xml index 1190cd9ff..18b0e649c 100644 --- a/interface-definitions/pppoe-server.xml +++ b/interface-definitions/pppoe-server.xml @@ -194,6 +194,11 @@ Specifies which radius attribute contains rate information. (default is Filter-Id) + + + Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius) + + Enables Bandwidth shaping via RADIUS diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 9d2efb0bc..3c7759b17 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -173,6 +173,9 @@ verbose=1 [shaper] verbose=1 attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} {% endif -%} {% endif %} @@ -485,6 +488,9 @@ def get_config(): 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') -- cgit v1.2.3 From 2ee0eff1bd04ef02b0769341eee22543f8011b68 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 19 Jun 2019 10:55:13 -0700 Subject: [wireguard] T1425 - assign a /31 address on Wireguard interface - added a validator for checking if the address is any cidr noted address --- interface-definitions/wireguard.xml | 2 +- src/validators/cidr | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100755 src/validators/cidr diff --git a/interface-definitions/wireguard.xml b/interface-definitions/wireguard.xml index cc86855a5..e752a2f1d 100644 --- a/interface-definitions/wireguard.xml +++ b/interface-definitions/wireguard.xml @@ -20,7 +20,7 @@ IP address - + ipv4-address diff --git a/src/validators/cidr b/src/validators/cidr new file mode 100755 index 000000000..815aa8ba1 --- /dev/null +++ b/src/validators/cidr @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-cidr $1 -- cgit v1.2.3 From 06e6ae3bac7cd39341c0b19b570020649d725344 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Thu, 20 Jun 2019 10:33:56 +0200 Subject: T1458: Regression in 1.2.1-S2 hostname & logging --- src/conf_mode/host_name.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 13b2b98ae..b0a4648c7 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -42,7 +42,8 @@ config_file_resolv = '/etc/resolv.conf' config_tmpl_hosts = """ ### Autogenerated by host_name.py ### -127.0.0.1 localhost {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} +127.0.0.1 localhost +127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback -- cgit v1.2.3 From efb1a1c88f436a3704c4ca6e15b65aeded4b9654 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 20 Jun 2019 22:14:36 +0200 Subject: firewall: T1461: deleting 'firewall options' causes Python TypeError [ firewall options interface wg01 ] Traceback (most recent call last): File "/usr/libexec/vyos/conf_mode/firewall_options.py", line 139, in apply(c) File "/usr/libexec/vyos/conf_mode/firewall_options.py", line 97, in apply if tcp['new_chain4']: TypeError: 'NoneType' object is not subscriptable delete [ firewall options ] failed delete [ firewall ] failed Commit failed --- src/conf_mode/firewall_options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index e2c306904..2be80cdbf 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -32,7 +32,8 @@ def get_config(): opts = copy.deepcopy(default_config_data) conf = Config() if not conf.exists('firewall options'): - return None + # bail out early + return opts else: conf.set_level('firewall options') -- cgit v1.2.3 From ecbe6c1c1b87792048512e3aa4913c1ce5b75c82 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 19:59:33 +0200 Subject: bfd: T1183: initial CLI implementation vyos@vyos# show protocols bfd peer 172.18.202.10 { local-address 172.18.201.10 local-interface eth0.201 shutdown } peer 172.18.202.12 { shutdown } --- interface-definitions/protocols-bfd.xml | 58 +++++++++++++++++++++++++ src/conf_mode/protocols_bfd.py | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 interface-definitions/protocols-bfd.xml create mode 100755 src/conf_mode/protocols_bfd.py diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml new file mode 100644 index 000000000..c8b25eb2d --- /dev/null +++ b/interface-definitions/protocols-bfd.xml @@ -0,0 +1,58 @@ + + + + + + + + Bidirectional Forwarding Detection (BFD) + 820 + + + + + Configures a new BFD peer to listen and talk to + + ipv4 + BFD peer IPv4 address + + + ipv6 + BFD peer IPv6 address + + + + + + Local interface to bind our peer listener to + + + + + + + + Local address to bind our peer listener to + + ipv4 + Local IPv4 address used to connect to the peer + + + ipv6 + Local IPv6 address used to connect to the peer + + + + + + Disable this peer + + + + + + + + + + diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py new file mode 100755 index 000000000..7e137c484 --- /dev/null +++ b/src/conf_mode/protocols_bfd.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 copy +from vyos.config import Config + +default_config_data = { + 'peers': [] +} + +def get_config(): + bfd = copy.deepcopy(default_config_data) + conf = Config() + if not conf.exists('protocols bfd'): + return None + else: + conf.set_level('protocols bfd') + + for peer in conf.list_nodes('peer'): + conf.set_level('protocols bfd peer {0}'.format(peer)) + bfd_peer = { + 'peer': peer, + 'shutdown': False, + 'local-interface': '', + 'local-address': '', + } + + # Check if individual peer is disabled + if conf.exists('shutdown'): + bfd_peer['shutdown'] = True + + # Check if peer has a local source interface configured + if conf.exists('local-interface'): + bfd_peer['local-interface'] = conf.return_value('local-interface') + + # Check if peer has a local source address configured - this is mandatory for IPv6 + if conf.exists('local-address'): + bfd_peer['local-address'] = conf.return_value('local-address') + + bfd['peers'].append(bfd_peer) + + print(bfd) + return bfd + +def verify(bfd): + return None + +def generate(bfd): + return None + +def apply(bfd): + return None + +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 4fa93aefd1e4c525267f90b5fd7797157946c9bd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 16 Jun 2019 20:20:09 +0200 Subject: bfd: T1183: IPv6 peers require explicit local address/interface --- src/conf_mode/protocols_bfd.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 7e137c484..51af67ff6 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -15,7 +15,11 @@ # along with this program. If not, see . # +import sys import copy +import vyos.validate + +from vyos import ConfigError from vyos.config import Config default_config_data = { @@ -33,7 +37,7 @@ def get_config(): for peer in conf.list_nodes('peer'): conf.set_level('protocols bfd peer {0}'.format(peer)) bfd_peer = { - 'peer': peer, + 'remote': peer, 'shutdown': False, 'local-interface': '', 'local-address': '', @@ -53,16 +57,36 @@ def get_config(): bfd['peers'].append(bfd_peer) - print(bfd) return bfd def verify(bfd): + if bfd is None: + return None + + for peer in bfd['peers']: + # Bail out early if peer is shutdown + if peer['shutdown']: + continue + + # IPv6 peers require an explicit local address/interface combination + if vyos.validate.is_ipv6(peer['remote']): + if not (peer['local-interface'] and peer['local-address']): + raise ConfigError("BFD IPv6 peers require explicit local address/interface setting") + + return None def generate(bfd): + if bfd is None: + return None + return None def apply(bfd): + if bfd is None: + return None + + print(bfd) return None if __name__ == '__main__': -- cgit v1.2.3 From 62ca0f55506245865dcc14fd95d68d9a3482df7b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 21 Jun 2019 21:02:13 +0200 Subject: bfd: T1183: first working FRR bfd peer configuration --- src/conf_mode/protocols_bfd.py | 54 +++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 51af67ff6..08d3991ff 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -16,31 +16,56 @@ # import sys +import jinja2 import copy +import os import vyos.validate from vyos import ConfigError from vyos.config import Config +config_file = r'/tmp/bfd.frr' + +# Please be careful if you edit the template. +config_tmpl = """ +! +bfd +{% for peer in old_peers -%} + no peer {{ peer }} +{% endfor -%} +! +{% for peer in new_peers -%} + peer {{ peer.remote }}{% if peer.local_address %} local-address {{ peer.local_address }}{% endif %}{% if peer.local_interface %} interface {{ peer.local_interface }}{% endif %} + {% if not peer.shutdown %}no {% endif %}shutdown +{% endfor -%} +! +""" + default_config_data = { - 'peers': [] + 'new_peers': [], + 'old_peers' : [] } def get_config(): bfd = copy.deepcopy(default_config_data) conf = Config() - if not conf.exists('protocols bfd'): + if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): return None else: conf.set_level('protocols bfd') + # as we have to use vtysh to talk to FRR we also need to know + # which peers are gone due to a config removal - thus we read in + # all peers (active or to delete) + bfd['old_peers'] = conf.list_effective_nodes('peer') + for peer in conf.list_nodes('peer'): conf.set_level('protocols bfd peer {0}'.format(peer)) bfd_peer = { 'remote': peer, 'shutdown': False, - 'local-interface': '', - 'local-address': '', + 'local_interface': '', + 'local_address': '', } # Check if individual peer is disabled @@ -49,13 +74,13 @@ def get_config(): # Check if peer has a local source interface configured if conf.exists('local-interface'): - bfd_peer['local-interface'] = conf.return_value('local-interface') + bfd_peer['local_interface'] = conf.return_value('local-interface') # Check if peer has a local source address configured - this is mandatory for IPv6 if conf.exists('local-address'): - bfd_peer['local-address'] = conf.return_value('local-address') + bfd_peer['local_address'] = conf.return_value('local-address') - bfd['peers'].append(bfd_peer) + bfd['new_peers'].append(bfd_peer) return bfd @@ -63,17 +88,16 @@ def verify(bfd): if bfd is None: return None - for peer in bfd['peers']: + for peer in bfd['new_peers']: # Bail out early if peer is shutdown if peer['shutdown']: continue # IPv6 peers require an explicit local address/interface combination if vyos.validate.is_ipv6(peer['remote']): - if not (peer['local-interface'] and peer['local-address']): + if not (peer['local_interface'] and peer['local_address']): raise ConfigError("BFD IPv6 peers require explicit local address/interface setting") - return None def generate(bfd): @@ -86,7 +110,15 @@ def apply(bfd): if bfd is None: return None - print(bfd) + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(bfd) + with open(config_file, 'w') as f: + f.write(config_text) + + os.system("sudo vtysh -d bfdd -f " + config_file) + if os.path.exists(config_file): + os.remove(config_file) + return None if __name__ == '__main__': -- cgit v1.2.3 From 1b1f6b20226c92e4beba171159ead8fb21713484 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 21 Jun 2019 21:07:45 +0200 Subject: bfd: T1183: add support for multihop multihop tells the BFD daemon that we should expect packets with TTL less than 254 (because it will take more than one hop) and to listen on the multihop port (4784). When using multi-hop mode echo-mode will not work (see RFC 5883 section 3). --- interface-definitions/protocols-bfd.xml | 6 ++++++ src/conf_mode/protocols_bfd.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index c8b25eb2d..91f0665f9 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -49,6 +49,12 @@ + + + Allow this BFD peer to not be directly connected + + + diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 08d3991ff..92fae990e 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -35,7 +35,7 @@ bfd {% endfor -%} ! {% for peer in new_peers -%} - peer {{ peer.remote }}{% if peer.local_address %} local-address {{ peer.local_address }}{% endif %}{% if peer.local_interface %} interface {{ peer.local_interface }}{% endif %} + peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.local_address %} local-address {{ peer.local_address }}{% endif %}{% if peer.local_interface %} interface {{ peer.local_interface }}{% endif %} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor -%} ! @@ -66,6 +66,7 @@ def get_config(): 'shutdown': False, 'local_interface': '', 'local_address': '', + 'multihop': False } # Check if individual peer is disabled @@ -80,6 +81,12 @@ def get_config(): if conf.exists('local-address'): bfd_peer['local_address'] = conf.return_value('local-address') + # Tell BFD daemon that we should expect packets with TTL less than 254 + # (because it will take more than one hop) and to listen on the multihop + # port (4784) + if conf.exists('multihop'): + bfd_peer['multihop'] = True + bfd['new_peers'].append(bfd_peer) return bfd -- 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 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 c2a8c1a22f432265c73606106046c02e995eb630 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 14:44:45 +0200 Subject: bfd: T1183: adjust CLI syntax for source address/interface Place address/interface under new source node. vyis@vyos# show protocols bfd peer 1.1.1.1 { source { address 1.2.3.4 interface eth0.201 } } --- interface-definitions/protocols-bfd.xml | 45 +++++++++++++++++++-------------- src/conf_mode/protocols_bfd.py | 16 ++++++------ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index 91f0665f9..ab8c9e233 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -22,27 +22,34 @@ - + - Local interface to bind our peer listener to - - - + Bind listener to specifid interface/address, mandatory for IPv6 - - - - Local address to bind our peer listener to - - ipv4 - Local IPv4 address used to connect to the peer - - - ipv6 - Local IPv6 address used to connect to the peer - - - + + + + Local interface to bind our peer listener to + + + + + + + + Local address to bind our peer listener to + + ipv4 + Local IPv4 address used to connect to the peer + + + ipv6 + Local IPv6 address used to connect to the peer + + + + + Disable this peer diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 92fae990e..6d2b8382a 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -35,7 +35,7 @@ bfd {% endfor -%} ! {% for peer in new_peers -%} - peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.local_address %} local-address {{ peer.local_address }}{% endif %}{% if peer.local_interface %} interface {{ peer.local_interface }}{% endif %} + peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor -%} ! @@ -64,8 +64,8 @@ def get_config(): bfd_peer = { 'remote': peer, 'shutdown': False, - 'local_interface': '', - 'local_address': '', + 'src_if': '', + 'src_addr': '', 'multihop': False } @@ -74,12 +74,12 @@ def get_config(): bfd_peer['shutdown'] = True # Check if peer has a local source interface configured - if conf.exists('local-interface'): - bfd_peer['local_interface'] = conf.return_value('local-interface') + if conf.exists('source interface'): + bfd_peer['src_if'] = conf.return_value('source interface') # Check if peer has a local source address configured - this is mandatory for IPv6 - if conf.exists('local-address'): - bfd_peer['local_address'] = conf.return_value('local-address') + if conf.exists('source address'): + bfd_peer['src_addr'] = conf.return_value('source address') # Tell BFD daemon that we should expect packets with TTL less than 254 # (because it will take more than one hop) and to listen on the multihop @@ -102,7 +102,7 @@ def verify(bfd): # IPv6 peers require an explicit local address/interface combination if vyos.validate.is_ipv6(peer['remote']): - if not (peer['local_interface'] and peer['local_address']): + if not (peer['src_if'] and peer['src_addr']): raise ConfigError("BFD IPv6 peers require explicit local address/interface setting") return None -- cgit v1.2.3 From 4e4b945b6b88308fe8938663ad12efebf98e08fd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 14:51:44 +0200 Subject: bfd: T1183: add support to configure detection multiplier Configures the detection multiplier to determine packet loss. The remote transmission interval will be multiplied by this value to determine the connection loss detection timer. The default value is 3. Example: when the local system has detect-multiplier 3 and the remote system has transmission interval 300, the local system will detect failures only after 900 milliseconds without receiving packets. --- interface-definitions/protocols-bfd.xml | 12 ++++++++++++ src/conf_mode/protocols_bfd.py | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index ab8c9e233..a731334a0 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -50,6 +50,18 @@ + + + Multiplier to determine packet loss + + 2-255 + Remote transmission interval will be multiplied by this value + + + + + + Disable this peer diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 6d2b8382a..96a41de11 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -36,6 +36,7 @@ bfd ! {% for peer in new_peers -%} peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} + detect-multiplier {{ peer.multiplier }} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor -%} ! @@ -66,6 +67,7 @@ def get_config(): 'shutdown': False, 'src_if': '', 'src_addr': '', + 'multiplier': '3', 'multihop': False } @@ -81,6 +83,12 @@ def get_config(): if conf.exists('source address'): bfd_peer['src_addr'] = conf.return_value('source address') + # Configures the detection multiplier to determine packet loss. The remote + # transmission interval will be multiplied by this value to determine the + # connection loss detection timer. The default value is 3. + if conf.exists('multiplier'): + bfd_peer['multiplier'] = conf.return_value('multiplier') + # Tell BFD daemon that we should expect packets with TTL less than 254 # (because it will take more than one hop) and to listen on the multihop # port (4784) -- cgit v1.2.3 From 67a35cfc37a5cc34a1b874f69626802ec3d35f94 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 14:59:27 +0200 Subject: bfd: T1183: multihop doesn't accept interface names --- src/conf_mode/protocols_bfd.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 96a41de11..3d60f5358 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -111,7 +111,12 @@ def verify(bfd): # IPv6 peers require an explicit local address/interface combination if vyos.validate.is_ipv6(peer['remote']): if not (peer['src_if'] and peer['src_addr']): - raise ConfigError("BFD IPv6 peers require explicit local address/interface setting") + raise ConfigError('BFD IPv6 peers require explicit local address/interface setting') + + # multihop doesn't accept interface names + if peer['multihop'] and peer['src_if']: + raise ConfigError('multihop does not accept interface names') + return None -- cgit v1.2.3 From 8b8c7424c90275a3814e7c17939cfb3a66145a19 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 15:18:31 +0200 Subject: bfd: T1183: add rx/tx interval configuration vyos@vyos# show protocols bfd { peer 1.1.1.1 { interval { receive 400 transmit 300 } } } --- interface-definitions/protocols-bfd.xml | 31 +++++++++++++++++++++++++++++++ src/conf_mode/protocols_bfd.py | 14 ++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index a731334a0..0e92a7ddd 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -62,6 +62,37 @@ + + + Configure timer intervals + + + + + Minimum interval of receiving control packets + + 10-60000 + Interval in milliseconds + + + + + + + + + Minimum interval of transmitting control packets + + 10-60000 + Interval in milliseconds + + + + + + + + Disable this peer diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 3d60f5358..2f494c2e4 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -37,6 +37,8 @@ bfd {% for peer in new_peers -%} peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} detect-multiplier {{ peer.multiplier }} + receive-interval {{ peer.rx_interval }} + transmit-interval {{ peer.tx_interval }} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor -%} ! @@ -68,6 +70,8 @@ def get_config(): 'src_if': '', 'src_addr': '', 'multiplier': '3', + 'rx_interval': '300', + 'tx_interval': '300', 'multihop': False } @@ -95,6 +99,16 @@ def get_config(): if conf.exists('multihop'): bfd_peer['multihop'] = True + # Configures the minimum interval that this system is capable of receiving + # control packets. The default value is 300 milliseconds. + if conf.exists('interval receive'): + bfd_peer['rx_interval'] = conf.return_value('interval receive') + + # The minimum transmission interval (less jitter) that this system wants + # to use to send BFD control packets. + if conf.exists('interval transmit'): + bfd_peer['tx_interval'] = conf.return_value('interval transmit') + bfd['new_peers'].append(bfd_peer) return bfd -- cgit v1.2.3 From 7d5b78242859540955006e11d8ca08b463244950 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 22 Jun 2019 15:21:08 +0200 Subject: bfd: T1183: move "multiplier" configuration node to "interval multiplier" --- interface-definitions/protocols-bfd.xml | 24 ++++++++++++------------ src/conf_mode/protocols_bfd.py | 12 ++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index 0e92a7ddd..47d5bf97d 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -50,18 +50,6 @@ - - - Multiplier to determine packet loss - - 2-255 - Remote transmission interval will be multiplied by this value - - - - - - Configure timer intervals @@ -91,6 +79,18 @@ + + + Multiplier to determine packet loss + + 2-255 + Remote transmission interval will be multiplied by this value + + + + + + diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 2f494c2e4..04549f4b4 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -87,12 +87,6 @@ def get_config(): if conf.exists('source address'): bfd_peer['src_addr'] = conf.return_value('source address') - # Configures the detection multiplier to determine packet loss. The remote - # transmission interval will be multiplied by this value to determine the - # connection loss detection timer. The default value is 3. - if conf.exists('multiplier'): - bfd_peer['multiplier'] = conf.return_value('multiplier') - # Tell BFD daemon that we should expect packets with TTL less than 254 # (because it will take more than one hop) and to listen on the multihop # port (4784) @@ -109,6 +103,12 @@ def get_config(): if conf.exists('interval transmit'): bfd_peer['tx_interval'] = conf.return_value('interval transmit') + # Configures the detection multiplier to determine packet loss. The remote + # transmission interval will be multiplied by this value to determine the + # connection loss detection timer. The default value is 3. + if conf.exists('interval multiplier'): + bfd_peer['multiplier'] = conf.return_value('interval multiplier') + bfd['new_peers'].append(bfd_peer) return bfd -- cgit v1.2.3 From 690ae8bf526b6d45997bedf5e856f858ad251658 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sat, 22 Jun 2019 01:52:01 +0200 Subject: T1433: fix also filenames in /etc/default/isc-dhcpv6-server --- src/conf_mode/dhcpv6_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index aa9c35fa1..37da3ef25 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -112,8 +112,8 @@ daemon_tmpl = """ # sourced by /etc/init.d/isc-dhcpv6-server -DHCPD_CONF=/etc/dhcp/dhcpd6.conf -DHCPD_PID=/var/run/dhcpd6.pid +DHCPD_CONF=/etc/dhcp/dhcpdv6.conf +DHCPD_PID=/var/run/dhcpdv6.pid OPTIONS="-6 -lf {{ lease_file }}" INTERFACES="" """ -- cgit v1.2.3 From 7773ad30bd940ffb5144224d61dc3354396f2c8b Mon Sep 17 00:00:00 2001 From: qiuchengxuan Date: Sat, 22 Jun 2019 21:51:17 +0800 Subject: [pdns-recursor] T1469 - replace forward-zones with forward-zones-recurse (#75) forward-zones-recurse behaves identically to dnsmasq server option in legacy vyos 1.1.8, while forward-zones option disallow recursive name resolving, which leads to dns lookup failure --- src/conf_mode/dns_forwarding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 7559a0af6..0ce2eee2c 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -63,7 +63,7 @@ local-address={{ listen_on | join(',') }} # domain ... server ... {% if domains -%} -forward-zones={% for d in domains %} +forward-zones-recurse={% for d in domains %} {{ d.name }}={{ d.servers | join(";") }} {{- "," if not loop.last -}} {% endfor %} -- 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(-) 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(-) 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 99cca9bea1a23c396b4b3121f759b3e21240fbd0 Mon Sep 17 00:00:00 2001 From: qiuchengxuan Date: Tue, 25 Jun 2019 15:55:55 +0800 Subject: [pdns-recursor] T1469 - specified dns forwarding not work when conflict exists between forward-zone-recurse entry, the lower one hides the upper one, which leads to inactive dns forwarding configuration --- src/conf_mode/dns_forwarding.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 0ce2eee2c..c7e362d07 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -60,16 +60,6 @@ export-etc-hosts={{ export_hosts_file }} # listen-on local-address={{ listen_on | join(',') }} -# domain ... server ... -{% if domains -%} - -forward-zones-recurse={% for d in domains %} -{{ d.name }}={{ d.servers | join(";") }} -{{- "," if not loop.last -}} -{% endfor %} - -{% endif %} - # dnssec dnssec={{ dnssec }} @@ -80,6 +70,16 @@ forward-zones-recurse=.={{ name_servers | join(';') }} # no name-servers specified - start full recursor {% endif %} +# domain ... server ... +{% if domains -%} + +forward-zones-recurse={% for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{{- "," if not loop.last -}} +{% endfor %} + +{% endif %} + """ default_config_data = { -- 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 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(-) 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 b23c28dfd0e296987d7f65c2a178e6ed0fde3983 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 28 Jun 2019 09:54:25 -0700 Subject: [IPoE] if authentication is local use .lower() for mac addresses --- src/conf_mode/ipoe_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py index 39f0cb279..470ef7ee6 100755 --- a/src/conf_mode/ipoe_server.py +++ b/src/conf_mode/ipoe_server.py @@ -231,6 +231,7 @@ def get_config(): 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'): + mac = mac.lower() 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] = {} -- cgit v1.2.3 From 2df12d1616c3f63c5db5a76ab315d06fa7d5d190 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 28 Jun 2019 10:20:06 -0700 Subject: [IPoE] configerror message fixed to show the interface where subnet is missing --- src/conf_mode/ipoe_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py index 470ef7ee6..4ecff2e8c 100755 --- a/src/conf_mode/ipoe_server.py +++ b/src/conf_mode/ipoe_server.py @@ -295,7 +295,7 @@ def verify(c): for intfc in c['interfaces']: if not c['interfaces'][intfc]['range']: - raise ConfigError("service ipoe-server interface eth2 client-subnet needs a value") + raise ConfigError("service ipoe-server interface " + intfc + " client-subnet needs a value") def apply(c): if c == None: -- cgit v1.2.3 From b83fed095c418d27a08309af2d6bf50c11505117 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 28 Jun 2019 12:39:40 -0700 Subject: [IPoE] config structure improved * fixed minor issues * fixed lower function for mac addresses if user capitalized it (local mode only) * added some checks to verify() * cli ip-address checks on input --- interface-definitions/ipoe-server.xml | 21 ++++ src/conf_mode/ipoe_server.py | 177 ++++++++++++++++++++-------------- 2 files changed, 125 insertions(+), 73 deletions(-) diff --git a/interface-definitions/ipoe-server.xml b/interface-definitions/ipoe-server.xml index 18968a033..4884b5915 100644 --- a/interface-definitions/ipoe-server.xml +++ b/interface-definitions/ipoe-server.xml @@ -244,6 +244,13 @@ 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 + IPv4 address of the DAE Server + + + + @@ -254,11 +261,25 @@ IP address for Dynamic Authorization Extension server (DM/CoA) + + ipv4 + IPv4 address of the DAE Server + + + + Port for Dynamic Authorization Extension server (DM/CoA) + + number + port number + + + + diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py index 4ecff2e8c..478fc139e 100755 --- a/src/conf_mode/ipoe_server.py +++ b/src/conf_mode/ipoe_server.py @@ -44,10 +44,10 @@ log_syslog ippool ipoe shaper -{% if auth == 'radius' %} +{% if auth['mech'] == 'radius' %} radius {% endif -%} -{% if auth == 'local' %} +{% if auth['mech'] == 'local' %} chap-secrets {% endif %} @@ -69,10 +69,10 @@ ifcfg={{interfaces[intfc]['ifcfg']}},\ range={{interfaces[intfc]['range']}},\ start={{interfaces[intfc]['sess_start']}} {% endfor %} -{% if auth == 'noauth' %} +{% if auth['mech'] == 'noauth' %} noauth=1 {% endif %} -{% if auth == 'local' %} +{% if auth['mech'] == 'local' %} username=ifname password=csid {% endif %} @@ -85,35 +85,40 @@ dns1={{dns['server1']}} {% if dns['server2'] %} dns2={{dns['server2']}} {% endif -%} -{% endif %} +{% endif -%} -{% if auth == 'local' %} +{% if auth['mech'] == 'local' %} [chap-secrets] chap-secrets=/etc/accel-ppp/ipoe/chap-secrets {% endif %} -{% if auth == 'radius' %} +{% if auth['mech'] == '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']}} +{% for srv in auth['radius'] %} +server={{srv}},{{auth['radius'][srv]['secret']}},\ +req-limit={{auth['radius'][srv]['req-limit']}},\ +fail-time={{auth['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']}} +{% if auth['radsettings']['dae-server']['ip-address'] %} +dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\ +{{auth['radsettings']['dae-server']['port']}},\ +{{auth['radsettings']['dae-server']['secret']}} +{% endif -%} +{% if auth['radsettings']['acct-timeout'] %} +acct-timeout={{auth['radsettings']['acct-timeout']}} {% endif -%} -{% if radsettings['acct-timeout'] %} -acct-timeout={{radsettings['acct-timeout']}} +{% if auth['radsettings']['max-try'] %} +max-try={{auth['radsettings']['max-try']}} {% endif -%} -{% if radsettings['max-try'] %} -max-try={{radsettings['max-try']}} +{% if auth['radsettings']['timeout'] %} +timeout={{auth['radsettings']['timeout']}} {% endif -%} -{% if radsettings['nas-ip-address'] %} -nas-ip-address={{radsettings['nas-ip-address']}} +{% if auth['radsettings']['nas-ip-address'] %} +nas-ip-address={{auth['radsettings']['nas-ip-address']}} {% endif -%} -{% if radsettings['nas-identifier'] %} -nas-identifier={{radsettings['nas-identifier']}} +{% if auth['radsettings']['nas-identifier'] %} +nas-identifier={{auth['radsettings']['nas-identifier']}} {% endif -%} {% endif %} @@ -124,12 +129,12 @@ 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']}} +{% for aifc in auth['auth_if'] %} +{% for mac in auth['auth_if'][aifc] %} +{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %} +{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} {% else %} -{{aifc}}\t*\t{{mac}}\t* +{{aifc}}\t*\t{{mac.lower()}}\t* {% endif %} {% endfor %} {% endfor %} @@ -191,30 +196,27 @@ def get_config(): config_data = {} c.set_level('service ipoe-server') + config_data['interfaces'] = {} 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' : {} - } + config_data['interfaces'][intfc] = { + 'mode' : 'L2', + 'shared' : '1', + 'sess_start' : 'dhcpv4', ### may need a conifg option, can be dhcpv4 or up for unclassified pkts + 'range' : None, + 'ifcfg' : '1' + } + config_data['dns'] = { + 'server1' : None, + 'server2' : None + } + config_data['auth'] = { + 'auth_if' : {}, + 'mech' : 'noauth', + 'radius' : {}, + 'radsettings' : { + 'dae-server' : {} } - ) + } if c.exists('interface ' + intfc + ' network-mode'): config_data['interfaces'][intfc]['mode'] = c.return_value('interface ' + intfc + ' network-mode') @@ -227,50 +229,51 @@ def get_config(): 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') + config_data['auth']['mech'] = 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'): - mac = mac.lower() - config_data['auth_if'][auth_int] = {} + config_data['auth']['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') + config_data['auth']['auth_if'][auth_int][mac] = {} + config_data['auth']['auth_if'][auth_int][mac]['up'] = c.return_value('authentication interface ' + auth_int + ' mac-address ' + mac + ' rate-limit upload') + config_data['auth']['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 + config_data['auth']['auth_if'][auth_int][mac] = {} + config_data['auth']['auth_if'][auth_int][mac]['up'] = None + config_data['auth']['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] = {} + config_data['auth']['radius'][rsrv] = {} if c.exists('authentication radius-server ' + rsrv + ' secret'): - config_data['radius'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') + config_data['auth']['radius'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') + else: + config_data['auth']['radius'][rsrv]['secret'] = None if c.exists('authentication radius-server ' + rsrv + ' fail-time'): - config_data['radius'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') + config_data['auth']['radius'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') else: - config_data['radius'][rsrv]['fail-time'] = '0' + config_data['auth']['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') + config_data['auth']['radius'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit') else: - config_data['radius'][rsrv]['req-limit'] = '0' + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['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') + config_data['auth']['radsettings']['dae-server']['secret'] = c.return_value('authentication radius-settings dae-server secret') return config_data @@ -280,7 +283,7 @@ def generate(c): c['thread_cnt'] = get_cpu() - if c['auth'] == 'local': + if c['auth']['mech'] == 'local': gen_chap_secrets(c) tmpl = jinja2.Template(ipoe_config, trim_blocks=True) @@ -295,7 +298,35 @@ def verify(c): for intfc in c['interfaces']: if not c['interfaces'][intfc]['range']: - raise ConfigError("service ipoe-server interface " + intfc + " client-subnet needs a value") + raise ConfigError("service ipoe-server interface " + intfc + " client-subnet needs a value") + + if c['auth']['mech'] == 'radius': + if not c['auth']['radius']: + raise ConfigError("service ipoe-server authentication radius-server requires a value for authentication mode radius") + else: + for radsrv in c['auth']['radius']: + if not c['auth']['radius'][radsrv]['secret']: + raise ConfigError("service ipoe-server authentication radius-server " + radsrv + " secret requires a value") + + if c['auth']['radsettings']['dae-server']: + try: + if c['auth']['radsettings']['dae-server']['ip-address']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server ip-address value required") + try: + if c['auth']['radsettings']['dae-server']['secret']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server secret value required") + try: + if c['auth']['radsettings']['dae-server']['port']: + pass + except: + raise ConfigError("service ipoe-server authentication radius-settings dae-server port value required") + + + return c def apply(c): if c == None: -- cgit v1.2.3 From d776db396239be57f8566720cc564751eeb9ffd5 Mon Sep 17 00:00:00 2001 From: Kim Hagen Date: Mon, 1 Jul 2019 15:41:56 +0200 Subject: T1498: Nameservers are not propagated into resolv.conf --- interface-definitions/dns-domain-name.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/dns-domain-name.xml b/interface-definitions/dns-domain-name.xml index a2c66495f..c16f0b02a 100644 --- a/interface-definitions/dns-domain-name.xml +++ b/interface-definitions/dns-domain-name.xml @@ -3,7 +3,7 @@ - + Domain Name Server (DNS) 400 -- cgit v1.2.3 From 2abd3cf50c6fedf79ce0f01337ef8bb1eb44116e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 1 Jul 2019 13:44:34 -0500 Subject: [HTTP API] T1431: check init/vyos-config before starting HTTP API service --- src/systemd/vyos-http-api.service | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service index 509af4816..4fa68b4ff 100644 --- a/src/systemd/vyos-http-api.service +++ b/src/systemd/vyos-http-api.service @@ -1,9 +1,12 @@ [Unit] Description=VyOS HTTP API service -After=auditd.service systemd-user-sessions.service time-sync.target +After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service +Requires=vyos-router.service [Service] +ExecStartPre=/usr/libexec/vyos/init/vyos-config ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server +Type=idle KillMode=process SyslogIdentifier=vyos-http-api @@ -16,5 +19,6 @@ User=root Group=vyattacfg [Install] -WantedBy=multi-user.target +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target -- cgit v1.2.3 From d56d52990f5b30a1b03b2479767e91aa3aa2cdc5 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 1 Jul 2019 13:57:52 -0500 Subject: [service https] T1443: add service https and service https api --- debian/rules | 3 + interface-definitions/https.xml | 87 ++++++++++++++++++++++++++ python/vyos/defaults.py | 1 + src/conf_mode/http-api.py | 104 +++++++++++++++++++++++++++++++ src/conf_mode/https.py | 132 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 interface-definitions/https.xml create mode 100755 src/conf_mode/http-api.py create mode 100755 src/conf_mode/https.py diff --git a/debian/rules b/debian/rules index b06117922..952867a76 100755 --- a/debian/rules +++ b/debian/rules @@ -77,3 +77,6 @@ override_dh_auto_install: # Install systemd service units mkdir -p $(DIR)/lib/systemd/system cp -r src/systemd/* $(DIR)/lib/systemd/system + + # Make directory for generated configuration file + mkdir -p $(DIR)/etc/vyos diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml new file mode 100644 index 000000000..828de449c --- /dev/null +++ b/interface-definitions/https.xml @@ -0,0 +1,87 @@ + + + + + + + + HTTPS configuration + 1001 + + + + + Addresses to listen for HTTPS requests + + ipv4 + HTTPS IPv4 address + + + ipv6 + HTTPS IPv6 address + + + + + + + + + + + VyOS HTTP API configuration + 1002 + + + + + Port for HTTP API service + + 1-65535 + Numeric IP port + + + + + + + + + HTTP API keys + + + + + HTTP API id + + + + + HTTP API plaintext key + + + + + + + + + Enforce strict path checking + + + + + + Debug + + + + + + + + + + + + diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index da363b8e1..f23e15631 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -16,6 +16,7 @@ directories = { "data": "/usr/share/vyos/", + "conf_mode": "/usr/libexec/vyos/conf_mode", "config": "/opt/vyatta/etc/config", "current": "/opt/vyatta/etc/config-migrate/current", "migrate": "/opt/vyatta/etc/config-migrate/migrate", diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py new file mode 100755 index 000000000..7d618dded --- /dev/null +++ b/src/conf_mode/http-api.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 subprocess +import json + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError + +config_file = '/etc/vyos/http-api.conf' + +default_config_data = { + 'listen_address' : '127.0.0.1', + 'port' : '8080', + 'strict' : 'false', + 'debug' : 'false', + 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] +} + +vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] + +# XXX: this model will need to be extended for tag nodes +dependencies = [ + 'https.py', +] + +def get_config(): + http_api = default_config_data + conf = Config() + if not conf.exists('service https api'): + return None + else: + conf.set_level('service https api') + + if conf.exists('strict'): + http_api['strict'] = 'true' + + if conf.exists('debug'): + http_api['debug'] = 'true' + + if conf.exists('port'): + port = conf.return_value('port') + http_api['port'] = port + + if conf.exists('keys'): + for name in conf.list_nodes('keys id'): + if conf.exists('keys id {0} key'.format(name)): + key = conf.return_value('keys id {0} key'.format(name)) + new_key = { 'id': name, 'key': key } + http_api['api_keys'].append(new_key) + + return http_api + +def verify(http_api): + return None + +def generate(http_api): + if http_api is None: + return None + + with open(config_file, 'w') as f: + json.dump(http_api, f, indent=2) + + return None + +def apply(http_api): + if http_api is not None: + os.system('sudo systemctl restart vyos-http-api.service') + for dep in dependencies: + cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + raise ConfigError("{}.".format(err)) + else: + os.system('sudo systemctl stop vyos-http-api.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py new file mode 100755 index 000000000..dae51dd7d --- /dev/null +++ b/src/conf_mode/https.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = '/etc/nginx/sites-available/default' + +# Please be careful if you edit the template. +config_tmpl = """ + +### Autogenerated by http-api.py ### +# Default server configuration +# +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 302 https://$server_name$request_uri; +} + +server { + + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; + +{% for l_addr in listen_address %} + server_name {{ l_addr }}; +{% endfor %} + + location / { +{% if api %} + proxy_pass http://localhost:{{ api.port }}; + proxy_buffering off; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } + +} +""" + +default_config_data = { + 'listen_address' : [ '127.0.0.1' ] +} + +default_api_config_data = { + 'port' : '8080', +} + +def get_config(): + https = default_config_data + conf = Config() + if not conf.exists('service https'): + return None + else: + conf.set_level('service https') + + if conf.exists('listen-address'): + addrs = conf.return_values('listen-address') + https['listen_address'] = addrs[:] + + if conf.exists('api'): + https['api'] = default_api_config_data + + if conf.exists('api port'): + port = conf.return_value('api port') + https['api']['port'] = port + + return https + +def verify(https): + return None + +def generate(https): + if https is None: + return None + + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + config_text = tmpl.render(https) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(https): + if https is not None: + os.system('sudo systemctl restart nginx.service') + else: + os.system('sudo systemctl stop nginx.service') + +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 6c2b62eafa0e2224fdc492c55d255228593ad960 Mon Sep 17 00:00:00 2001 From: UnicronNL Date: Tue, 2 Jul 2019 20:49:06 +0200 Subject: T1497: "set system name-server" generates invalid/incorrect resolv.conf --- src/conf_mode/host_name.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index b0a4648c7..43f36dd35 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -124,7 +124,10 @@ def get_config(arguments): hosts['domain_search'].append(search) if conf.exists("system name-server"): - hosts['nameserver'] = conf.return_values("system name-server") + if not isinstance(conf.return_values("system name-server"), list): + hosts['nameserver'] = conf.return_values("system name-server").replace("'", "").split() + else: + hosts['nameserver'] = conf.return_values("system name-server") if conf.exists("system disable-dhcp-nameservers"): hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') -- cgit v1.2.3 From c35d1b7a1d958327f67c806740428929ff86b151 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 2 Jul 2019 13:09:11 -0700 Subject: [IPoE] T1495 - IA-PD via IPoE implemented --- interface-definitions/ipoe-server.xml | 52 ++++++++++++++++++++++++++++++++++- src/conf_mode/ipoe_server.py | 52 +++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/interface-definitions/ipoe-server.xml b/interface-definitions/ipoe-server.xml index 4884b5915..46ac2357a 100644 --- a/interface-definitions/ipoe-server.xml +++ b/interface-definitions/ipoe-server.xml @@ -107,7 +107,7 @@ - IP address of the primary DNS server + IP address of the secondary DNS server @@ -115,6 +115,56 @@ + + + DNSv6 servers offered via internal DHCPv6 + + + + + IP address of the primary DNS server + + + + + + + + IP address of the secondary DNS server + + + + + + + + IP address of the tertiary DNS server + + + + + + + + + + Pool of client IPv6 addresses + + + + + Format: ipv6prefix/mask,prefix_len (e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients) + + + + + + Format: ipv6prefix/mask,prefix_len (delegates prefix to clients via DHCPv6 prefix delegation + + + + + Client authentication methods diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py index 478fc139e..45c64c617 100755 --- a/src/conf_mode/ipoe_server.py +++ b/src/conf_mode/ipoe_server.py @@ -44,6 +44,9 @@ log_syslog ippool ipoe shaper +ipv6pool +ipv6_nd +ipv6_dhcp {% if auth['mech'] == 'radius' %} radius {% endif -%} @@ -67,7 +70,8 @@ shared={{interfaces[intfc]['shared']}},\ mode={{interfaces[intfc]['mode']}},\ ifcfg={{interfaces[intfc]['ifcfg']}},\ range={{interfaces[intfc]['range']}},\ -start={{interfaces[intfc]['sess_start']}} +start={{interfaces[intfc]['sess_start']}},\ +ipv6=1 {% endfor %} {% if auth['mech'] == 'noauth' %} noauth=1 @@ -87,6 +91,29 @@ dns2={{dns['server2']}} {% endif -%} {% endif -%} +{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %} +[dnsv6] +dns={{dnsv6['server1']}} +dns={{dnsv6['server2']}} +dns={{dnsv6['server3']}} +{% endif %} + +[ipv6-nd] +verbose=1 + +[ipv6-dhcp] +verbose=1 + +{% if ipv6['prfx'] %} +[ipv6-pool] +{% for prfx in ipv6['prfx'] %} +{{prfx}} +{% endfor %} +{% for pd in ipv6['pd'] %} +delegate={{pd}} +{% endfor %} +{% endif %} + {% if auth['mech'] == 'local' %} [chap-secrets] chap-secrets=/etc/accel-ppp/ipoe/chap-secrets @@ -209,6 +236,15 @@ def get_config(): 'server1' : None, 'server2' : None } + config_data['dnsv6'] = { + 'server1' : None, + 'server2' : None, + 'server3' : None + } + config_data['ipv6'] = { + 'prfx' : [], + 'pd' : [], + } config_data['auth'] = { 'auth_if' : {}, 'mech' : 'noauth', @@ -228,6 +264,12 @@ def get_config(): 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 c.exists('dnsv6-server server-1'): + config_data['dnsv6']['server1'] = c.return_value('dnsv6-server server-1') + if c.exists('dnsv6-server server-2'): + config_data['dnsv6']['server2'] = c.return_value('dnsv6-server server-2') + if c.exists('dnsv6-server server-3'): + config_data['dnsv6']['server3'] = c.return_value('dnsv6-server server-3') if not c.exists('authentication mode noauth'): config_data['auth']['mech'] = c.return_value('authentication mode') if c.exists('authentication mode local'): @@ -274,6 +316,11 @@ def get_config(): config_data['auth']['radsettings']['dae-server']['port'] = c.return_value('authentication radius-settings dae-server port') if c.exists('authentication radius-settings dae-server secret'): config_data['auth']['radsettings']['dae-server']['secret'] = c.return_value('authentication radius-settings dae-server secret') + + if c.exists('client-ipv6-pool prefix'): + config_data['ipv6']['prfx'] = c.return_values('client-ipv6-pool prefix') + if c.exists('client-ipv6-pool delegate-prefix'): + config_data['ipv6']['pd'] = c.return_values('client-ipv6-pool delegate-prefix') return config_data @@ -288,7 +335,6 @@ def generate(c): tmpl = jinja2.Template(ipoe_config, trim_blocks=True) config_text = tmpl.render(c) - open(ipoe_cnf,'w').write(config_text) return c @@ -325,6 +371,8 @@ def verify(c): except: raise ConfigError("service ipoe-server authentication radius-settings dae-server port value required") + if len(c['ipv6']['pd']) != 0 and len(c['ipv6']['prfx']) == 0: + raise ConfigError("service ipoe-server client-ipv6-pool prefix needs a value") return c -- cgit v1.2.3 From c1fdee12f94dcf4395992152358d03cb8c74f155 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 3 Jul 2019 03:35:31 +0200 Subject: T1503: add functions for commit lock checking and waiting. --- python/vyos/defaults.py | 2 ++ python/vyos/util.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index f23e15631..524b80424 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -25,3 +25,5 @@ directories = { cfg_group = 'vyattacfg' cfg_vintage = 'vyatta' + +commit_lock = '/opt/vyatta/config/.lock' diff --git a/python/vyos/util.py b/python/vyos/util.py index 8b5342575..6ab606983 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -1,4 +1,4 @@ -# Copyright 2018 VyOS maintainers and contributors +# Copyright 2019 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +16,9 @@ import os import re import grp +import time +import subprocess + import psutil import vyos.defaults @@ -131,3 +134,45 @@ def file_is_persistent(path): return (False, warning) else: return (True, None) + +def commit_in_progress(): + """ Not to be used in normal op mode scripts! """ + + # The CStore backend locks the config by opening a file + # The file is not removed after commit, so just checking + # if it exists is insufficient, we need to know if it's open by anyone + + # There are two ways to check if any other process keeps a file open. + # The first one is to try opening it and see if the OS objects. + # That's faster but prone to race conditions and can be intrusive. + # The other one is to actually check if any process keeps it open. + # It's non-intrusive but needs root permissions, else you can't check + # processes of other users. + # + # Since this will be used in scripts that modify the config outside of the CLI + # framework, those knowingly have root permissions. + # For everything else, we add a safeguard. + id = subprocess.check_output(['/usr/bin/id', '-u']).decode().strip() + if id != '0': + raise OSError("This functions needs root permissions to return correct results") + + for proc in psutil.process_iter(): + try: + files = proc.open_files() + if files: + for f in files: + if f.path == vyos.defaults.commit_lock: + return True + except psutil.NoSuchProcess as err: + # Process died before we could examine it + pass + # Default case + return False + +def wait_for_commit_lock(): + """ Not to be used in normal op mode scripts! """ + + # Very synchronous approach to multiprocessing + while commit_in_progress(): + time.sleep(1) + -- cgit v1.2.3 From 65f5e295c3dbe72ca3df831c552d7bc92389c958 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 3 Jul 2019 04:00:50 +0200 Subject: T1504: wait for commit lock before trying to update resolv.conf in the out of CLI mode. --- src/conf_mode/dns_forwarding.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 0ce2eee2c..b9a5b99e9 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -23,6 +23,8 @@ import argparse import jinja2 import netifaces +import vyos.util + from vyos.config import Config from vyos import ConfigError @@ -265,6 +267,12 @@ def apply(dns): if __name__ == '__main__': args = parser.parse_args() + + if args.dhclient: + # There's a big chance it was triggered by a commit still in progress + # so we need to wait until the new values are in the running config + vyos.util.wait_for_commit_lock() + try: c = get_config(args) verify(c) -- cgit v1.2.3 From 3f7322d7f15ac348f1d06ba411c935956e276a76 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 3 Jul 2019 12:40:27 +0200 Subject: [vyos.config] T1505: correct return_effective_values output splitting. --- python/vyos/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/vyos/config.py b/python/vyos/config.py index 96e9631e0..c9c73b971 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -405,7 +405,8 @@ class Config(object): else: try: out = self._run(self._make_command('returnEffectiveValues', full_path)) - return out + values = re.findall(r"\'(.*?)\'", out) + return values except VyOSError: return(default) -- cgit v1.2.3 From acd5f855bcca98008719d0ef371be3076861b4f1 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 3 Jul 2019 12:41:33 +0200 Subject: T1497: remove the no longer necessary workaround for bad return_effective_values output. --- src/conf_mode/host_name.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 43f36dd35..b0a4648c7 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -124,10 +124,7 @@ def get_config(arguments): hosts['domain_search'].append(search) if conf.exists("system name-server"): - if not isinstance(conf.return_values("system name-server"), list): - hosts['nameserver'] = conf.return_values("system name-server").replace("'", "").split() - else: - hosts['nameserver'] = conf.return_values("system name-server") + hosts['nameserver'] = conf.return_values("system name-server") if conf.exists("system disable-dhcp-nameservers"): hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') -- cgit v1.2.3 From 6b759f81fec573859798a6d3f971bfaa0b1960c6 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 3 Jul 2019 13:29:52 +0200 Subject: T1497: make host_name.py wait for commit lock too. --- src/conf_mode/host_name.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index b0a4648c7..6fb7031a8 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -29,6 +29,8 @@ import subprocess import argparse import jinja2 +import vyos.util + from vyos.config import Config from vyos import ConfigError @@ -273,6 +275,13 @@ def apply(config): if __name__ == '__main__': args = parser.parse_args() + + if args.dhclient: + # There's a big chance it was triggered by a commit still in progress + # so we need to wait until the new values are in the running config + vyos.util.wait_for_commit_lock() + + try: c = get_config(args) verify(c) -- cgit v1.2.3 From 377c04cbd7c11f3288664f9e64a95ee8fda23457 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Thu, 4 Jul 2019 12:15:52 +0200 Subject: T1435 plus other dhcp/dhcpv6-server enhancements - T1435: dhcp-server: make ip-address optional in static-mapping - remove [REQUIRED] from dhcpv6-server static-mapping identifier and ipv6-address - verify if static-mapping ipv6-address is in subnet - make help and error messages in conf-mode more descriptive - remove regex ^$ anchors (implied in re.fullmatch) --- interface-definitions/dhcp-server.xml | 41 ++++++++++++++++++--------------- interface-definitions/dhcpv6-server.xml | 29 ++++++++++++----------- src/conf_mode/dhcp_server.py | 21 ++++++++--------- src/conf_mode/dhcpv6_server.py | 16 +++++++++++++ 4 files changed, 64 insertions(+), 43 deletions(-) diff --git a/interface-definitions/dhcp-server.xml b/interface-definitions/dhcp-server.xml index 87999f496..7d42294e8 100644 --- a/interface-definitions/dhcp-server.xml +++ b/interface-definitions/dhcp-server.xml @@ -46,9 +46,9 @@ DHCP shared network name [REQUIRED] - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid DHCP pool name + Invalid shared network name. May only contain letters, numbers and .-_ @@ -151,7 +151,7 @@ - IP address that needs to be excluded from DHCP lease range + IP address to exclude from DHCP lease range ipv4 IPv4 address to exclude from lease range @@ -183,9 +183,9 @@ DHCP failover peer name [REQUIRED] - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid failover peer name + Invalid failover peer name. May only contain letters, numbers and .-_ @@ -193,7 +193,7 @@ IP address of failover peer [REQUIRED] ipv4 - IPv4 address to exclude from lease range + IPv4 address of failover peer @@ -225,12 +225,12 @@ Lease timeout in seconds (default: 86400) 0-4294967295 - DHCP lease time in seconds must be between 0 and 4294967295 (49 days) + DHCP lease time in seconds - DHCP lease time must be 0 to 4294967295 + DHCP lease time must be between 0 and 4294967295 (49 days) @@ -288,9 +288,9 @@ DHCP lease range - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid DHCP lease range name + Invalid DHCP lease range name. May only contain letters, numbers and .-_ @@ -321,22 +321,22 @@ - Static mapping for specified address type + Name of static mapping - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid static-mapping name + Invalid static mapping name. May only contain letters, numbers and .-_ - Option to disable static-mapping + Option to disable static mapping - Static mapping for specified IP address [REQUIRED] + Fixed IP address of static mapping ipv4 IPv4 address used in static mapping @@ -348,7 +348,7 @@ - Static mapping for specified MAC address [REQUIRED] + MAC address of static mapping [REQUIRED] h:h:h:h:h:h MAC address used in static mapping [REQUIRED] @@ -358,6 +358,7 @@ Additional static-mapping parameters for DHCP server. + Will be placed inside the "host" block of the mapping. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors. @@ -414,10 +415,14 @@ Offset of the client's subnet in seconds from Coordinated Universal Time (UTC) + + [-]N + Time offset (number, may be negative) + - ^-?[0-9]+$ + -?[0-9]+ - Invalid time offset valuee + Invalid time offset value diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml index e18a58608..28b56a64d 100644 --- a/interface-definitions/dhcpv6-server.xml +++ b/interface-definitions/dhcpv6-server.xml @@ -32,9 +32,9 @@ DHCPv6 shared network name [REQUIRED] - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid DHCPv6 pool name + Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_ @@ -112,9 +112,9 @@ Domain name for client to search - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid domain name syntax + Invalid domain name. May only contain letters, numbers and .-_ @@ -157,9 +157,9 @@ NIS domain name for client to use - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid NIS domain name syntax + Invalid NIS domain name @@ -179,9 +179,9 @@ NIS+ domain name for client to use - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid NIS+ domain name syntax + Invalid NIS+ domain name. May only contain letters, numbers and .-_ @@ -260,9 +260,9 @@ SIP server name - ^[-_a-zA-Z0-9.]+$ + [-_a-zA-Z0-9.]+ - Invalid SIP server name syntax + Invalid SIP server name. May only contain letters, numbers and .-_ @@ -281,7 +281,7 @@ [-_a-zA-Z0-9.]+ - Invalid static-mapping name. May only contain letters, numbers and .-_ + Invalid static mapping name. May only contain letters, numbers and .-_ @@ -292,7 +292,7 @@ - Client identifier (DUID) for this static mapping [REQUIRED] + Client identifier (DUID) for this static mapping h[[:h]...] DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id) @@ -300,14 +300,15 @@ ([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2}) + Invalid DUID. Must be in the format h[[:h]...] where each \"h\" is 1 to 2 hex characters. - Client IPv6 address for this static mapping [REQUIRED] + Client IPv6 address for this static mapping ipv6 - IPv6 address for this static mapping [REQUIRED] + IPv6 address for this static mapping diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 78927a847..6d88ea95a 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -187,7 +187,9 @@ shared-network {{ network.name }} { {%- for host in subnet.static_mapping %} {% if not host.disabled -%} host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { + {%- if host.ip_address %} fixed-address {{ host.ip_address }}; + {%- endif %} hardware ethernet {{ host.mac_address }}; {%- if host.static_parameters %} # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated @@ -728,22 +730,19 @@ def verify(dhcp): raise ConfigError('No DHCP address range or active static-mapping set\n' \ 'for subnet {0}!'.format(subnet['network'])) - # Static IP address mappings require both an IP address and MAC address + # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) for mapping in subnet['static_mapping']: - # Static IP address must be configured - if not mapping['ip_address']: - raise ConfigError('DHCP static lease IP address not specified for static mapping\n' \ - '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) - # Static IP address must be in bound - if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ - 'in shared network {2} is outside DHCP lease subnet {3}!' \ - .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) + if mapping['ip_address']: + # Static IP address must be in bound + if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease subnet {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) # Static mapping requires MAC address if not mapping['mac_address']: - raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ + raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) # There must be one subnet connected to a listen interface. diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index f5117de53..d2769466e 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -94,8 +94,12 @@ shared-network {{ network.name }} { {%- for host in subnet.static_mapping %} {% if not host.disabled -%} host {{ network.name }}_{{ host.name }} { + {%- if host.client_identifier %} host-identifier option dhcp6.client-id {{ host.client_identifier }}; + {%- endif %} + {%- if host.ipv6_address %} fixed-address6 {{ host.ipv6_address }}; + {%- endif %} } {%- endif %} {%- endfor %} @@ -384,7 +388,19 @@ def verify(dhcpv6): raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + # Static mappings don't require anything (but check if IP is in subnet if it's set) + for mapping in subnet['static_mapping']: + if mapping['ipv6_address']: + # Static address must be in subnet + if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside subnet {3}!' \ + .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network'])) + # DHCPv6 requires at least one configured address range or one static mapping + # (FIXME: is not actually checked right now?) + + # There must be one subnet connected to a listen interface if network is not disabled. if not network['disabled']: if vyos.validate.is_subnet_connected(subnet['network']): listen_ok = True -- cgit v1.2.3 From f17f897a315d3177c0c42dd3d3b7dadf307a33c8 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 5 Jul 2019 10:21:14 -0700 Subject: [PPPoE] - T1489: vlan_mon config options --- interface-definitions/pppoe-server.xml | 27 +++++++++++++++++++--- src/conf_mode/accel_pppoe.py | 19 +++++++++++----- src/migration-scripts/pppoe-server/1-to-2 | 38 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100755 src/migration-scripts/pppoe-server/1-to-2 diff --git a/interface-definitions/pppoe-server.xml b/interface-definitions/pppoe-server.xml index 18b0e649c..aab830f1e 100644 --- a/interface-definitions/pppoe-server.xml +++ b/interface-definitions/pppoe-server.xml @@ -337,15 +337,36 @@ - + + interface(s) to listen on - - + + + + VLAN monitor for the automatic creation of vlans (user per vlan) + + + + VLAN ID needs to be between 1 and 4096 + + + + + + VLAN monitor for the automatic creation of vlans (user per vlan) + + (409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) + + + + + + local gateway address diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py index 3c7759b17..9c879502a 100755 --- a/src/conf_mode/accel_pppoe.py +++ b/src/conf_mode/accel_pppoe.py @@ -242,13 +242,16 @@ ac-name={{concentrator}} {% if interface %} {% for int in interface %} interface={{int}} -{% endfor %} +{% if interface[int]['vlans'] %} +vlan_mon={{interface[int]['vlans']|join(',')}} +interface=re:{{int}}\.(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) {% endif %} +{% endfor -%} +{% endif -%} {% if svc_name %} service-name={{svc_name}} -{% endif %} +{% endif -%} pado-delay=0 -# maybe: called-sid, tr101, padi-limit etc. {% if limits %} [connlimit] @@ -338,7 +341,7 @@ def get_config(): 'client_ip_pool' : '', 'client_ip_subnets' : [], 'client_ipv6_pool' : {}, - 'interface' : [], + 'interface' : {}, 'ppp_gw' : '', 'svc_name' : '', 'dns' : [], @@ -357,7 +360,12 @@ def get_config(): if c.exists('service-name'): config_data['svc_name'] = c.return_value('service-name') if c.exists('interface'): - config_data['interface'] = c.return_values('interface') + for intfc in c.list_nodes('interface'): + config_data['interface'][intfc] = {'vlans' : []} + if c.exists('interface ' + intfc + ' vlan-id'): + config_data['interface'][intfc]['vlans'] += c.return_values('interface ' + intfc + ' vlan-id') + if c.exists('interface ' + intfc + ' vlan-range'): + config_data['interface'][intfc]['vlans'] +=c.return_values('interface ' + intfc + ' vlan-range') if c.exists('local-ip'): config_data['ppp_gw'] = c.return_value('local-ip') if c.exists('dns-servers'): @@ -491,7 +499,6 @@ def get_config(): 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') diff --git a/src/migration-scripts/pppoe-server/1-to-2 b/src/migration-scripts/pppoe-server/1-to-2 new file mode 100755 index 000000000..fa83896d3 --- /dev/null +++ b/src/migration-scripts/pppoe-server/1-to-2 @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +# Convert "service pppoe-server interface ethX" +# to: +# "service pppoe-server interface ethX {}" + +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() + +ctree = ConfigTree(config_file) +cbase = ['service', 'pppoe-server','interface'] + +if not ctree.exists(cbase): + sys.exit(0) +else: + nics = ctree.return_values(cbase) + # convert leafNode to a tagNode + ctree.set(cbase) + ctree.set_tag(cbase) + for nic in nics: + ctree.set(cbase + [nic]) + + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + -- cgit v1.2.3 From 14b4fa607f37b051957abc9cbe01014a610adc81 Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 8 Jul 2019 12:15:09 -0700 Subject: [IPoE] - T1510: vlan-mon option implementation --- interface-definitions/ipoe-server.xml | 19 +++++++++++++++++++ src/conf_mode/ipoe_server.py | 16 ++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/interface-definitions/ipoe-server.xml b/interface-definitions/ipoe-server.xml index 46ac2357a..6c93d3699 100644 --- a/interface-definitions/ipoe-server.xml +++ b/interface-definitions/ipoe-server.xml @@ -90,6 +90,25 @@ + + + VLAN monitor for the automatic creation of vlans (user per vlan) + + + + VLAN ID needs to be between 1 and 4096 + + + + + + VLAN monitor for the automatic creation of vlans (user per vlan) + + (409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) + + + + diff --git a/src/conf_mode/ipoe_server.py b/src/conf_mode/ipoe_server.py index 45c64c617..ca6b423e5 100755 --- a/src/conf_mode/ipoe_server.py +++ b/src/conf_mode/ipoe_server.py @@ -81,6 +81,13 @@ username=ifname password=csid {% endif %} +{%- for intfc in interfaces %} +{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %} +vlan_mon={{interfaces[intfc]['vlan_mon']|join(',')}} +interface=re:{{intfc}}\.(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2}) +{% endif %} +{% endfor %} + {% if (dns['server1']) or (dns['server2']) %} [dns] {% if dns['server1'] %} @@ -230,7 +237,8 @@ def get_config(): 'shared' : '1', 'sess_start' : 'dhcpv4', ### may need a conifg option, can be dhcpv4 or up for unclassified pkts 'range' : None, - 'ifcfg' : '1' + 'ifcfg' : '1', + 'vlan_mon' : [] } config_data['dns'] = { 'server1' : None, @@ -258,6 +266,10 @@ def get_config(): 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 + ' vlan-id'): + config_data['interfaces'][intfc]['vlan_mon'] += c.return_values('interface ' + intfc + ' vlan-id') + if c.exists('interface ' + intfc + ' vlan-range'): + config_data['interfaces'][intfc]['vlan_mon'] += c.return_values('interface ' + intfc + ' vlan-range') 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'): @@ -321,7 +333,7 @@ def get_config(): config_data['ipv6']['prfx'] = c.return_values('client-ipv6-pool prefix') if c.exists('client-ipv6-pool delegate-prefix'): config_data['ipv6']['pd'] = c.return_values('client-ipv6-pool delegate-prefix') - + return config_data def generate(c): -- cgit v1.2.3 From 02e5bb55d3922516cfe53016202d58924b72950a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 9 Jul 2019 18:07:50 +0200 Subject: T1497: remove duplicate name servers and search domains obtained from DHCP. --- src/conf_mode/host_name.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 6fb7031a8..eb4b339e9 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -209,6 +209,12 @@ def generate(config): config['nameserver'] += dhcp_ns config['domain_search'] += dhcp_sd + # Prune duplicate values + # Not order preserving, but then when multiple DHCP clients are used, + # there can't be guarantees about the order anyway + dhcp_ns = list(set(dhcp_ns)) + dhcp_sd = list(set(dhcp_sd)) + # We have third party scripts altering /etc/hosts, too. # One example are the DHCP hostname update scripts thus we need to cache in # every modification first - so changing domain-name, domain-search or hostname -- cgit v1.2.3 From 65b2f36a77f311a207b8e5406d222f4dbef177cf Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 9 Jul 2019 09:58:16 -0700 Subject: [wireguard] - T1516: changing committed config causes error --- src/conf_mode/wireguard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py index b6c1e189b..8234fad0b 100755 --- a/src/conf_mode/wireguard.py +++ b/src/conf_mode/wireguard.py @@ -225,7 +225,7 @@ def apply(c): ### config updates if c['interfaces'][intf]['status'] == 'exists': ### IP address change - addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split() + addr_eff = c_eff.return_effective_values(intf + ' address') addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) -- cgit v1.2.3 From 5c4e5e9a6a893aa2fb0df50cf327e942b52995b9 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Fri, 12 Jul 2019 15:31:10 +0200 Subject: Do not try to verify the hostname config if the script is run by cloud-init. --- src/conf_mode/host_name.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index eb4b339e9..1988f7b4f 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -117,6 +117,10 @@ def get_config(arguments): if conf.exists("system host-name"): hosts['hostname'] = conf.return_value("system host-name") + # This may happen if the config is not loaded yet, + # e.g. if run by cloud-init + if not hosts['hostname']: + hosts['hostname'] = default_config_data['hostname'] if conf.exists("system domain-name"): hosts['domain_name'] = conf.return_value("system domain-name") @@ -290,7 +294,12 @@ if __name__ == '__main__': try: c = get_config(args) - verify(c) + # If it's called from dhclient, then either: + # a) verification was already done at commit time + # b) it's run on an unconfigured system, e.g. by cloud-init + # Therefore, verification is either redundant or useless + if not args.dhclient: + verify(c) generate(c) apply(c) except ConfigError as e: -- 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(-) 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 a8a92eb3fcef8ed911ebcaedd32dc5b948c6c79a Mon Sep 17 00:00:00 2001 From: Eshenko Dmitriy Date: Sun, 14 Jul 2019 00:06:00 +0300 Subject: Fix bind param if outside-address not present If in config exist `bind=` without value, accel-ppp listen wrong ip address 255.255.255.255:1723. If need default behavior with listening on 0.0.0.0:1723 we don't set empty bind option. --- src/conf_mode/accel_pptp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/conf_mode/accel_pptp.py b/src/conf_mode/accel_pptp.py index 6c53e8dd4..5f8ccfd84 100755 --- a/src/conf_mode/accel_pptp.py +++ b/src/conf_mode/accel_pptp.py @@ -84,7 +84,9 @@ wins2={{wins[1]}} {% endif %} [pptp] +{% if outside_addr %} bind={{outside_addr}} +{% endif %} verbose=5 ppp-max-mtu={{mtu}} mppe={{authentication['mppe']}} -- cgit v1.2.3 From 5886dd27cbc65f8cda04752bbd39a960b0887523 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 14 Jul 2019 10:59:07 +0200 Subject: [dns-forwarding] T1333: handle domain forward and general recursion in one configuration line In the past we used the PowerDNS cofniguration option forward-zones and forward-zones-recurse, but only the latter one sets the recursion bit in the DNS query. Thus all recursions have been moved to this config statement. --- src/conf_mode/dns_forwarding.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index aab389074..3ca77adee 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -65,21 +65,19 @@ local-address={{ listen_on | join(',') }} # dnssec dnssec={{ dnssec }} -{% if name_servers -%} -# name-server -forward-zones-recurse=.={{ name_servers | join(';') }} -{% else %} -# no name-servers specified - start full recursor -{% endif %} - -# domain ... server ... -{% if domains -%} - -forward-zones-recurse={% for d in domains %} +# forward-zones / recursion +# +# statement is only inserted if either one forwarding domain or nameserver is configured +# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own +# +{% if name_servers or domains %}forward-zones-recurse= +{%- for d in domains %} {{ d.name }}={{ d.servers | join(";") }} -{{- "," if not loop.last -}} -{% endfor %} - +{{- ", " if not loop.last -}} +{%- endfor -%} +{%- if name_servers -%} +{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} +{% endif %} {% endif %} """ @@ -248,7 +246,6 @@ def generate(dns): return None tmpl = jinja2.Template(config_tmpl, trim_blocks=True) - config_text = tmpl.render(dns) with open(config_file, 'w') as f: f.write(config_text) -- cgit v1.2.3 From 5159252e53b620a1a6345f7651f122ef6c82bb07 Mon Sep 17 00:00:00 2001 From: Eshenko Dmitriy Date: Mon, 15 Jul 2019 20:10:49 +0300 Subject: Fix typo pppoe-server to pptp-server --- src/conf_mode/accel_pptp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/accel_pptp.py b/src/conf_mode/accel_pptp.py index 5f8ccfd84..1dd7efb3e 100755 --- a/src/conf_mode/accel_pptp.py +++ b/src/conf_mode/accel_pptp.py @@ -296,7 +296,7 @@ def verify(c): if c['authentication']['mode'] == 'local': if not c['authentication']['local-users']: - raise ConfigError('pppoe-server authentication local-users required') + raise ConfigError('pptp-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') -- cgit v1.2.3 From 334677572aef752b0bf2c893bd14bdf6f801bb4b Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 15 Jul 2019 11:25:12 -0700 Subject: [T1299] - SNMP extension with custom scripts --- interface-definitions/snmp.xml | 22 ++++++++++++++++++++++ src/conf_mode/snmp.py | 27 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index 103aa39d7..8b2fa8a93 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -599,6 +599,28 @@ + + + SNMP script extensions + + + + + Extension name + + + + + Script location and name + + + + + + + + + diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 06d2e253a..0ddab2129 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -206,6 +206,13 @@ group {{ u.group }} usm {{ u.name }} group {{ u.group }} tsm {{ u.name }} {% endfor %} {%- endif %} + +{% if script_ext %} +# extension scripts +{%- for ext in script_ext|sort %} +extend\t{{ext}}\t{{script_ext[ext]}} +{%- endfor %} +{% endif %} """ # SNMP template (/etc/default/snmpd) - be careful if you edit the template. @@ -240,7 +247,8 @@ default_config_data = { 'v3_tsm_key': '', 'v3_tsm_port': '10161', 'v3_users': [], - 'v3_views': [] + 'v3_views': [], + 'script_ext': {} } def rmfile(file): @@ -345,6 +353,14 @@ def get_config(): snmp['trap_targets'].append(trap_tgt) + # + # 'set service snmp script-extensions' + # + if conf.exists('script-extensions'): + for extname in conf.list_nodes('script-extensions extension-name'): + snmp['script_ext'][extname] = '/config/user-data/' + conf.return_value('script-extensions extension-name ' + extname + ' script') + + ######################################################################### # ____ _ _ __ __ ____ _____ # # / ___|| \ | | \/ | _ \ __ _|___ / # @@ -581,6 +597,14 @@ def verify(snmp): if snmp is None: return None + ### check if the configured script actually exist under /config/user-data + if snmp['script_ext']: + for ext in snmp['script_ext']: + if not os.path.isfile(snmp['script_ext'][ext]): + print ("WARNING: script: " + snmp['script_ext'][ext] + " doesn\'t exist") + else: + os.chmod(snmp['script_ext'][ext], 0o555) + # bail out early if SNMP v3 is not configured if not snmp['v3_enabled']: return None @@ -633,7 +657,6 @@ def verify(snmp): if not 'seclevel' in group.keys(): raise ConfigError('"seclevel" must be specified') - if 'v3_traps' in snmp.keys(): for trap in snmp['v3_traps']: if trap['authPassword'] and trap['authMasterKey']: -- cgit v1.2.3 From 016524841da12b68468468cf8f4947b82213ffcd Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 16 Jul 2019 12:04:47 -0700 Subject: [syslog] T1530 - "set system syslog global archive file" doesn't work --- src/conf_mode/syslog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 105f35657..7b79c701b 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -72,8 +72,8 @@ logrotate_configs = ''' missingok notifempty create - rotate {{files[file]['max-files']}} - size={{ files[file]['max-size']//1024}}k + rotate {{files[file]['max-files']}} + size={{files[file]['max-size']//1024}}k postrotate invoke-rc.d rsyslog rotate > /dev/null endscript @@ -120,8 +120,8 @@ def get_config(): config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') if c.exists('global archive size'): config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 - if c.exists('global archive files'): - config_data['files']['global']['max-files'] = c.return_value('global archive files') + if c.exists('global archive file'): + config_data['files']['global']['max-files'] = c.return_value('global archive file') if c.exists('global preserve-fqdn'): config_data['files']['global']['preserver_fqdn'] = True @@ -196,7 +196,7 @@ def get_config(): } } ) - + return config_data def generate_selectors(c, config_node): -- cgit v1.2.3 From f62f31fb14aeaff70edb53d0be2d501916e8e39c Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 16 Jul 2019 23:02:32 +0200 Subject: T1531: do not include the domain name in system hostname. --- src/conf_mode/host_name.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 1988f7b4f..16467c8df 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -258,18 +258,17 @@ def apply(config): if config is None: return None - fqdn = config['hostname'] - if config['domain_name']: - fqdn += '.' + config['domain_name'] + # No domain name -- the Debian way. + hostname_new = config['hostname'] # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. - hn = subprocess.check_output(['hostnamectl', '--static']).decode().strip() + hostname_old = subprocess.check_output(['hostnamectl', '--static']).decode().strip() - os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) + os.system("hostnamectl set-hostname --static {0}".format(hostname_new)) # Restart services that use the hostname - if hn != fqdn: + if hostname_new != hostname_old: os.system("systemctl restart rsyslog.service") # If SNMP is running, restart it too -- cgit v1.2.3 From fdcce3a65e556eed1ced4156d71bbc32b3c87d5a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 18 Jul 2019 23:55:37 +0200 Subject: T1440: in IPv4 DHCP, print the subnet rather than a dict dump when a non-unique subnet is found. --- src/conf_mode/dhcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 6d88ea95a..3e1381cd0 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -753,7 +753,7 @@ def verify(dhcp): # Subnets must be non overlapping if subnet['network'] in subnets: - raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet)) + raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) else: subnets.append(subnet['network']) -- cgit v1.2.3 From 4236848372a4565141167877f2eb32b0eedae577 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Fri, 19 Jul 2019 00:03:07 +0200 Subject: [DHCPv6 server] T1440: add subnet uniqueness check to DHCPv6. --- src/conf_mode/dhcpv6_server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index d2769466e..039321430 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -397,6 +397,12 @@ def verify(dhcpv6): 'in shared network {2} is outside subnet {3}!' \ .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network'])) + # Subnets must be unique + if subnet['network'] in subnets: + raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + else: + subnets.append(subnet['network']) + # DHCPv6 requires at least one configured address range or one static mapping # (FIXME: is not actually checked right now?) -- cgit v1.2.3 From c0b7e14cb2adf5e686a46d5d25c4aed63bac9f53 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Fri, 19 Jul 2019 00:41:54 +0200 Subject: [VRRP] T1362: quote VRRP password strings to avoid config parse errors. --- src/conf_mode/vrrp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 85c6ad580..a08493309 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -85,7 +85,7 @@ vrrp_instance {{ group.name }} { {% if group.auth_password -%} authentication { - auth_pass {{ group.auth_password }} + auth_pass "{{ group.auth_password }}" auth_type {{ group.auth_type }} } {% endif -%} -- 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(-) 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 From d99bf6a3a623433e743bb2d1d72e2ef3e0ab5057 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 21 Jul 2019 18:30:27 +0200 Subject: T1537: add missing help for 'set service dns' --- interface-definitions/dns-dynamic.xml | 230 +++++++++++++++++++++++++++++++ interface-definitions/dns-forwarding.xml | 3 + interface-definitions/dynamic-dns.xml | 227 ------------------------------ 3 files changed, 233 insertions(+), 227 deletions(-) create mode 100644 interface-definitions/dns-dynamic.xml delete mode 100644 interface-definitions/dynamic-dns.xml diff --git a/interface-definitions/dns-dynamic.xml b/interface-definitions/dns-dynamic.xml new file mode 100644 index 000000000..8e7e77475 --- /dev/null +++ b/interface-definitions/dns-dynamic.xml @@ -0,0 +1,230 @@ + + + + + + + + Domain Name System related services + + + + + Dynamic DNS + 919 + + + + + Interface to send DDNS updates for [REQUIRED] + + + + + + + + RFC2136 Update name + + + + + File containing the secret key shared with remote DNS server [REQUIRED] + + file + File in /config/auth directory + + + + + + Record to be updated [REQUIRED] + + + + + + Server to be updated [REQUIRED] + + + + + Time To Live (default: 600) + + 1-86400 + DNS forwarding cache size + + + + + + + + + Zone to be updated [REQUIRED] + + + + + + + Service being used for Dynamic DNS [REQUIRED] + + custom afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit + + + custom + Custom or predefined service + + + afraid + + + + changeip + + + + cloudflare + + + + dnspark + + + + dslreports + + + + dyndns + + + + easydns + + + + namecheap + + + + noip + + + + sitelutions + + + + zoneedit + + + + + + + Hostname registered with DDNS service [REQUIRED] + + + + + + Login for DDNS service [REQUIRED] + + + + + Password for DDNS service [REQUIRED] + + + + + ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM] + + protocol + ddclient protocol + + + changeip + + + + cloudflare + + + + dnspark + + + + dslreports1 + + + + dyndns2 + + + + easydns + + + + namecheap + + + + noip + + + + sitelutions + + + + zoneedit1 + + + + + + + Server to send DDNS update to [REQUIRED FOR CUSTOM] + + IPv4 + IP address of DDNS server + + + FQDN + Hostname of DDNS server + + + + + + + + Web check used for obtaining the external IP address + + + + + Skip everything before this on the given URL + + + + + URL to obtain the current external IP address + + + + + + + + + + + + + diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index b989434f2..56820608c 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -4,6 +4,9 @@ + + Domain Name System related services + diff --git a/interface-definitions/dynamic-dns.xml b/interface-definitions/dynamic-dns.xml deleted file mode 100644 index 974bbcbce..000000000 --- a/interface-definitions/dynamic-dns.xml +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - Dynamic DNS - 919 - - - - - Interface to send DDNS updates for [REQUIRED] - - - - - - - - RFC2136 Update name - - - - - File containing the secret key shared with remote DNS server [REQUIRED] - - file - File in /config/auth directory - - - - - - Record to be updated [REQUIRED] - - - - - - Server to be updated [REQUIRED] - - - - - Time To Live (default: 600) - - 1-86400 - DNS forwarding cache size - - - - - - - - - Zone to be updated [REQUIRED] - - - - - - - Service being used for Dynamic DNS [REQUIRED] - - custom afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit - - - custom - Custom or predefined service - - - afraid - - - - changeip - - - - cloudflare - - - - dnspark - - - - dslreports - - - - dyndns - - - - easydns - - - - namecheap - - - - noip - - - - sitelutions - - - - zoneedit - - - - - - - Hostname registered with DDNS service [REQUIRED] - - - - - - Login for DDNS service [REQUIRED] - - - - - Password for DDNS service [REQUIRED] - - - - - ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM] - - protocol - ddclient protocol - - - changeip - - - - cloudflare - - - - dnspark - - - - dslreports1 - - - - dyndns2 - - - - easydns - - - - namecheap - - - - noip - - - - sitelutions - - - - zoneedit1 - - - - - - - Server to send DDNS update to [REQUIRED FOR CUSTOM] - - IPv4 - IP address of DDNS server - - - FQDN - Hostname of DDNS server - - - - - - - - Web check used for obtaining the external IP address - - - - - Skip everything before this on the given URL - - - - - URL to obtain the current external IP address - - - - - - - - - - - - - -- cgit v1.2.3