summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Jenkinsfile178
-rw-r--r--Makefile5
-rw-r--r--data/templates/accel-ppp/pppoe.config.tmpl2
-rw-r--r--data/templates/conserver/conserver.conf.tmpl37
-rw-r--r--data/templates/firewall/nftables-nat.tmpl27
-rw-r--r--data/templates/ntp/override.conf.tmpl8
-rw-r--r--data/templates/router-advert/radvd.conf.tmpl2
-rw-r--r--data/templates/snmp/override.conf.tmpl (renamed from src/etc/systemd/system/snmpd.service.d/override.conf)5
-rw-r--r--data/templates/system-login/pam_radius_auth.conf.tmpl15
-rw-r--r--data/templates/wwan/ip-down.script.tmpl23
-rw-r--r--data/templates/wwan/ip-up.script.tmpl22
-rw-r--r--debian/control3
-rw-r--r--interface-definitions/interfaces-wireless.xml.in3
-rw-r--r--interface-definitions/ntp.xml.in1
-rw-r--r--interface-definitions/service_console-server.xml.in90
-rw-r--r--interface-definitions/snmp.xml.in1
-rw-r--r--interface-definitions/system-login.xml.in12
-rw-r--r--op-mode-definitions/show-console-server.xml49
-rw-r--r--op-mode-definitions/show-ip-ports.xml17
-rw-r--r--op-mode-definitions/show-route-map.xml22
-rw-r--r--op-mode-definitions/show-rpki.xml32
-rw-r--r--op-mode-definitions/show-system.xml12
-rw-r--r--op-mode-definitions/show-table.xml13
-rw-r--r--op-mode-definitions/show-version.xml6
-rw-r--r--op-mode-definitions/show-vrf.xml8
-rw-r--r--python/setup.py2
-rw-r--r--python/vyos/config.py57
-rw-r--r--python/vyos/configdict.py13
-rw-r--r--python/vyos/ifconfig/__init__.py1
-rw-r--r--python/vyos/ifconfig/section.py33
-rw-r--r--python/vyos/util.py25
-rw-r--r--schema/interface_definition.rnc10
-rw-r--r--schema/interface_definition.rng20
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py9
-rwxr-xr-xsrc/conf_mode/ntp.py58
-rwxr-xr-xsrc/conf_mode/service_console-server.py109
-rwxr-xr-xsrc/conf_mode/service_router-advert.py19
-rwxr-xr-xsrc/conf_mode/snmp.py31
-rwxr-xr-xsrc/conf_mode/ssh.py10
-rwxr-xr-xsrc/conf_mode/system-login.py7
-rwxr-xr-xsrc/conf_mode/system-syslog.py51
-rwxr-xr-xsrc/conf_mode/system_console.py13
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py2
-rw-r--r--src/etc/systemd/system/conserver-server.service.d/override.conf10
-rw-r--r--src/etc/systemd/system/radvd.service.d/override.conf17
-rwxr-xr-xsrc/migration-scripts/interfaces/5-to-618
-rwxr-xr-xsrc/migration-scripts/system/16-to-1745
-rwxr-xr-xsrc/op_mode/show_interfaces.py13
-rw-r--r--src/systemd/dhcp6c@.service3
-rw-r--r--src/systemd/dropbear@.service14
-rw-r--r--src/systemd/dropbearkey.service11
-rw-r--r--src/tests/test_util.py34
-rwxr-xr-xsrc/validators/wireless-phy25
53 files changed, 911 insertions, 342 deletions
diff --git a/Jenkinsfile b/Jenkinsfile
index 7a79b0f43..7a760b40b 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,4 +1,4 @@
-// Copyright (C) 2019 VyOS maintainers and contributors
+// Copyright (C) 2020 VyOS maintainers and contributors
//
// This program is free software; you can redistribute it and/or modify
// in order to easy exprort images built to "external" world
@@ -15,174 +15,10 @@
@NonCPS
-def getGitBranchName() {
- def branch = scm.branches[0].name
- return branch.split('/')[-1]
-}
-
-def getGitRepoURL() {
- return scm.userRemoteConfigs[0].url
-}
-
-def getGitRepoName() {
- return getGitRepoURL().split('/').last()
-}
-
-// Returns true if this is a custom build launched on any project fork.
-// Returns false if this is build from git@github.com:vyos/<reponame>.
-// <reponame> can be e.g. vyos-1x.git or vyatta-op.git
-def isCustomBuild() {
- // GitHub organisation base URL
- def gitURI = 'git@github.com:vyos/' + getGitRepoName()
- def httpURI = 'https://github.com/vyos/' + getGitRepoName()
-
- return !((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI)) || env.CHANGE_ID
-}
-
-def setDescription() {
- def item = Jenkins.instance.getItemByFullName(env.JOB_NAME)
-
- // build up the main description text
- def description = ""
- description += "<h2>VyOS individual package build: " + getGitRepoName().replace('.git', '') + "</h2>"
-
- if (isCustomBuild()) {
- description += "<p style='border: 3px dashed red; width: 50%;'>"
- description += "<b>Build not started from official Git repository!</b><br>"
- description += "<br>"
- description += "Repository: <font face = 'courier'>" + getGitRepoURL() + "</font><br>"
- description += "Branch: <font face = 'courier'>" + getGitBranchName() + "</font><br>"
- description += "</p>"
- } else {
- description += "Sources taken from Git branch: <font face = 'courier'>" + getGitBranchName() + "</font><br>"
- }
-
- item.setDescription(description)
- item.save()
-}
-
-/* Only keep the 10 most recent builds. */
-def projectProperties = [
- [$class: 'BuildDiscarderProperty',strategy: [$class: 'LogRotator', numToKeepStr: '10']],
-]
-
-properties(projectProperties)
-setDescription()
-
-node('Docker') {
- stage('Define Agent') {
- script {
- // create container name on demand
- def branchName = getGitBranchName()
- // Adjust PR target branch name so we can re-map it to the proper
- // Docker image. CHANGE_ID is set only for pull requests, so it is
- // safe to access the pullRequest global variable
- if (env.CHANGE_ID) {
- branchName = "${env.CHANGE_TARGET}".toLowerCase()
- }
- if (branchName.equals("master")) {
- branchName = "current"
- }
- env.DOCKER_IMAGE = "vyos/vyos-build:" + branchName
- }
- }
-}
-
-pipeline {
- agent {
- docker {
- args "--sysctl net.ipv6.conf.lo.disable_ipv6=0 -e GOSU_UID=1006 -e GOSU_GID=1006"
- image "${env.DOCKER_IMAGE}"
- alwaysPull true
- }
- }
- options {
- disableConcurrentBuilds()
- timeout(time: 30, unit: 'MINUTES')
- timestamps()
- }
- stages {
- stage('Fetch') {
- steps {
- script {
- dir('build') {
- checkout scm
- }
- }
- }
- }
- stage('Build') {
- steps {
- script {
- dir('build') {
- def commitId = sh(returnStdout: true, script: 'git rev-parse --short=11 HEAD').trim()
- currentBuild.description = sprintf('Git SHA1: %s', commitId[-11..-1])
-
- sh 'dpkg-buildpackage -b -us -uc -tc'
- }
- }
- }
- }
- }
- post {
- cleanup {
- deleteDir()
- }
- success {
- script {
- // archive *.deb artifact on custom builds, deploy to repo otherwise
- if ( isCustomBuild()) {
- archiveArtifacts artifacts: '*.deb', fingerprint: true
- } else {
- // publish build result, using SSH-dev.packages.vyos.net Jenkins Credentials
- sshagent(['SSH-dev.packages.vyos.net']) {
- // build up some fancy groovy variables so we do not need to write/copy
- // every option over and over again!
- def RELEASE = getGitBranchName()
- if (getGitBranchName() == "master") {
- RELEASE = 'current'
- }
-
- def VYOS_REPO_PATH = '/home/sentrium/web/dev.packages.vyos.net/public_html/repositories/' + RELEASE + '/'
- if (getGitBranchName() == "crux")
- VYOS_REPO_PATH += 'vyos/'
-
- def SSH_OPTS = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR'
- def SSH_REMOTE = 'khagen@10.217.48.113'
-
- echo "Uploading package(s) and updating package(s) in the repository ..."
-
- files = findFiles(glob: '*.deb')
- files.each { PACKAGE ->
- def ARCH = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Architecture").trim()
- def SUBSTRING = sh(returnStdout: true, script: "dpkg-deb -f ${PACKAGE} Package").trim()
- def SSH_DIR = '~/VyOS/' + RELEASE + '/' + ARCH
- def ARCH_OPT = ''
- if (ARCH != 'all')
- ARCH_OPT = '-A ' + ARCH
-
- // No need to explicitly check the return code. The pipeline
- // will fail if sh returns a non 0 exit code
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "bash --login -c 'mkdir -p ${SSH_DIR}'"
- """
- sh """
- scp ${SSH_OPTS} ${PACKAGE} ${SSH_REMOTE}:${SSH_DIR}/
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} ${ARCH_OPT} remove ${RELEASE} ${SUBSTRING}'"
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} deleteunreferenced'"
- """
- sh """
- ssh ${SSH_OPTS} ${SSH_REMOTE} -t "uncron-add 'reprepro -v -b ${VYOS_REPO_PATH} ${ARCH_OPT} includedeb ${RELEASE} ${SSH_DIR}/${PACKAGE}'"
- """
- }
- }
- }
- }
- }
- }
-}
+// Using a version specifier library, use 'current' branch. The underscore (_)
+// is not a typo! You need this underscore if the line immediately after the
+// @Library annotation is not an import statement!
+@Library('vyos-build@current')_
+// Start package build using library function from https://github.com/c-po/vyos-build
+buildPackage()
diff --git a/Makefile b/Makefile
index 77ac03a82..593a8a5f7 100644
--- a/Makefile
+++ b/Makefile
@@ -135,3 +135,8 @@ docs:
deb:
dpkg-buildpackage -uc -us -tc -b
+
+.PHONY: schema
+schema:
+ trang -I rnc -O rng schema/interface_definition.rnc schema/interface_definition.rng
+ trang -I rnc -O rng schema/op-mode-definition.rnc schema/op-mode-definition.rng
diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl
index 6c4ff89b1..370ca7946 100644
--- a/data/templates/accel-ppp/pppoe.config.tmpl
+++ b/data/templates/accel-ppp/pppoe.config.tmpl
@@ -129,6 +129,8 @@ verbose=1
check-ip=1
{% if ppp_ccp %}
ccp=1
+{% else %}
+ccp=0
{% endif %}
{% if ppp_min_mtu %}
min-mtu={{ ppp_min_mtu }}
diff --git a/data/templates/conserver/conserver.conf.tmpl b/data/templates/conserver/conserver.conf.tmpl
new file mode 100644
index 000000000..4e7b5d8d7
--- /dev/null
+++ b/data/templates/conserver/conserver.conf.tmpl
@@ -0,0 +1,37 @@
+### Autogenerated by service_console-server.py ###
+
+# See https://www.conserver.com/docs/conserver.cf.man.html for additional options
+
+config * {
+ primaryport 3109;
+ daemonmode false;
+}
+
+default * {
+ motd "VyOS Console Server";
+ rw *;
+}
+
+##
+## list of consoles we serve
+##
+{% for key, value in device.items() %}
+{# Depending on our USB serial console we could require a path adjustment #}
+{% set path = '/dev' if key.startswith('ttyS') else '/dev/serial/by-bus' %}
+console {{ key }} {
+ master localhost;
+ type device;
+ device {{ path }}/{{ key }};
+ baud {{ value.speed }};
+ parity {{ value.parity }};
+ options {{ "!" if value.stop_bits == "1" }}cstopb;
+}
+{% endfor %}
+
+##
+## list of clients we allow
+##
+access * {
+ trusted localhost;
+ allowed localhost;
+}
diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl
index 0a3dfa369..8108d5e0f 100644
--- a/data/templates/firewall/nftables-nat.tmpl
+++ b/data/templates/firewall/nftables-nat.tmpl
@@ -29,9 +29,22 @@ add rule ip raw NAT_CONNTRACK counter accept
{% macro nat_rule(rule, chain) %}
{% set src_addr = "ip saddr " + rule.source_address if rule.source_address %}
-{% set src_port = "sport " + rule.source_port if rule.source_port %}
{% set dst_addr = "ip daddr " + rule.dest_address if rule.dest_address %}
-{% set dst_port = "dport " + rule.dest_port if rule.dest_port %}
+
+{# negated port groups need special treatment, move != in front of { } group #}
+{% if rule.source_port.startswith('!=') %}
+{% set src_port = "sport != { " + rule.source_port.replace('!=','') +" }" if rule.source_port %}
+{% else %}
+{% set src_port = "sport { " + rule.source_port +" }" if rule.source_port %}
+{% endif %}
+
+{# negated port groups need special treatment, move != in front of { } group #}
+{% if rule.dest_port.startswith('!=') %}
+{% set dst_port = "dport != { " + rule.dest_port.replace('!=','') +" }" if rule.dest_port %}
+{% else %}
+{% set dst_port = "dport { " + rule.dest_port +" }" if rule.dest_port %}
+{% endif %}
+
{% set comment = "DST-NAT-" + rule.number %}
{% if chain == "PREROUTING" %}
@@ -39,9 +52,13 @@ add rule ip raw NAT_CONNTRACK counter accept
{% set trns_addr = "dnat to " + rule.translation_address %}
{% elif chain == "POSTROUTING" %}
{% set interface = " oifname \"" + rule.interface_out + "\"" %}
-{% set trns_addr = rule.translation_address %}
-{% if rule.translation_address != 'masquerade' %}
-{% set trns_addr = "snat to " + trns_addr %}
+{% if rule.translation_address == 'masquerade' %}
+{% set trns_addr = rule.translation_address %}
+{% if rule.translation_port %}
+{% set trns_addr = trns_addr + " to " %}
+{% endif %}
+{% else %}
+{% set trns_addr = "snat to " + rule.translation_address %}
{% endif %}
{% endif %}
{% set trns_port = ":" + rule.translation_port if rule.translation_port %}
diff --git a/data/templates/ntp/override.conf.tmpl b/data/templates/ntp/override.conf.tmpl
new file mode 100644
index 000000000..69a73b128
--- /dev/null
+++ b/data/templates/ntp/override.conf.tmpl
@@ -0,0 +1,8 @@
+[Service]
+ExecStart=
+{% if vrf %}
+ExecStart=/sbin/ip vrf exec {{ vrf }} /usr/lib/ntp/ntp-systemd-wrapper
+{% else %}
+ExecStart=/usr/lib/ntp/ntp-systemd-wrapper
+{% endif %}
+
diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl
index 2768f6f2e..073623eac 100644
--- a/data/templates/router-advert/radvd.conf.tmpl
+++ b/data/templates/router-advert/radvd.conf.tmpl
@@ -1,4 +1,4 @@
-### Autogenerated by service-router-advert.py ###
+### Autogenerated by service_router-advert.py ###
{% for i in interfaces -%}
interface {{ i.name }} {
diff --git a/src/etc/systemd/system/snmpd.service.d/override.conf b/data/templates/snmp/override.conf.tmpl
index c366f9073..1eb8f20a9 100644
--- a/src/etc/systemd/system/snmpd.service.d/override.conf
+++ b/data/templates/snmp/override.conf.tmpl
@@ -2,5 +2,8 @@
Environment=
Environment="MIBSDIR=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/mibs/site:/usr/share/snmp/mibs:/usr/share/mibs/iana:/usr/share/mibs/ietf:/usr/share/mibs/netsnmp"
ExecStart=
+{% if vrf %}
+ExecStart=/sbin/ip vrf exec {{ vrf }} /usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid
+{% else %}
ExecStart=/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid
-
+{% endif %}
diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl
index ad196fa3d..ec2d6df95 100644
--- a/data/templates/system-login/pam_radius_auth.conf.tmpl
+++ b/data/templates/system-login/pam_radius_auth.conf.tmpl
@@ -1,12 +1,11 @@
-# Automatically generated by VyOS
+# Automatically generated by system-login.py
# RADIUS configuration file
-{%- if radius_server %}
-# server[:port] shared_secret timeout (s) source_ip
-{% for s in radius_server %}
-{%- if not s.disabled -%}
-{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %}
-{% endif %}
-{%- endfor %}
+{% if radius_server %}
+# server[:port] shared_secret timeout source_ip
+{% for s in radius_server|sort(attribute='priority') if not s.disabled %}
+{% set addr_port = s.address + ":" + s.port %}
+{{ "%-22s" | format(addr_port) }} {{ "%-25s" | format(s.key) }} {{ "%-10s" | format(s.timeout) }} {{ radius_source_address if radius_source_address }}
+{% endfor %}
priv-lvl 15
mapped_priv_user radius_priv_user
diff --git a/data/templates/wwan/ip-down.script.tmpl b/data/templates/wwan/ip-down.script.tmpl
index 194f8d863..f7b38cbc5 100644
--- a/data/templates/wwan/ip-down.script.tmpl
+++ b/data/templates/wwan/ip-down.script.tmpl
@@ -1,26 +1,27 @@
#!/bin/sh
-tty=$2
+# Script parameters will be like:
+# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0
# Only applicable for Wireless Modems (WWAN)
-if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then
+if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then
exit 0
fi
-# Determine if we are enslaved to a VRF, this is needed to properly insert
-# the default route
-VRF_NAME=""
+# Determine if we are running inside a VRF or not, required for proper routing table
+# NOTE: the down script can not be properly templated as we need the VRF name,
+# which is not present on deletion, thus we read it from the operating system.
if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
# Determine upper (VRF) interface
VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
# Remove upper_ prefix from result string
- VRF=${VRF#"upper_"}
- # Populate variable to run in VR context
- VRF_NAME=" -c vrf ${VRF_NAME} "
+ VRF_NAME=${VRF#"upper_"}
+ # Remove default route from VRF routing table
+ vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "no ip route 0.0.0.0/0 {{ intf }}"
+else
+ # Remove default route from GRT (global routing table)
+ vtysh -c "conf t" -c "no ip route 0.0.0.0/0 {{ intf }}"
fi
-# Remove default route to either default or VRF routing table
-vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} {{ metric }}"
-
DIALER_PID=$(cat /var/run/{{ intf }}.pid)
logger -t pppd[$DIALER_PID] "removed default route via {{ intf }} metric {{ metric }}"
diff --git a/data/templates/wwan/ip-up.script.tmpl b/data/templates/wwan/ip-up.script.tmpl
index 89e42a23a..3a7eec800 100644
--- a/data/templates/wwan/ip-up.script.tmpl
+++ b/data/templates/wwan/ip-up.script.tmpl
@@ -1,25 +1,25 @@
#!/bin/sh
-tty=$2
+# Script parameters will be like:
+# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0
# Only applicable for Wireless Modems (WWAN)
-if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then
+if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then
exit 0
fi
-DIALER_PID=$(cat /var/run/{{ intf }}.pid)
-
-# Determine if we are enslaved to a VRF, this is needed to properly insert
-# the default route
-VRF_NAME=""
+# Determine if we are running inside a VRF or not, required for proper routing table
if [ -d /sys/class/net/{{ intf }}/upper_* ]; then
# Determine upper (VRF) interface
VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*))
# Remove upper_ prefix from result string
- VRF=${VRF#"upper_"}
- VRF_NAME="vrf ${VRF}"
+ VRF_NAME=${VRF#"upper_"}
+ # Remove default route from VRF routing table
+ vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "ip route 0.0.0.0/0 {{ intf }} {{ metric }}"
+else
+ # Remove default route from GRT (global routing table)
+ vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ intf }} {{ metric }}"
fi
-# Apply default route to either default or VRF routing table
-vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME} {{ metric }}"
+DIALER_PID=$(cat /var/run/{{ intf }}.pid)
logger -t pppd[$DIALER_PID] "added default route via {{ intf }} metric {{ metric }} ${VRF_NAME}"
diff --git a/debian/control b/debian/control
index 24390ae50..aaaf33e2a 100644
--- a/debian/control
+++ b/debian/control
@@ -60,6 +60,9 @@ Depends: python3,
iputils-arping,
libvyosconfig0,
beep,
+ dropbear,
+ conserver-server,
+ conserver-client,
isc-dhcp-server,
isc-dhcp-relay,
keepalived (>=2.0.5),
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index d21074b6a..06c7734f5 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -561,6 +561,9 @@
<completionHelp>
<script>${vyos_completion_dir}/list_wireless_phys.sh</script>
</completionHelp>
+ <constraint>
+ <validator name="wireless-phy"/>
+ </constraint>
</properties>
</leafNode>
<leafNode name="reduce-transmit-power">
diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in
index 945345898..485487a42 100644
--- a/interface-definitions/ntp.xml.in
+++ b/interface-definitions/ntp.xml.in
@@ -76,6 +76,7 @@
</constraint>
</properties>
</leafNode>
+ #include <include/interface-vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service_console-server.xml.in
new file mode 100644
index 000000000..348d591dd
--- /dev/null
+++ b/interface-definitions/service_console-server.xml.in
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="console-server" owner="${vyos_conf_scripts_dir}/service_console-server.py">
+ <properties>
+ <help>Serial Console Server</help>
+ <priority>990</priority>
+ </properties>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>System serial interface name (ttyS or ttyUSB)</help>
+ <completionHelp>
+ <script>ls -1 /dev | grep ttyS</script>
+ <script>ls -1 /dev/serial/by-bus</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ttySxxx</format>
+ <description>Regular serial interface</description>
+ </valueHelp>
+ <valueHelp>
+ <format>usbxbxpx</format>
+ <description>USB based serial interface</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(ttyS\d+|usb\d+b.*p.*)$</regex>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/interface-description.xml.i>
+ <leafNode name="speed">
+ <properties>
+ <help>Serial port baud rate</help>
+ <completionHelp>
+ <list>300 1200 2400 4800 9600 19200 38400 57600 115200</list>
+ </completionHelp>
+ <constraint>
+ <regex>(300|1200|2400|4800|9600|19200|38400|57600|115200)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="data-bits">
+ <properties>
+ <help>Serial port data bits (default: 8)</help>
+ <completionHelp>
+ <list>7 8</list>
+ </completionHelp>
+ <constraint>
+ <regex>(7|8)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop-bits">
+ <properties>
+ <help>Serial port stop bits (default: 1)</help>
+ <completionHelp>
+ <list>1 2</list>
+ </completionHelp>
+ <constraint>
+ <regex>(1|2)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="parity">
+ <properties>
+ <help>Parity setting (default: none)</help>
+ <completionHelp>
+ <list>even odd none</list>
+ </completionHelp>
+ <constraint>
+ <regex>(even|odd|none)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="ssh">
+ <properties>
+ <help>SSH remote access to this console</help>
+ </properties>
+ <children>
+ #include <include/port-number.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in
index 4c6a993b2..31428092f 100644
--- a/interface-definitions/snmp.xml.in
+++ b/interface-definitions/snmp.xml.in
@@ -623,6 +623,7 @@
</tagNode>
</children>
</node>
+ #include <include/interface-vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 053b6babd..812a50c8a 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -128,6 +128,18 @@
<constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="priority">
+ <properties>
+ <help>Server priority</help>
+ <valueHelp>
+ <format>1-255</format>
+ <description>Server priority (default: 255)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
</children>
</tagNode>
#include <include/interface-vrf.xml.i>
diff --git a/op-mode-definitions/show-console-server.xml b/op-mode-definitions/show-console-server.xml
new file mode 100644
index 000000000..e47b6cfaa
--- /dev/null
+++ b/op-mode-definitions/show-console-server.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="connect">
+ <children>
+ <tagNode name="console">
+ <properties>
+ <help>Connect to device attached to serial console server</help>
+ <completionHelp>
+ <path>service console-server device</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/console "$3"</command>
+ </tagNode>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="log">
+ <children>
+ <leafNode name="console-server">
+ <properties>
+ <help>Show log for serial console server</help>
+ </properties>
+ <command>/usr/bin/journalctl -u conserver-server.service</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="console-server">
+ <properties>
+ <help>Show Console-Server information</help>
+ </properties>
+ <children>
+ <leafNode name="ports">
+ <properties>
+ <help>Examine console ports and configured baud rates</help>
+ </properties>
+ <command>/usr/bin/console -x</command>
+ </leafNode>
+ <leafNode name="user">
+ <properties>
+ <help>Show users on various consoles</help>
+ </properties>
+ <command>/usr/bin/console -u</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-ip-ports.xml b/op-mode-definitions/show-ip-ports.xml
new file mode 100644
index 000000000..a74b68ffc
--- /dev/null
+++ b/op-mode-definitions/show-ip-ports.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ip">
+ <children>
+ <leafNode name="ports">
+ <properties>
+ <help>Show IP ports in use by various system services</help>
+ </properties>
+ <command>sudo /usr/bin/netstat -tulnp</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-route-map.xml b/op-mode-definitions/show-route-map.xml
new file mode 100644
index 000000000..0e376757b
--- /dev/null
+++ b/op-mode-definitions/show-route-map.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="route-map">
+ <properties>
+ <help>Show route-map information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show route-map"</command>
+ </node>
+ <tagNode name="route-map">
+ <properties>
+ <help>Show specified route-map information</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/vtysh -c "show route-map $3"</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-rpki.xml b/op-mode-definitions/show-rpki.xml
new file mode 100644
index 000000000..d68c3b862
--- /dev/null
+++ b/op-mode-definitions/show-rpki.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="rpki">
+ <properties>
+ <help>Show RPKI information</help>
+ </properties>
+ <children>
+ <leafNode name="cache-connection">
+ <properties>
+ <help>Show RPKI cache connections</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki cache-connection"</command>
+ </leafNode>
+ <leafNode name="cache-server">
+ <properties>
+ <help>Show RPKI cache servers information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki cache-server"</command>
+ </leafNode>
+ <leafNode name="prefix-table">
+ <properties>
+ <help>Show RPKI-validated prefixes</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show rpki prefix-table"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-system.xml b/op-mode-definitions/show-system.xml
index a39d9b481..74b34ae92 100644
--- a/op-mode-definitions/show-system.xml
+++ b/op-mode-definitions/show-system.xml
@@ -124,6 +124,12 @@
</properties>
<command>cat /proc/meminfo</command>
</leafNode>
+ <leafNode name="routing-daemons">
+ <properties>
+ <help>Show memory usage of all routing protocols</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show memory"</command>
+ </leafNode>
</children>
</node>
<node name="processes">
@@ -152,6 +158,12 @@
</leafNode>
</children>
</node>
+ <leafNode name="routing-daemons">
+ <properties>
+ <help>Show Quagga routing daemons</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show daemons"</command>
+ </leafNode>
<leafNode name="storage">
<properties>
<help>Show filesystem usage</help>
diff --git a/op-mode-definitions/show-table.xml b/op-mode-definitions/show-table.xml
new file mode 100644
index 000000000..b093a5de7
--- /dev/null
+++ b/op-mode-definitions/show-table.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <leafNode name="table">
+ <properties>
+ <help>Show routing tables</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show zebra router table summary"</command>
+ </leafNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-version.xml b/op-mode-definitions/show-version.xml
index 8bf23de82..aae5bb008 100644
--- a/op-mode-definitions/show-version.xml
+++ b/op-mode-definitions/show-version.xml
@@ -20,6 +20,12 @@
</properties>
<command>${vyos_op_scripts_dir}/show_version.py --all</command>
</leafNode>
+ <leafNode name="quagga">
+ <properties>
+ <help>Show Quagga version information</help>
+ </properties>
+ <command>/usr/bin/vtysh -c "show version"</command>
+ </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml
index 360153d8e..1c806908b 100644
--- a/op-mode-definitions/show-vrf.xml
+++ b/op-mode-definitions/show-vrf.xml
@@ -16,6 +16,14 @@
</completionHelp>
</properties>
<command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command>
+ <children>
+ <leafNode name="processes">
+ <properties>
+ <help>Shows all process ids associated with VRF</help>
+ </properties>
+ <command>/usr/sbin/ip vrf pids "$3"</command>
+ </leafNode>
+ </children>
</tagNode>
</children>
</node>
diff --git a/python/setup.py b/python/setup.py
index ac7d0b573..9440e7fe7 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
setup(
name = "vyos",
- version = "1.2.0",
+ version = "1.3.0",
author = "VyOS maintainers and contributors",
author_email = "maintainers@vyos.net",
description = ("VyOS configuration libraries."),
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 54cb518c3..56353c322 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -68,6 +68,7 @@ import re
import json
import subprocess
+import vyos.util
import vyos.configtree
@@ -100,23 +101,33 @@ class Config(object):
# once the config system is initialized during boot;
# before initialization, set to empty string
if os.path.isfile('/tmp/vyos-config-status'):
- running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ try:
+ running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ except VyOSError:
+ running_config_text = ''
else:
running_config_text = ''
# Session config ("active") only exists in conf mode.
# In op mode, we'll just use the same running config for both active and session configs.
if self.in_session():
- session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ try:
+ session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
+ except VyOSError:
+ session_config_text = ''
else:
session_config_text = running_config_text
- self._session_config = vyos.configtree.ConfigTree(session_config_text)
if running_config_text:
self._running_config = vyos.configtree.ConfigTree(running_config_text)
else:
self._running_config = None
+ if session_config_text:
+ self._session_config = vyos.configtree.ConfigTree(session_config_text)
+ else:
+ self._session_config = None
+
def _make_command(self, op, path):
args = path.split()
cmd = [self._cli_shell_api, op] + args
@@ -193,6 +204,8 @@ class Config(object):
This function cannot be used outside a configuration sessions.
In operational mode scripts, use ``exists_effective``.
"""
+ if not self._session_config:
+ return False
if self._session_config.exists(self._make_path(path)):
return True
else:
@@ -275,7 +288,7 @@ class Config(object):
self.__session_env = save_env
return(default)
- def get_config_dict(self, path=[], effective=False):
+ def get_config_dict(self, path=[], effective=False, key_mangling=None):
"""
Args: path (str list): Configuration tree path, can be empty
Returns: a dict representation of the config
@@ -287,6 +300,15 @@ class Config(object):
else:
config_dict = {}
+ if key_mangling:
+ if not (isinstance(key_mangling, tuple) and \
+ (len(key_mangling) == 2) and \
+ isinstance(key_mangling[0], str) and \
+ isinstance(key_mangling[1], str)):
+ raise ValueError("key_mangling must be a tuple of two strings")
+ else:
+ config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1])
+
return config_dict
def is_multi(self, path):
@@ -362,9 +384,12 @@ class Config(object):
This function cannot be used outside a configuration session.
In operational mode scripts, use ``return_effective_value``.
"""
- try:
- value = self._session_config.return_value(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._session_config:
+ try:
+ value = self._session_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
value = None
if not value:
@@ -387,9 +412,12 @@ class Config(object):
This function cannot be used outside a configuration session.
In operational mode scripts, use ``return_effective_values``.
"""
- try:
- values = self._session_config.return_values(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._session_config:
+ try:
+ values = self._session_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
values = []
if not values:
@@ -408,9 +436,12 @@ class Config(object):
string list: child node names
"""
- try:
- nodes = self._session_config.list_nodes(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._session_config:
+ try:
+ nodes = self._session_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
nodes = []
if not nodes:
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 4708d3b50..973fbdd8b 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -87,6 +87,19 @@ def retrieve_config(path_hash, base_path, config):
return config_hash
+def dict_merge(source, destination):
+ """ Merge two dictionaries. Only keys which are not present in destination
+ will be copied from source, anything else will be kept untouched. Function
+ will return a new dict which has the merged key/value pairs. """
+ from copy import deepcopy
+ tmp = deepcopy(destination)
+
+ for key, value in source.items():
+ if key not in tmp.keys():
+ tmp[key] = value
+
+ return tmp
+
def list_diff(first, second):
"""
Diff two dictionaries and return only unique items
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index 1757adf26..a7cdeadd1 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -31,6 +31,7 @@ from vyos.ifconfig.macvlan import MACVLANIf
from vyos.ifconfig.vxlan import VXLANIf
from vyos.ifconfig.wireguard import WireGuardIf
from vyos.ifconfig.vtun import VTunIf
+from vyos.ifconfig.vti import VTIIf
from vyos.ifconfig.pppoe import PPPoEIf
from vyos.ifconfig.tunnel import GREIf
from vyos.ifconfig.tunnel import GRETapIf
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 926c22e8a..173a90bb4 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+import re
import netifaces
@@ -103,12 +104,42 @@ class Section:
yield ifname
@classmethod
+ def _sort_interfaces(cls, generator):
+ """
+ return a list of the sorted interface by number, vlan, qinq
+ """
+ def key(ifname):
+ value = 0
+ parts = re.split(r'([^0-9]+)([0-9]+)[.]?([0-9]+)?[.]?([0-9]+)?', ifname)
+ length = len(parts)
+ name = parts[1] if length >= 3 else parts[0]
+ # the +1 makes sure eth0.0.0 after eth0.0
+ number = int(parts[2]) + 1 if length >= 4 and parts[2] is not None else 0
+ vlan = int(parts[3]) + 1 if length >= 5 and parts[3] is not None else 0
+ qinq = int(parts[4]) + 1 if length >= 6 and parts[4] is not None else 0
+
+ # so that "lo" (or short names) are handled (as "loa")
+ for n in (name + 'aaa')[:3]:
+ value *= 100
+ value += (ord(n) - ord('a'))
+ value += number
+ # vlan are 16 bits, so this can not overflow
+ value = (value << 16) + vlan
+ value = (value << 16) + qinq
+ return value
+
+ l = list(generator)
+ l.sort(key=key)
+ return l
+
+ @classmethod
def interfaces(cls, section=''):
"""
return a list of the name of the configured interface which are under a section
if no section is provided, then it returns all configured interfaces
"""
- return list(cls._intf_under_section(section))
+
+ return cls._sort_interfaces(cls._intf_under_section(section))
@classmethod
def _intf_with_feature(cls, feature=''):
diff --git a/python/vyos/util.py b/python/vyos/util.py
index c93f8d3f0..0ddc14963 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import re
import sys
#
@@ -340,6 +341,28 @@ def colon_separated_to_dict(data_string, uniquekeys=False):
return data
+def mangle_dict_keys(data, regex, replacement):
+ """ Mangles dict keys according to a regex and replacement character.
+ Some libraries like Jinja2 do not like certain characters in dict keys.
+ This function can be used for replacing all offending characters
+ with something acceptable.
+
+ Args:
+ data (dict): Original dict to mangle
+
+ Returns: dict
+ """
+ new_dict = {}
+ for key in data.keys():
+ new_key = re.sub(regex, replacement, key)
+
+ value = data[key]
+ if isinstance(value, dict):
+ new_dict[new_key] = mangle_dict_keys(value, regex, replacement)
+ else:
+ new_dict[new_key] = value
+
+ return new_dict
def process_running(pid_file):
""" Checks if a process with PID in pid_file is running """
diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc
index 0ce8226cd..6647f5e11 100644
--- a/schema/interface_definition.rnc
+++ b/schema/interface_definition.rnc
@@ -57,14 +57,18 @@ tagNode = element tagNode
# but can have values.
# Leaf node may contain one or more valueConstraint tags
# If multiple valueConstraint tags are used, they work a logical OR
-# Leaf nodes can have "multi" attribute that indicated that it can have
-# more than one value
+# Leaf nodes can have "multi" attribute that indicated that it can have more than one value
+# It can also have a default value
leafNode = element leafNode
{
(ownerAttr? & nodeNameAttr),
- properties
+ (defaultValue? & properties)
}
+# Default value for leaf node, if applicable
+# It is used to generate default node state representation
+defaultValue = element defaultValue { text }
+
# Normal and tag nodes may have children
children = element children
{
diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng
index bfd8d376f..22e886006 100644
--- a/schema/interface_definition.rng
+++ b/schema/interface_definition.rng
@@ -95,8 +95,8 @@
but can have values.
Leaf node may contain one or more valueConstraint tags
If multiple valueConstraint tags are used, they work a logical OR
- Leaf nodes can have "multi" attribute that indicated that it can have
- more than one value
+ Leaf nodes can have "multi" attribute that indicated that it can have more than one value
+ It can also have a default value
-->
<define name="leafNode">
<element name="leafNode">
@@ -106,7 +106,21 @@
</optional>
<ref name="nodeNameAttr"/>
</interleave>
- <ref name="properties"/>
+ <interleave>
+ <optional>
+ <ref name="defaultValue"/>
+ </optional>
+ <ref name="properties"/>
+ </interleave>
+ </element>
+ </define>
+ <!--
+ Default value for leaf node, if applicable
+ It is used to generate default node state representation
+ -->
+ <define name="defaultValue">
+ <element name="defaultValue">
+ <text/>
</element>
</define>
<!-- Normal and tag nodes may have children -->
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index b142688f6..70710e97c 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -46,17 +46,18 @@ default_config_data = {
}
def get_config():
+ peth = deepcopy(default_config_data)
+ conf = Config()
+
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- ifname = os.environ['VYOS_TAGNODE_VALUE']
- conf = Config()
+ peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
- cfg_base = ['interfaces', 'pseudo-ethernet', ifname]
+ cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
if not conf.exists(cfg_base):
- peth = deepcopy(default_config_data)
peth['deleted'] = True
return peth
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index a66fddc61..9180998aa 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -18,6 +18,7 @@ import os
from copy import deepcopy
from ipaddress import ip_network
+from netifaces import interfaces
from sys import exit
from vyos.config import Config
@@ -29,23 +30,27 @@ from vyos import airbag
airbag.enable()
config_file = r'/etc/ntp.conf'
+systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf'
default_config_data = {
'servers': [],
'allowed_networks': [],
- 'listen_address': []
+ 'listen_address': [],
+ 'vrf': ''
}
def get_config():
ntp = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('system ntp'):
+ base = ['system', 'ntp']
+ if not conf.exists(base):
return None
else:
- conf.set_level('system ntp')
+ conf.set_level(base)
- if conf.exists('allow-clients address'):
- networks = conf.return_values('allow-clients address')
+ node = ['allow-clients', 'address']
+ if conf.exists(node):
+ networks = conf.return_values(node)
for n in networks:
addr = ip_network(n)
net = {
@@ -56,11 +61,13 @@ def get_config():
ntp['allowed_networks'].append(net)
- if conf.exists('listen-address'):
- ntp['listen_address'] = conf.return_values('listen-address')
+ node = ['listen-address']
+ if conf.exists(node):
+ ntp['listen_address'] = conf.return_values(node)
- if conf.exists('server'):
- for node in conf.list_nodes('server'):
+ node = ['server']
+ if conf.exists(node):
+ for node in conf.list_nodes(node):
options = []
server = {
"name": node,
@@ -76,41 +83,50 @@ def get_config():
server['options'] = options
ntp['servers'].append(server)
+ node = ['vrf']
+ if conf.exists(node):
+ ntp['vrf'] = conf.return_value(node)
+
return ntp
def verify(ntp):
# bail out early - looks like removal from running config
- if ntp is None:
+ if not ntp:
return None
# Configuring allowed clients without a server makes no sense
if len(ntp['allowed_networks']) and not len(ntp['servers']):
raise ConfigError('NTP server not configured')
- for n in ntp['allowed_networks']:
- try:
- addr = ip_network( n['network'] )
- break
- except ValueError:
- raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network']))
+ if ntp['vrf'] and ntp['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**ntp))
return None
def generate(ntp):
# bail out early - looks like removal from running config
- if ntp is None:
+ if not ntp:
return None
render(config_file, 'ntp/ntp.conf.tmpl', ntp)
+ render(systemd_override, 'ntp/override.conf.tmpl', ntp, trim_blocks=True)
+
return None
def apply(ntp):
- if ntp is not None:
- call('systemctl restart ntp.service')
- else:
+ if not ntp:
# NTP support is removed in the commit
call('systemctl stop ntp.service')
- os.unlink(config_file)
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ if os.path.isfile(systemd_override):
+ os.unlink(systemd_override)
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if ntp:
+ call('systemctl restart ntp.service')
return None
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
new file mode 100755
index 000000000..7f6967983
--- /dev/null
+++ b/src/conf_mode/service_console-server.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+
+config_file = r'/run/conserver/conserver.cf'
+
+# Default values are necessary until the implementation of T2588 is completed
+default_values = {
+ 'data_bits': '8',
+ 'parity': 'none',
+ 'stop_bits': '1'
+}
+
+def get_config():
+ conf = Config()
+ base = ['service', 'console-server']
+
+ if not conf.exists(base):
+ return None
+
+ # Retrieve CLI representation as dictionary
+ proxy = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # The retrieved dictionary will look something like this:
+ #
+ # {'device': {'usb0b2.4p1.0': {'speed': '9600'},
+ # 'usb0b2.4p1.1': {'data_bits': '8',
+ # 'parity': 'none',
+ # 'speed': '115200',
+ # 'stop_bits': '2'}}}
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ for device in proxy['device'].keys():
+ tmp = dict_merge(default_values, proxy['device'][device])
+ proxy['device'][device] = tmp
+
+ return proxy
+
+def verify(proxy):
+ if not proxy:
+ return None
+
+ for device in proxy['device']:
+ keys = proxy['device'][device].keys()
+ if 'speed' not in keys:
+ raise ConfigError(f'Serial port speed must be defined for "{tmp}"!')
+
+ if 'ssh' in keys:
+ ssh_keys = proxy['device'][device]['ssh'].keys()
+ if 'port' not in ssh_keys:
+ raise ConfigError(f'SSH port must be defined for "{tmp}"!')
+
+ return None
+
+def generate(proxy):
+ if not proxy:
+ return None
+
+ render(config_file, 'conserver/conserver.conf.tmpl', proxy)
+ return None
+
+def apply(proxy):
+ call('systemctl stop dropbear@*.service conserver-server.service')
+
+ if not proxy:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ return None
+
+ call('systemctl restart conserver-server.service')
+
+ for device in proxy['device']:
+ if 'ssh' in proxy['device'][device].keys():
+ port = proxy['device'][device]['ssh']['port']
+ call(f'systemctl restart dropbear@{device}.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index da7019e2c..ef6148ebd 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -27,7 +27,7 @@ from vyos.template import render
from vyos import airbag
airbag.enable()
-config_file = r'/etc/radvd.conf'
+config_file = r'/run/radvd/radvd.conf'
default_config_data = {
'interfaces': []
@@ -66,8 +66,8 @@ def get_config():
if conf.exists(['hop-limit']):
intf['hop_limit'] = conf.return_value(['hop-limit'])
- if conf.exists(['default-lifetim']):
- intf['default_lifetime'] = conf.return_value(['default-lifetim'])
+ if conf.exists(['default-lifetime']):
+ intf['default_lifetime'] = conf.return_value(['default-lifetime'])
if conf.exists(['default-preference']):
intf['default_preference'] = conf.return_value(['default-preference'])
@@ -107,8 +107,8 @@ def get_config():
'prefix' : prefix,
'autonomous_flag' : 'on',
'on_link' : 'on',
- 'preferred_lifetime': '14400',
- 'valid_lifetime' : '2592000'
+ 'preferred_lifetime': 14400,
+ 'valid_lifetime' : 2592000
}
@@ -122,10 +122,10 @@ def get_config():
tmp['on_link'] = 'off'
if conf.exists(['preferred-lifetime']):
- tmp['preferred_lifetime'] = conf.return_value(['preferred-lifetime'])
+ tmp['preferred_lifetime'] = int(conf.return_value(['preferred-lifetime']))
if conf.exists(['valid-lifetime']):
- tmp['valid_lifetime'] = conf.return_value(['valid-lifetime'])
+ tmp['valid_lifetime'] = int(conf.return_value(['valid-lifetime']))
intf['prefixes'].append(tmp)
@@ -134,6 +134,11 @@ def get_config():
return rtradv
def verify(rtradv):
+ for interface in rtradv['interfaces']:
+ for prefix in interface['prefixes']:
+ if not (prefix['valid_lifetime'] > prefix['preferred_lifetime']):
+ raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime')
+
return None
def generate(rtradv):
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 2b6140f41..eb0d20654 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -17,8 +17,8 @@
import os
from binascii import hexlify
+from netifaces import interfaces
from time import sleep
-from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP
from sys import exit
from vyos.config import Config
@@ -36,6 +36,7 @@ config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
default_script_dir = r'/config/user-data/'
+systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf'
# SNMP OIDs used to mark auth/priv type
OIDs = {
@@ -66,7 +67,8 @@ default_config_data = {
'v3_traps': [],
'v3_users': [],
'v3_views': [],
- 'script_ext': []
+ 'script_ext': [],
+ 'vrf': ''
}
def rmfile(file):
@@ -174,9 +176,6 @@ 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'):
conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
@@ -191,6 +190,10 @@ def get_config():
snmp['script_ext'].append(extension)
+ if conf.exists('vrf'):
+ snmp['vrf'] = conf.return_value('vrf')
+
+
#########################################################################
# ____ _ _ __ __ ____ _____ #
# / ___|| \ | | \/ | _ \ __ _|___ / #
@@ -393,7 +396,7 @@ def verify(snmp):
if not os.path.isfile(ext['script']):
print ("WARNING: script: {} doesn't exist".format(ext['script']))
else:
- os.chmod(ext['script'], S_IRWXU | S_IXGRP | S_IXOTH | S_IROTH | S_IRGRP)
+ chmod_755(ext['script'])
for listen in snmp['listen_address']:
addr = listen[0]
@@ -413,6 +416,9 @@ def verify(snmp):
else:
print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+ if snmp['vrf'] and snmp['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**snmp))
+
# bail out early if SNMP v3 is not configured
if not snmp['v3_enabled']:
return None
@@ -512,11 +518,14 @@ def generate(snmp):
# This is even save if service is going to be removed
call('systemctl stop snmpd.service')
config_files = [config_file_client, config_file_daemon, config_file_access,
- config_file_user]
+ config_file_user, systemd_override]
for file in config_files:
rmfile(file)
- if snmp is None:
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if not snmp:
return None
# Write client config file
@@ -527,13 +536,17 @@ def generate(snmp):
render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
# Write access rights config file
render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
+ # Write daemon configuration file
+ render(systemd_override, 'snmp/override.conf.tmpl', snmp)
return None
def apply(snmp):
- if snmp is None:
+ if not snmp:
return None
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
# start SNMP daemon
call("systemctl restart snmpd.service")
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 5a0ae059b..43fa2ff39 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -31,7 +31,7 @@ config_file = r'/etc/ssh/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
default_config_data = {
- 'port' : '22',
+ 'port' : ['22'],
'log_level': 'INFO',
'password_authentication': 'yes',
'host_validation': 'yes',
@@ -137,9 +137,11 @@ def apply(ssh):
os.unlink(config_file)
if os.path.isfile(systemd_override):
os.unlink(systemd_override)
- else:
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if ssh:
call('systemctl restart ssh.service')
return None
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 5990c3777..93d4cc679 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -144,7 +144,8 @@ def get_config():
'disabled': False,
'key': '',
'port': '1812',
- 'timeout': '2'
+ 'timeout': '2',
+ 'priority': 255
}
conf.set_level(base_level + ['radius', 'server', server])
@@ -164,6 +165,10 @@ def get_config():
if conf.exists(['timeout']):
server_cfg['timeout'] = conf.return_value(['timeout'])
+ # Check if RADIUS server has priority
+ if conf.exists(['priority']):
+ server_cfg['priority'] = int(conf.return_value(['priority']))
+
# Append individual RADIUS server configuration to global server list
login['radius_server'].append(server_cfg)
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 8b20e1135..cfc1ca55f 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -162,32 +162,31 @@ def generate_selectors(c, config_node):
# protocols and security are being mapped here
# for backward compatibility with old configs
# security and protocol mappings can be removed later
- if c.is_tag(config_node):
- nodes = c.list_nodes(config_node)
- selectors = ""
- for node in nodes:
- lvl = c.return_value(config_node + ' ' + node + ' level')
- if lvl == None:
- lvl = "err"
- if lvl == 'all':
- lvl = '*'
- if node == 'all' and node != nodes[-1]:
- selectors += "*." + lvl + ";"
- elif node == 'all':
- selectors += "*." + lvl
- elif node != nodes[-1]:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl + ";"
- else:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl
- return selectors
+ nodes = c.list_nodes(config_node)
+ selectors = ""
+ for node in nodes:
+ lvl = c.return_value(config_node + ' ' + node + ' level')
+ if lvl == None:
+ lvl = "err"
+ if lvl == 'all':
+ lvl = '*'
+ if node == 'all' and node != nodes[-1]:
+ selectors += "*." + lvl + ";"
+ elif node == 'all':
+ selectors += "*." + lvl
+ elif node != nodes[-1]:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl + ";"
+ else:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl
+ return selectors
def generate(c):
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 0831232fb..034cbee63 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -74,12 +74,15 @@ def generate(console):
call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
+ if not console:
+ return None
+
for device in console['device'].keys():
config_file = base_dir + f'/serial-getty@{device}.service'
- render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
+ render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ os.symlink(config_file, getty_wants_symlink)
# GRUB
# For existing serial line change speed (if necessary)
@@ -107,6 +110,10 @@ def generate(console):
def apply(console):
# reset screen blanking
call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
if not console:
return None
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index febe8c3b7..7e40be32a 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -278,7 +278,7 @@ def get_config():
l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret'])
if conf.exists(['ccp-disable']):
- l2tp[['ccp_disable']] = True
+ l2tp['ccp_disable'] = True
# PPP options
if conf.exists(['idle']):
diff --git a/src/etc/systemd/system/conserver-server.service.d/override.conf b/src/etc/systemd/system/conserver-server.service.d/override.conf
new file mode 100644
index 000000000..3c753f572
--- /dev/null
+++ b/src/etc/systemd/system/conserver-server.service.d/override.conf
@@ -0,0 +1,10 @@
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=/run/conserver/conserver.cf
+
+[Service]
+Type=simple
+ExecStart=
+ExecStart=/usr/sbin/conserver -M localhost -C /run/conserver/conserver.cf
+Restart=on-failure
diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf
new file mode 100644
index 000000000..44c4345e1
--- /dev/null
+++ b/src/etc/systemd/system/radvd.service.d/override.conf
@@ -0,0 +1,17 @@
+[Unit]
+ConditionPathExists=/run/radvd/radvd.conf
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/radvd
+ExecStartPre=
+ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
+ExecStart=
+ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid
+ExecReload=
+ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf
+ExecReload=/bin/kill -HUP $MAINPID
+PIDFile=
+PIDFile=/run/radvd/radvd.pid
diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6
index 85a1994c6..1291751d8 100755
--- a/src/migration-scripts/interfaces/5-to-6
+++ b/src/migration-scripts/interfaces/5-to-6
@@ -59,13 +59,17 @@ def copy_rtradv(c, old_base, interface):
prefix_base = new_base + ['prefix']
if c.exists(prefix_base):
for prefix in config.list_nodes(prefix_base):
- bool_cleanup = ['autonomous-flag', 'on-link-flag']
- for bool in bool_cleanup:
- if c.exists(prefix_base + [prefix, bool]):
- tmp = c.return_value(prefix_base + [prefix, bool])
- c.delete(prefix_base + [prefix, bool])
- if tmp == 'true':
- c.set(prefix_base + [prefix, bool])
+ if c.exists(prefix_base + [prefix, 'autonomous-flag']):
+ tmp = c.return_value(prefix_base + [prefix, 'autonomous-flag'])
+ c.delete(prefix_base + [prefix, 'autonomous-flag'])
+ if tmp == 'false':
+ c.set(prefix_base + [prefix, 'no-autonomous-flag'])
+
+ if c.exists(prefix_base + [prefix, 'on-link-flag']):
+ tmp = c.return_value(prefix_base + [prefix, 'on-link-flag'])
+ c.delete(prefix_base + [prefix, 'on-link-flag'])
+ if tmp == 'true':
+ c.set(prefix_base + [prefix, 'on-link-flag'])
# router advertisement can be individually disabled per interface
# the node has been renamed from send-advert {true | false} to no-send-advert
diff --git a/src/migration-scripts/system/16-to-17 b/src/migration-scripts/system/16-to-17
index 981149d1b..8f762c0e2 100755
--- a/src/migration-scripts/system/16-to-17
+++ b/src/migration-scripts/system/16-to-17
@@ -41,31 +41,32 @@ else:
if config.exists(base + ['netconsole']):
config.delete(base + ['netconsole'])
- for device in config.list_nodes(base + ['device']):
- dev_path = base + ['device', device]
- # remove "system console device <device> modem" (T2570)
- if config.exists(dev_path + ['modem']):
- config.delete(dev_path + ['modem'])
+ if config.exists(base + ['device']):
+ for device in config.list_nodes(base + ['device']):
+ dev_path = base + ['device', device]
+ # remove "system console device <device> modem" (T2570)
+ if config.exists(dev_path + ['modem']):
+ config.delete(dev_path + ['modem'])
- # Only continue on USB based serial consoles
- if not 'ttyUSB' in device:
- continue
+ # Only continue on USB based serial consoles
+ if not 'ttyUSB' in device:
+ continue
- # A serial console has been configured but it does no longer
- # exist on the system - cleanup
- if not os.path.exists(f'/dev/{device}'):
- config.delete(dev_path)
- continue
+ # A serial console has been configured but it does no longer
+ # exist on the system - cleanup
+ if not os.path.exists(f'/dev/{device}'):
+ config.delete(dev_path)
+ continue
- # migrate from ttyUSB device to new device in /dev/serial/by-bus
- for root, dirs, files in os.walk('/dev/serial/by-bus'):
- for usb_device in files:
- device_file = os.path.realpath(os.path.join(root, usb_device))
- # migrate to new USB device names (T2529)
- if os.path.basename(device_file) == device:
- config.copy(dev_path, base + ['device', usb_device])
- # Delete old USB node from config
- config.delete(dev_path)
+ # migrate from ttyUSB device to new device in /dev/serial/by-bus
+ for root, dirs, files in os.walk('/dev/serial/by-bus'):
+ for usb_device in files:
+ device_file = os.path.realpath(os.path.join(root, usb_device))
+ # migrate to new USB device names (T2529)
+ if os.path.basename(device_file) == device:
+ config.copy(dev_path, base + ['device', usb_device])
+ # Delete old USB node from config
+ config.delete(dev_path)
try:
with open(file_name, 'w') as f:
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index 2f0f8a1c9..46571c0c0 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -55,7 +55,6 @@ def filtered_interfaces(ifnames, iftypes, vif, vrrp):
return an instance of the interface class
"""
allnames = Section.interfaces()
- allnames.sort()
vrrp_interfaces = VRRP.active_interfaces() if vrrp else []
@@ -97,10 +96,14 @@ def split_text(text, used=0):
line = ''
for word in text.split():
- if len(line) + len(word) >= desc_len:
- yield f'{line} {word}'[1:]
- line = ''
- line = f'{line} {word}'
+ if len(line) + len(word) < desc_len:
+ line = f'{line} {word}'
+ continue
+ if line:
+ yield line[1:]
+ else:
+ line = f'{line} {word}'
+
yield line[1:]
diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service
index e110eccc1..9a97ee261 100644
--- a/src/systemd/dhcp6c@.service
+++ b/src/systemd/dhcp6c@.service
@@ -3,15 +3,14 @@ Description=WIDE DHCPv6 client on %i
Documentation=man:dhcp6c(8) man:dhcp6c.conf(5)
ConditionPathExists=/run/dhcp6c/dhcp6c.%i.conf
After=vyos-router.service
+StartLimitIntervalSec=0
[Service]
WorkingDirectory=/run/dhcp6c
Type=forking
PIDFile=/run/dhcp6c/dhcp6c.%i.pid
ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
-
Restart=on-failure
-StartLimitIntervalSec=0
RestartSec=20
[Install]
diff --git a/src/systemd/dropbear@.service b/src/systemd/dropbear@.service
new file mode 100644
index 000000000..606a7ea6d
--- /dev/null
+++ b/src/systemd/dropbear@.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Dropbear SSH per-connection server
+Requires=dropbearkey.service
+Wants=conserver-server.service
+ConditionPathExists=/run/conserver/conserver.cf
+After=dropbearkey.service vyos-router.service conserver-server.service
+
+[Service]
+Type=forking
+ExecStartPre=/usr/bin/bash -c '/usr/bin/systemctl set-environment PORT=$(cli-shell-api returnActiveValue service console-server device "%I" ssh port)'
+ExecStart=-/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console %I" -P /run/conserver/dropbear.%I.pid -p ${PORT}
+PIDFile=/run/conserver/dropbear.%I.pid
+KillMode=process
+Restart=on-failure
diff --git a/src/systemd/dropbearkey.service b/src/systemd/dropbearkey.service
new file mode 100644
index 000000000..770641c8b
--- /dev/null
+++ b/src/systemd/dropbearkey.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Dropbear SSH Key Generation
+ConditionPathExists=|!/etc/dropbear/dropbear_rsa_host_key
+
+[Service]
+ExecStart=/usr/bin/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
new file mode 100644
index 000000000..0e56a67a8
--- /dev/null
+++ b/src/tests/test_util.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import unittest
+from unittest import TestCase
+
+import vyos.util
+
+
+class TestVyOSUtil(TestCase):
+ def setUp(self):
+ pass
+
+ def test_key_mangline(self):
+ data = {"foo-bar": {"baz-quux": None}}
+ expected_data = {"foo_bar": {"baz_quux": None}}
+ new_data = vyos.util.mangle_dict_keys(data, '-', '_')
+ self.assertEqual(new_data, expected_data)
+
diff --git a/src/validators/wireless-phy b/src/validators/wireless-phy
new file mode 100755
index 000000000..513a902de
--- /dev/null
+++ b/src/validators/wireless-phy
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+if [ ! -d /sys/class/ieee80211 ]; then
+ echo No IEEE 802.11 physical interfaces detected
+ exit 1
+fi
+
+if [ ! -e /sys/class/ieee80211/$1 ]; then
+ echo Device interface "$1" does not exist
+ exit 1
+fi