diff options
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() @@ -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 | 
