diff options
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><Minutes></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><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> + <list><DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></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()) |