summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/frr/staticd.frr.tmpl4
-rw-r--r--data/templates/ipsec/charon.tmpl11
-rw-r--r--data/templates/ipsec/swanctl/peer.tmpl7
-rw-r--r--interface-definitions/include/interface/dhcp-options.xml.i3
-rw-r--r--interface-definitions/include/interface/tunnel-remote-multi.xml.i19
-rw-r--r--interface-definitions/include/interface/tunnel-remote.xml.i2
-rw-r--r--interface-definitions/interfaces-macsec.xml.in4
-rw-r--r--interface-definitions/interfaces-vxlan.xml.in2
-rw-r--r--interface-definitions/vpn_ipsec.xml.in37
-rw-r--r--op-mode-definitions/reboot.xml.in4
-rw-r--r--python/vyos/configdict.py92
-rwxr-xr-xpython/vyos/ifconfig/interface.py26
-rw-r--r--python/vyos/ifconfig/vxlan.py19
-rw-r--r--python/vyos/util.py8
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py32
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py21
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_macsec.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_pseudo_ethernet.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py36
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py73
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py1
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py32
-rwxr-xr-xsrc/op_mode/cpu_summary.py36
-rwxr-xr-xsrc/op_mode/powerctrl.py25
-rwxr-xr-xsrc/op_mode/show_cpu.py63
-rwxr-xr-xsrc/op_mode/show_ram.py19
-rwxr-xr-xsrc/op_mode/show_uptime.py27
-rwxr-xr-xsrc/op_mode/show_version.py22
30 files changed, 497 insertions, 140 deletions
diff --git a/data/templates/frr/staticd.frr.tmpl b/data/templates/frr/staticd.frr.tmpl
index bfe959c1d..5d833228a 100644
--- a/data/templates/frr/staticd.frr.tmpl
+++ b/data/templates/frr/staticd.frr.tmpl
@@ -17,10 +17,10 @@ vrf {{ vrf }}
{% endif %}
{# IPv4 default routes from DHCP interfaces #}
{% if dhcp is defined and dhcp is not none %}
-{% for interface in dhcp %}
+{% for interface, interface_config in dhcp.items() %}
{% set next_hop = interface | get_dhcp_router %}
{% if next_hop is defined and next_hop is not none %}
-{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 210
+{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.distance }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/ipsec/charon.tmpl b/data/templates/ipsec/charon.tmpl
index 4d710921e..b9b020dcd 100644
--- a/data/templates/ipsec/charon.tmpl
+++ b/data/templates/ipsec/charon.tmpl
@@ -20,6 +20,17 @@ charon {
# Send Cisco Unity vendor ID payload (IKEv1 only).
# cisco_unity = no
+ # Cisco FlexVPN
+{% if options is defined %}
+ cisco_flexvpn = {{ 'yes' if options.flexvpn is defined else 'no' }}
+{% if options.virtual_ip is defined %}
+ install_virtual_ip = yes
+{% endif %}
+{% if options.interface is defined and options.interface is not none %}
+ install_virtual_ip_on = {{ options.interface }}
+{% endif %}
+{% endif %}
+
# Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed.
# close_ike_on_child_failure = no
diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index 481ea7224..562e8fdd5 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -5,6 +5,9 @@
peer_{{ name }} {
proposals = {{ ike | get_esp_ike_cipher | join(',') }}
version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }}
+{% if peer_conf.virtual_address is defined and peer_conf.virtual_address is not none %}
+ vips = {{ peer_conf.virtual_address | join(', ') }}
+{% endif %}
local_addrs = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '0.0.0.0/0' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }}
remote_addrs = {{ peer if peer not in ['any', '0.0.0.0'] and peer[0:1] != '@' else '0.0.0.0/0' }}
{% if peer_conf.authentication is defined and peer_conf.authentication.mode is defined and peer_conf.authentication.mode == 'x509' %}
@@ -80,6 +83,8 @@
start_action = start
{% elif peer_conf.connection_type == 'respond' %}
start_action = trap
+{% elif peer_conf.connection_type == 'none' %}
+ start_action = none
{% endif %}
{% if ike.dead_peer_detection is defined %}
{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
@@ -128,6 +133,8 @@
start_action = start
{% elif peer_conf.connection_type == 'respond' %}
start_action = trap
+{% elif peer_conf.connection_type == 'none' %}
+ start_action = none
{% endif %}
{% if ike.dead_peer_detection is defined %}
{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
diff --git a/interface-definitions/include/interface/dhcp-options.xml.i b/interface-definitions/include/interface/dhcp-options.xml.i
index b65b0802a..f62b06640 100644
--- a/interface-definitions/include/interface/dhcp-options.xml.i
+++ b/interface-definitions/include/interface/dhcp-options.xml.i
@@ -30,12 +30,13 @@
<help>Distance for the default route from DHCP server</help>
<valueHelp>
<format>u32:1-255</format>
- <description>Distance for the default route from DHCP server (default 210)</description>
+ <description>Distance for the default route from DHCP server (default: 210)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-255"/>
</constraint>
</properties>
+ <defaultValue>210</defaultValue>
</leafNode>
<leafNode name="reject">
<properties>
diff --git a/interface-definitions/include/interface/tunnel-remote-multi.xml.i b/interface-definitions/include/interface/tunnel-remote-multi.xml.i
new file mode 100644
index 000000000..f672087a4
--- /dev/null
+++ b/interface-definitions/include/interface/tunnel-remote-multi.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from interface/tunnel-remote-multi.xml.i -->
+<leafNode name="remote">
+ <properties>
+ <help>Tunnel remote address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Tunnel remote IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Tunnel remote IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/tunnel-remote.xml.i b/interface-definitions/include/interface/tunnel-remote.xml.i
index 1ba9b0382..2a8891b85 100644
--- a/interface-definitions/include/interface/tunnel-remote.xml.i
+++ b/interface-definitions/include/interface/tunnel-remote.xml.i
@@ -1,4 +1,4 @@
-<!-- include start from rip/tunnel-remote.xml.i -->
+<!-- include start from interface/tunnel-remote.xml.i -->
<leafNode name="remote">
<properties>
<help>Tunnel remote address</help>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index d69a093af..598935e51 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -16,7 +16,9 @@
</valueHelp>
</properties>
<children>
- #include <include/interface/address-ipv4-ipv6.xml.i>
+ #include <include/interface/address-ipv4-ipv6-dhcp.xml.i>
+ #include <include/interface/dhcp-options.xml.i>
+ #include <include/interface/dhcpv6-options.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
#include <include/interface/interface-firewall.xml.i>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 4c3c3ac71..0546b4199 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -98,7 +98,7 @@
</leafNode>
#include <include/source-address-ipv4-ipv6.xml.i>
#include <include/source-interface.xml.i>
- #include <include/interface/tunnel-remote.xml.i>
+ #include <include/interface/tunnel-remote-multi.xml.i>
#include <include/interface/vrf.xml.i>
#include <include/vni.xml.i>
</children>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index af92eec31..dae76218f 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -335,7 +335,7 @@
</completionHelp>
<valueHelp>
<format>ikev1</format>
- <description>Use IKEv1 for key exchange [DEFAULT]</description>
+ <description>Use IKEv1 for key exchange</description>
</valueHelp>
<valueHelp>
<format>ikev2</format>
@@ -646,6 +646,19 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="flexvpn">
+ <properties>
+ <help>Allow FlexVPN vendor ID payload (IKEv2 only)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/generic-interface.xml.i>
+ <leafNode name="virtual-ip">
+ <properties>
+ <help>Allow install virtual-ip addresses</help>
+ <valueless/>
+ </properties>
+ </leafNode>
</children>
</node>
<tagNode name="profile">
@@ -989,7 +1002,7 @@
<properties>
<help>Connection type</help>
<completionHelp>
- <list>initiate respond</list>
+ <list>initiate respond none</list>
</completionHelp>
<valueHelp>
<format>initiate</format>
@@ -999,8 +1012,12 @@
<format>respond</format>
<description>Bring the connection up only if traffic is detected</description>
</valueHelp>
+ <valueHelp>
+ <format>none</format>
+ <description>Load the connection only</description>
+ </valueHelp>
<constraint>
- <regex>^(initiate|respond)$</regex>
+ <regex>^(initiate|respond|none)$</regex>
</constraint>
</properties>
</leafNode>
@@ -1111,6 +1128,20 @@
</node>
</children>
</tagNode>
+ <leafNode name="virtual-address">
+ <properties>
+ <help>Initiator request virtual-address from peer</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Request IPv4 address from peer</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>Request IPv6 address from peer</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
<node name="vti">
<properties>
<help>Virtual tunnel interface [REQUIRED]</help>
diff --git a/op-mode-definitions/reboot.xml.in b/op-mode-definitions/reboot.xml.in
index 2c8daec5d..6414742d9 100644
--- a/op-mode-definitions/reboot.xml.in
+++ b/op-mode-definitions/reboot.xml.in
@@ -25,7 +25,7 @@
<list>&lt;Minutes&gt;</list>
</completionHelp>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command>
+ <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot_in $3 $4</command>
</tagNode>
<tagNode name="at">
<properties>
@@ -40,7 +40,7 @@
<properties>
<help>Reboot at a specific date</help>
<completionHelp>
- <list>&lt;DDMMYYYY&gt; &lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
+ <list>&lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index e7f515ea9..f2ec93520 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -319,34 +319,42 @@ def is_source_interface(conf, interface, intftype=None):
def get_dhcp_interfaces(conf, vrf=None):
""" Common helper functions to retrieve all interfaces from current CLI
sessions that have DHCP configured. """
- dhcp_interfaces = []
+ dhcp_interfaces = {}
dict = conf.get_config_dict(['interfaces'], get_first_key=True)
if not dict:
return dhcp_interfaces
def check_dhcp(config, ifname):
- out = []
+ tmp = {}
if 'address' in config and 'dhcp' in config['address']:
+ options = {}
+ if 'dhcp_options' in config and 'default_route_distance' in config['dhcp_options']:
+ options.update({'distance' : config['dhcp_options']['default_route_distance']})
if 'vrf' in config:
- if vrf is config['vrf']: out.append(ifname)
- else: out.append(ifname)
- return out
+ if vrf is config['vrf']: tmp.update({ifname : options})
+ else: tmp.update({ifname : options})
+ return tmp
for section, interface in dict.items():
- for ifname, ifconfig in interface.items():
+ for ifname in interface:
+ # we already have a dict representation of the config from get_config_dict(),
+ # but with the extended information from get_interface_dict() we also
+ # get the DHCP client default-route-distance default option if not specified.
+ ifconfig = get_interface_dict(conf, ['interfaces', section], ifname)
+
tmp = check_dhcp(ifconfig, ifname)
- dhcp_interfaces.extend(tmp)
+ dhcp_interfaces.update(tmp)
# check per VLAN interfaces
for vif, vif_config in ifconfig.get('vif', {}).items():
tmp = check_dhcp(vif_config, f'{ifname}.{vif}')
- dhcp_interfaces.extend(tmp)
+ dhcp_interfaces.update(tmp)
# check QinQ VLAN interfaces
for vif_s, vif_s_config in ifconfig.get('vif-s', {}).items():
tmp = check_dhcp(vif_s_config, f'{ifname}.{vif_s}')
- dhcp_interfaces.extend(tmp)
+ dhcp_interfaces.update(tmp)
for vif_c, vif_c_config in vif_s_config.get('vif-c', {}).items():
tmp = check_dhcp(vif_c_config, f'{ifname}.{vif_s}.{vif_c}')
- dhcp_interfaces.extend(tmp)
+ dhcp_interfaces.update(tmp)
return dhcp_interfaces
@@ -405,6 +413,12 @@ def get_interface_dict(config, base, ifname=''):
if 'deleted' not in dict:
dict = dict_merge(default_values, dict)
+ # If interface does not request an IPv4 DHCP address there is no need
+ # to keep the dhcp-options key
+ if 'address' not in dict or 'dhcp' not in dict['address']:
+ if 'dhcp_options' in dict:
+ del dict['dhcp_options']
+
# XXX: T2665: blend in proper DHCPv6-PD default values
dict = T2665_set_dhcpv6pd_defaults(dict)
@@ -423,6 +437,15 @@ def get_interface_dict(config, base, ifname=''):
bond = is_member(config, ifname, 'bonding')
if bond: dict.update({'is_bond_member' : bond})
+ # Check if any DHCP options changed which require a client restat
+ for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+ 'no-default-route', 'vendor-class-id']:
+ dhcp = leaf_node_changed(config, ['dhcp-options', leaf_node])
+ if dhcp:
+ dict.update({'dhcp_options_old' : dhcp})
+ # one option is suffiecient to set 'dhcp_options_old' key
+ break
+
# Some interfaces come with a source_interface which must also not be part
# of any other bond or bridge interface as it is exclusivly assigned as the
# Kernels "lower" interface to this new "virtual/upper" interface.
@@ -466,10 +489,25 @@ def get_interface_dict(config, base, ifname=''):
# XXX: T2665: blend in proper DHCPv6-PD default values
dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif])
+ # If interface does not request an IPv4 DHCP address there is no need
+ # to keep the dhcp-options key
+ if 'address' not in dict['vif'][vif] or 'dhcp' not in dict['vif'][vif]['address']:
+ if 'dhcp_options' in dict['vif'][vif]:
+ del dict['vif'][vif]['dhcp_options']
+
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif}', 'bridge')
if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge})
+ # Check if any DHCP options changed which require a client restat
+ for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+ 'no-default-route', 'vendor-class-id']:
+ dhcp = leaf_node_changed(config, ['vif', vif, 'dhcp-options', leaf_node])
+ if dhcp:
+ dict['vif'][vif].update({'dhcp_options_old' : dhcp})
+ # one option is suffiecient to set 'dhcp_options_old' key
+ break
+
for vif_s, vif_s_config in dict.get('vif_s', {}).items():
default_vif_s_values = defaults(base + ['vif-s'])
# XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c
@@ -491,10 +529,26 @@ def get_interface_dict(config, base, ifname=''):
# XXX: T2665: blend in proper DHCPv6-PD default values
dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s])
+ # If interface does not request an IPv4 DHCP address there is no need
+ # to keep the dhcp-options key
+ if 'address' not in dict['vif_s'][vif_s] or 'dhcp' not in \
+ dict['vif_s'][vif_s]['address']:
+ if 'dhcp_options' in dict['vif_s'][vif_s]:
+ del dict['vif_s'][vif_s]['dhcp_options']
+
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge')
if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge})
+ # Check if any DHCP options changed which require a client restat
+ for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+ 'no-default-route', 'vendor-class-id']:
+ dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'dhcp-options', leaf_node])
+ if dhcp:
+ dict['vif_s'][vif_s].update({'dhcp_options_old' : dhcp})
+ # one option is suffiecient to set 'dhcp_options_old' key
+ break
+
for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
default_vif_c_values = defaults(base + ['vif-s', 'vif-c'])
@@ -516,11 +570,28 @@ def get_interface_dict(config, base, ifname=''):
dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults(
dict['vif_s'][vif_s]['vif_c'][vif_c])
+ # If interface does not request an IPv4 DHCP address there is no need
+ # to keep the dhcp-options key
+ if 'address' not in dict['vif_s'][vif_s]['vif_c'][vif_c] or 'dhcp' \
+ not in dict['vif_s'][vif_s]['vif_c'][vif_c]['address']:
+ if 'dhcp_options' in dict['vif_s'][vif_s]['vif_c'][vif_c]:
+ del dict['vif_s'][vif_s]['vif_c'][vif_c]['dhcp_options']
+
# Check if we are a member of a bridge device
bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge')
if bridge: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
{'is_bridge_member' : bridge})
+ # Check if any DHCP options changed which require a client restat
+ for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+ 'no-default-route', 'vendor-class-id']:
+ dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c,
+ 'dhcp-options', leaf_node])
+ if dhcp:
+ dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_old' : dhcp})
+ # one option is suffiecient to set 'dhcp_options_old' key
+ break
+
# Check vif, vif-s/vif-c VLAN interfaces for removal
dict = get_removed_vlans(config, dict)
return dict
@@ -545,7 +616,6 @@ def get_vlan_ids(interface):
return vlan_ids
-
def get_accel_dict(config, base, chap_secrets):
"""
Common utility function to retrieve and mangle the Accel-PPP configuration
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 91c7f0c33..cf1887bf6 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1228,12 +1228,11 @@ class Interface(Control):
options_file = f'{config_base}_{ifname}.options'
pid_file = f'{config_base}_{ifname}.pid'
lease_file = f'{config_base}_{ifname}.leases'
-
- # Stop client with old config files to get the right IF_METRIC.
systemd_service = f'dhclient@{ifname}.service'
- if is_systemd_service_active(systemd_service):
- self._cmd(f'systemctl stop {systemd_service}')
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
if enable and 'disable' not in self._config:
if dict_search('dhcp_options.host_name', self._config) == None:
# read configured system hostname.
@@ -1244,16 +1243,19 @@ class Interface(Control):
tmp = {'dhcp_options' : { 'host_name' : hostname}}
self._config = dict_merge(tmp, self._config)
- render(options_file, 'dhcp-client/daemon-options.tmpl',
- self._config)
- render(config_file, 'dhcp-client/ipv4.tmpl',
- self._config)
+ render(options_file, 'dhcp-client/daemon-options.tmpl', self._config)
+ render(config_file, 'dhcp-client/ipv4.tmpl', self._config)
- # 'up' check is mandatory b/c even if the interface is A/D, as soon as
- # the DHCP client is started the interface will be placed in u/u state.
- # This is not what we intended to do when disabling an interface.
- return self._cmd(f'systemctl restart {systemd_service}')
+ # When the DHCP client is restarted a brief outage will occur, as
+ # the old lease is released a new one is acquired (T4203). We will
+ # only restart DHCP client if it's option changed, or if it's not
+ # running, but it should be running (e.g. on system startup)
+ if 'dhcp_options_old' in self._config or not is_systemd_service_active(systemd_service):
+ return self._cmd(f'systemctl restart {systemd_service}')
+ return None
else:
+ if is_systemd_service_active(systemd_service):
+ self._cmd(f'systemctl stop {systemd_service}')
# cleanup old config files
for file in [config_file, options_file, pid_file, lease_file]:
if os.path.isfile(file):
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 0c5282db4..516a19f24 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -68,6 +68,16 @@ class VXLANIf(Interface):
'vni' : 'id',
}
+ # IPv6 flowlabels can only be used on IPv6 tunnels, thus we need to
+ # ensure that at least the first remote IP address is passed to the
+ # tunnel creation command. Subsequent tunnel remote addresses can later
+ # be added to the FDB
+ remote_list = None
+ if 'remote' in self.config:
+ # skip first element as this is already configured as remote
+ remote_list = self.config['remote'][1:]
+ self.config['remote'] = self.config['remote'][0]
+
cmd = 'ip link add {ifname} type {type} dstport {port}'
for vyos_key, iproute2_key in mapping.items():
# dict_search will return an empty dict "{}" for valueless nodes like
@@ -82,3 +92,10 @@ class VXLANIf(Interface):
self._cmd(cmd.format(**self.config))
# interface is always A/D down. It needs to be enabled explicitly
self.set_admin_state('down')
+
+ # VXLAN tunnel is always recreated on any change - see interfaces-vxlan.py
+ if remote_list:
+ for remote in remote_list:
+ cmd = f'bridge fdb append to 00:00:00:00:00:00 dst {remote} ' \
+ 'port {port} dev {ifname}'
+ self._cmd(cmd.format(**self.config))
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 1767ff9d3..4526375df 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -774,6 +774,14 @@ def dict_search_recursive(dict_object, key):
for x in dict_search_recursive(j, key):
yield x
+def get_bridge_fdb(interface):
+ """ Returns the forwarding database entries for a given interface """
+ if not os.path.exists(f'/sys/class/net/{interface}'):
+ return None
+ from json import loads
+ tmp = loads(cmd(f'bridge -j fdb show dev {interface}'))
+ return tmp
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 9de961249..ba5acf5d6 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -56,6 +56,7 @@ def is_mirrored_to(interface, mirror_if, qdisc):
class BasicInterfaceTest:
class TestCase(VyOSUnitTestSHIM.TestCase):
+ _test_dhcp = False
_test_ip = False
_test_mtu = False
_test_vlan = False
@@ -96,6 +97,35 @@ class BasicInterfaceTest:
for intf in self._interfaces:
self.assertNotIn(intf, interfaces())
+ # No daemon that was started during a test should remain running
+ for daemon in ['dhcp6c', 'dhclient']:
+ self.assertFalse(process_named_running(daemon))
+
+ def test_dhcp_disable_interface(self):
+ if not self._test_dhcp:
+ self.skipTest('not supported')
+
+ # When interface is configured as admin down, it must be admin down
+ # even when dhcpc starts on the given interface
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'disable'])
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'disable'])
+
+ # Also enable DHCP (ISC DHCP always places interface in admin up
+ # state so we check that we do not start DHCP client.
+ # https://phabricator.vyos.net/T2767
+ self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ flags = read_file(f'/sys/class/net/{interface}/flags')
+ self.assertEqual(int(flags, 16) & 1, 0)
+
def test_span_mirror(self):
if not self._mirror_interfaces:
self.skipTest('not supported')
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 1d9a887bd..4f2fe979a 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -28,6 +28,7 @@ from vyos.util import read_file
class BondingInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
+ cls._test_dhcp = True
cls._test_ip = True
cls._test_ipv6 = True
cls._test_ipv6_pd = True
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 4f7e03298..f2e111425 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -31,6 +31,7 @@ from vyos.validate import is_intf_addr_assigned
class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
+ cls._test_dhcp = True
cls._test_ip = True
cls._test_ipv6 = True
cls._test_ipv6_pd = True
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index ae3d5ed96..ee7649af8 100755
--- a/smoketest/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -102,6 +102,7 @@ def get_certificate_count(interface, cert_type):
class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
+ cls._test_dhcp = True
cls._test_ip = True
cls._test_ipv6 = True
cls._test_ipv6_pd = True
@@ -146,24 +147,6 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
- def test_dhcp_disable_interface(self):
- # When interface is configured as admin down, it must be admin down
- # even when dhcpc starts on the given interface
- for interface in self._interfaces:
- self.cli_set(self._base_path + [interface, 'disable'])
-
- # Also enable DHCP (ISC DHCP always places interface in admin up
- # state so we check that we do not start DHCP client.
- # https://phabricator.vyos.net/T2767
- self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
-
- self.cli_commit()
-
- # Validate interface state
- for interface in self._interfaces:
- flags = read_file(f'/sys/class/net/{interface}/flags')
- self.assertEqual(int(flags, 16) & 1, 0)
-
def test_offloading_rps(self):
# enable RPS on all available CPUs, RPS works woth a CPU bitmask,
# where each bit represents a CPU (core/thread). The formula below
diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py
index e4280a5b7..5b10bfa44 100755
--- a/smoketest/scripts/cli/test_interfaces_macsec.py
+++ b/smoketest/scripts/cli/test_interfaces_macsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -40,6 +40,7 @@ def get_cipher(interface):
class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
+ cls._test_dhcp = True
cls._test_ip = True
cls._test_ipv6 = True
cls._base_path = ['interfaces', 'macsec']
diff --git a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
index ae899cddd..adcadc5eb 100755
--- a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -23,6 +23,7 @@ from base_interfaces_test import BasicInterfaceTest
class PEthInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
+ cls._test_dhcp = True
cls._test_ip = True
cls._test_ipv6 = True
cls._test_ipv6_pd = True
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 9278adadd..f34b99ea4 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -18,8 +18,9 @@ import unittest
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
+from vyos.util import get_bridge_fdb
from vyos.util import get_interface_config
-
+from vyos.template import is_ipv6
from base_interfaces_test import BasicInterfaceTest
class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
@@ -33,6 +34,8 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
'vxlan10': ['vni 10', 'remote 127.0.0.2'],
'vxlan20': ['vni 20', 'group 239.1.1.1', 'source-interface eth0'],
'vxlan30': ['vni 30', 'remote 2001:db8:2000::1', 'source-address 2001:db8:1000::1', 'parameters ipv6 flowlabel 0x1000'],
+ 'vxlan40': ['vni 40', 'remote 127.0.0.2', 'remote 127.0.0.3'],
+ 'vxlan50': ['vni 50', 'remote 2001:db8:2000::1', 'remote 2001:db8:2000::2', 'parameters ipv6 flowlabel 0x1000'],
}
cls._interfaces = list(cls._options)
# call base-classes classmethod
@@ -55,21 +58,34 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
ttl = 20
for interface in self._interfaces:
options = get_interface_config(interface)
+ bridge = get_bridge_fdb(interface)
vni = options['linkinfo']['info_data']['id']
self.assertIn(f'vni {vni}', self._options[interface])
- if any('link' in s for s in self._options[interface]):
+ if any('source-interface' in s for s in self._options[interface]):
link = options['linkinfo']['info_data']['link']
self.assertIn(f'source-interface {link}', self._options[interface])
- if any('local6' in s for s in self._options[interface]):
- remote = options['linkinfo']['info_data']['local6']
- self.assertIn(f'source-address {local6}', self._options[interface])
-
- if any('remote6' in s for s in self._options[interface]):
- remote = options['linkinfo']['info_data']['remote6']
- self.assertIn(f'remote {remote}', self._options[interface])
+ # Verify source-address setting was properly configured on the Kernel
+ if any('source-address' in s for s in self._options[interface]):
+ for s in self._options[interface]:
+ if 'source-address' in s:
+ address = s.split()[-1]
+ if is_ipv6(address):
+ tmp = options['linkinfo']['info_data']['local6']
+ else:
+ tmp = options['linkinfo']['info_data']['local']
+ self.assertIn(f'source-address {tmp}', self._options[interface])
+
+ # Verify remote setting was properly configured on the Kernel
+ if any('remote' in s for s in self._options[interface]):
+ for s in self._options[interface]:
+ if 'remote' in s:
+ for fdb in bridge:
+ if 'mac' in fdb and fdb['mac'] == '00:00:00:00:00:00':
+ remote = fdb['dst']
+ self.assertIn(f'remote {remote}', self._options[interface])
if any('group' in s for s in self._options[interface]):
group = options['linkinfo']['info_data']['group']
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 14079c905..1338fe81c 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -28,6 +28,7 @@ vti_path = ['interfaces', 'vti']
nhrp_path = ['protocols', 'nhrp']
base_path = ['vpn', 'ipsec']
+charon_file = '/etc/strongswan.d/charon.conf'
dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting'
swanctl_file = '/etc/swanctl/swanctl.conf'
@@ -244,6 +245,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
peer_base_path = base_path + ['site-to-site', 'peer', peer_ip]
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
+ self.cli_set(peer_base_path + ['connection-type', 'none'])
self.cli_set(peer_base_path + ['ike-group', ike_group])
self.cli_set(peer_base_path + ['default-esp-group', esp_group])
self.cli_set(peer_base_path + ['local-address', local_address])
@@ -272,6 +274,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'mode = tunnel',
f'local_ts = 172.16.10.0/24,172.16.11.0/24',
f'remote_ts = 172.17.10.0/24,172.17.11.0/24',
+ f'start_action = none',
f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
f'if_id_out = {if_id}',
f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
@@ -423,5 +426,75 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
# There is only one VTI test so no need to delete this globally in tearDown()
self.cli_delete(vti_path)
+
+ def test_06_flex_vpn_vips(self):
+ local_address = '192.0.2.5'
+ local_id = 'vyos-r1'
+ remote_id = 'vyos-r2'
+ peer_base_path = base_path + ['site-to-site', 'peer', peer_ip]
+
+ self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre'])
+ self.cli_set(tunnel_path + ['tun1', 'source-address', local_address])
+
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['options', 'flexvpn'])
+ self.cli_set(base_path + ['options', 'interface', 'tun1'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'ikev2-reauth', 'no'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+
+ self.cli_set(peer_base_path + ['authentication', 'id', local_id])
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
+ self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id])
+ self.cli_set(peer_base_path + ['connection-type', 'initiate'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre'])
+
+ self.cli_set(peer_base_path + ['virtual-address', '203.0.113.55'])
+ self.cli_set(peer_base_path + ['virtual-address', '203.0.113.56'])
+
+ self.cli_commit()
+
+ # Verify strongSwan configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ f'version = 2',
+ f'vips = 203.0.113.55, 203.0.113.56',
+ f'life_time = 3600s', # default value
+ f'local_addrs = {local_address} # dhcp:no',
+ f'remote_addrs = {peer_ip}',
+ f'peer_{peer_ip.replace(".","-")}_tunnel_1',
+ f'mode = tunnel',
+ ]
+
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'id-local = {local_address} # dhcp:no',
+ f'id-remote = {peer_ip}',
+ f'id-localid = {local_id}',
+ f'id-remoteid = {remote_id}',
+ f'secret = "{secret}"',
+ ]
+
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Verify charon configuration
+ charon_conf = read_file(charon_file)
+ charon_conf_lines = [
+ f'# Cisco FlexVPN',
+ f'cisco_flexvpn = yes',
+ f'install_virtual_ip = yes',
+ f'install_virtual_ip_on = tun1',
+ ]
+
+ for line in charon_conf_lines:
+ self.assertIn(line, charon_conf)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4d3ebc587..f4dba9d4a 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,7 +22,6 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configdict import has_vlan_subinterface_configured
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 1f097c4e3..85604508e 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -34,8 +34,8 @@ airbag.enable()
def get_config(config=None):
"""
- Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
- interface name will be added or a deleted flag
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
"""
if config:
conf = config
@@ -70,8 +70,7 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
-
+ raise ConfigError('Multicast VXLAN requires an underlaying interface')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
@@ -108,15 +107,33 @@ def verify(vxlan):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+ # Check for mixed IPv4 and IPv6 addresses
+ protocol = None
+ if 'source_address' in vxlan:
+ if is_ipv6(vxlan['source_address']):
+ protocol = 'ipv6'
+ else:
+ protocol = 'ipv4'
+
+ if 'remote' in vxlan:
+ error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+ for remote in vxlan['remote']:
+ if is_ipv6(remote):
+ if protocol == 'ipv4':
+ raise ConfigError(error_msg)
+ protocol = 'ipv6'
+ else:
+ if protocol == 'ipv6':
+ raise ConfigError(error_msg)
+ protocol = 'ipv4'
+
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
return None
-
def generate(vxlan):
return None
-
def apply(vxlan):
# Check if the VXLAN interface already exists
if vxlan['ifname'] in interfaces():
@@ -132,7 +149,6 @@ def apply(vxlan):
return None
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
index cfd321522..3bdf5a718 100755
--- a/src/op_mode/cpu_summary.py
+++ b/src/op_mode/cpu_summary.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 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
@@ -19,18 +19,30 @@ from vyos.util import colon_separated_to_dict
FILE_NAME = '/proc/cpuinfo'
-with open(FILE_NAME, 'r') as f:
- data_raw = f.read()
+def get_raw_data():
+ with open(FILE_NAME, 'r') as f:
+ data_raw = f.read()
-data = colon_separated_to_dict(data_raw)
+ data = colon_separated_to_dict(data_raw)
-# Accumulate all data in a dict for future support for machine-readable output
-cpu_data = {}
-cpu_data['cpu_number'] = len(data['processor'])
-cpu_data['models'] = list(set(data['model name']))
+ # Accumulate all data in a dict for future support for machine-readable output
+ cpu_data = {}
+ cpu_data['cpu_number'] = len(data['processor'])
+ cpu_data['models'] = list(set(data['model name']))
-# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
-cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+ # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
+ cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']))
+
+ return cpu_data
+
+def get_formatted_output():
+ cpu_data = get_raw_data()
+
+ out = "CPU(s): {0}\n".format(cpu_data['cpu_number'])
+ out += "CPU model(s): {0}".format(",".join(cpu_data['models']))
+
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
-print("CPU(s): {0}".format(cpu_data['cpu_number']))
-print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 679b03c0b..fd4f86d88 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -33,10 +33,12 @@ def utc2local(datetime):
def parse_time(s):
try:
- if re.match(r'^\d{1,2}$', s):
- if (int(s) > 59):
+ if re.match(r'^\d{1,9999}$', s):
+ if (int(s) > 59) and (int(s) < 1440):
s = str(int(s)//60) + ":" + str(int(s)%60)
return datetime.strptime(s, "%H:%M").time()
+ if (int(s) >= 1440):
+ return s.split()
else:
return datetime.strptime(s, "%M").time()
else:
@@ -141,7 +143,7 @@ def execute_shutdown(time, reboot=True, ask=True):
cmd(f'/usr/bin/wall "{wall_msg}"')
else:
if not ts:
- exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
+ exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format')
else:
exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')
else:
@@ -172,7 +174,12 @@ def main():
action.add_argument("--reboot", "-r",
help="Reboot the system",
nargs="*",
- metavar="Minutes|HH:MM")
+ metavar="HH:MM")
+
+ action.add_argument("--reboot_in", "-i",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes")
action.add_argument("--poweroff", "-p",
help="Poweroff the system",
@@ -190,7 +197,17 @@ def main():
try:
if args.reboot is not None:
+ for r in args.reboot:
+ if ':' not in r and '/' not in r and '.' not in r:
+ print("Incorrect format! Use HH:MM")
+ exit(1)
execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.reboot_in is not None:
+ for i in args.reboot_in:
+ if ':' in i:
+ print("Incorrect format! Use Minutes")
+ exit(1)
+ execute_shutdown(args.reboot_in, reboot=True, ask=args.yes)
if args.poweroff is not None:
execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
if args.cancel:
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
index 0040e950d..9973d9789 100755
--- a/src/op_mode/show_cpu.py
+++ b/src/op_mode/show_cpu.py
@@ -21,7 +21,7 @@ from sys import exit
from vyos.util import popen, DEVNULL
OUT_TMPL_SRC = """
-{% if cpu %}
+{%- if cpu -%}
{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{% endif %}
{% if 'model' in cpu %}Model: {{cpu.model}}{% endif %}
{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{% endif %}
@@ -31,31 +31,42 @@ OUT_TMPL_SRC = """
{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{% endif %}
{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{% endif %}
{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{% endif %}
-{% endif %}
+{%- endif -%}
"""
-cpu = {}
-cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
-
-if code == 0:
- cpu_info = json.loads(cpu_json)
- if len(cpu_info) > 0 and 'lscpu' in cpu_info:
- for prop in cpu_info['lscpu']:
- if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
- if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
- if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
- if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
- if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
- if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
- if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
- if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
- if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
-
-if len(cpu) > 0:
- tmp = { 'cpu':cpu }
+def get_raw_data():
+ cpu = {}
+ cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
+
+ if code == 0:
+ cpu_info = json.loads(cpu_json)
+ if len(cpu_info) > 0 and 'lscpu' in cpu_info:
+ for prop in cpu_info['lscpu']:
+ if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
+ if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
+ if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
+ if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
+ if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
+ if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
+ if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
+ if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
+ if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
+
+ return cpu
+
+def get_formatted_output():
+ cpu = get_raw_data()
+
+ tmp = {'cpu':cpu}
tmpl = Template(OUT_TMPL_SRC)
- print(tmpl.render(tmp))
- exit(0)
-else:
- print('CPU information could not be determined\n')
- exit(1)
+ return tmpl.render(tmp)
+
+if __name__ == '__main__':
+ cpu = get_raw_data()
+
+ if len(cpu) > 0:
+ print(get_formatted_output())
+ else:
+ print('CPU information could not be determined\n')
+ exit(1)
+
diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py
index 5818ec132..2b0be3965 100755
--- a/src/op_mode/show_ram.py
+++ b/src/op_mode/show_ram.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2022 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
@@ -55,10 +55,17 @@ def get_system_memory_human():
return mem
-if __name__ == '__main__':
- mem = get_system_memory_human()
+def get_raw_data():
+ return get_system_memory_human()
+
+def get_formatted_output():
+ mem = get_raw_data()
- print("Total: {}".format(mem["total"]))
- print("Free: {}".format(mem["free"]))
- print("Used: {}".format(mem["used"]))
+ out = "Total: {}\n".format(mem["total"])
+ out += "Free: {}\n".format(mem["free"])
+ out += "Used: {}".format(mem["used"])
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py
index c3dea52e6..1b5e33fa9 100755
--- a/src/op_mode/show_uptime.py
+++ b/src/op_mode/show_uptime.py
@@ -37,14 +37,27 @@ def get_load_averages():
return res
-if __name__ == '__main__':
+def get_raw_data():
from vyos.util import seconds_to_human
- print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds())))
+ res = {}
+ res["uptime_seconds"] = get_uptime_seconds()
+ res["uptime"] = seconds_to_human(get_uptime_seconds())
+ res["load_average"] = get_load_averages()
+
+ return res
- avgs = get_load_averages()
+def get_formatted_output():
+ data = get_raw_data()
- print("Load averages:")
- print("1 minute: {:.02f}%".format(avgs[1]*100))
- print("5 minutes: {:.02f}%".format(avgs[5]*100))
- print("15 minutes: {:.02f}%".format(avgs[15]*100))
+ out = "Uptime: {}\n\n".format(data["uptime"])
+ avgs = data["load_average"]
+ out += "Load averages:\n"
+ out += "1 minute: {:.02f}%\n".format(avgs[1]*100)
+ out += "5 minutes: {:.02f}%\n".format(avgs[5]*100)
+ out += "15 minutes: {:.02f}%\n".format(avgs[15]*100)
+
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
index 7962e1e7b..b82ab6eca 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/show_version.py
@@ -26,10 +26,6 @@ from jinja2 import Template
from sys import exit
from vyos.util import call
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
-parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-
version_output_tmpl = """
Version: VyOS {{version}}
Release train: {{release_train}}
@@ -51,7 +47,20 @@ Hardware UUID: {{hardware_uuid}}
Copyright: VyOS maintainers and contributors
"""
+def get_raw_data():
+ version_data = vyos.version.get_full_version_data()
+ return version_data
+
+def get_formatted_output():
+ version_data = get_raw_data()
+ tmpl = Template(version_output_tmpl)
+ return tmpl.render(version_data)
+
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+ parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
args = parser.parse_args()
version_data = vyos.version.get_full_version_data()
@@ -60,9 +69,8 @@ if __name__ == '__main__':
import json
print(json.dumps(version_data))
exit(0)
-
- tmpl = Template(version_output_tmpl)
- print(tmpl.render(version_data))
+ else:
+ print(get_formatted_output())
if args.funny:
print(vyos.limericks.get_random())