diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/firewall.py | 1 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_openvpn.py | 6 | ||||
-rwxr-xr-x | src/conf_mode/load-balancing_reverse-proxy.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 36 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 16 | ||||
-rwxr-xr-x | src/conf_mode/system_ip.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 14 | ||||
-rw-r--r-- | src/etc/rsyslog.conf | 30 | ||||
-rwxr-xr-x | src/helpers/vyos-failover.py | 5 | ||||
-rwxr-xr-x | src/helpers/vyos-vrrp-conntracksync.sh | 4 | ||||
-rwxr-xr-x | src/init/vyos-router | 1 | ||||
-rwxr-xr-x | src/migration-scripts/firewall/14-to-15 | 46 | ||||
-rwxr-xr-x | src/op_mode/connect_disconnect.py | 6 | ||||
-rwxr-xr-x | src/op_mode/image_installer.py | 25 | ||||
-rwxr-xr-x | src/op_mode/openvpn.py | 7 | ||||
-rwxr-xr-x | src/op_mode/pki.py | 4 | ||||
-rwxr-xr-x | src/op_mode/uptime.py | 2 |
17 files changed, 168 insertions, 48 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 3cf618363..e96e57154 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -44,6 +44,7 @@ nftables_conf = '/run/nftables.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'enable': '0', 'disable': '1'}, + 'directed_broadcast' : {'sysfs': '/proc/sys/net/ipv4/conf/all/bc_forwarding', 'enable': '1', 'disable': '0'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'enable': '0', 'disable': '-1'}, diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 505ec55c6..0ecffd3be 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -198,6 +198,12 @@ def verify_pki(openvpn): raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') if 'dh_params' in tls: + if 'dh' not in pki: + raise ConfigError(f'pki dh is not configured') + proposed_dh = tls['dh_params'] + if proposed_dh not in pki['dh'].keys(): + raise ConfigError(f"pki dh '{proposed_dh}' is not configured") + pki_dh = pki['dh'][tls['dh_params']] dh_params = load_dh_parameters(pki_dh['parameters']) dh_numbers = dh_params.parameter_numbers() diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 694a4e1ea..1569d8d71 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -75,6 +75,10 @@ def verify(lb): raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') for back, back_config in lb['backend'].items(): + if 'http-check' in back_config: + http_check = back_config['http-check'] + if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: + raise ConfigError(f'"expect status" and "expect string" can not be configured together!') if 'server' not in back_config: raise ConfigError(f'"{back} server" must be configured!') for bk_server, bk_server_conf in back_config['server'].items(): @@ -84,6 +88,10 @@ def verify(lb): if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf): raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') + if 'ssl' in back_config: + if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): + raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') + for front, front_config in lb['service'].items(): for cert in dict_search('ssl.certificate', front_config) or []: verify_pki_certificate(lb, cert) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 3ab6ac5c3..8deec0e85 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -24,6 +24,8 @@ from vyos.config import config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import node_changed +from vyos.configdiff import Diff +from vyos.configdiff import get_config_diff from vyos.defaults import directories from vyos.pki import is_ca_certificate from vyos.pki import load_certificate @@ -136,32 +138,32 @@ def get_config(config=None): if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} - tmp = node_changed(conf, base + ['ca'], recursive=True) + tmp = node_changed(conf, base + ['ca'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'ca' : tmp}) - tmp = node_changed(conf, base + ['certificate'], recursive=True) + tmp = node_changed(conf, base + ['certificate'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'certificate' : tmp}) - tmp = node_changed(conf, base + ['dh'], recursive=True) + tmp = node_changed(conf, base + ['dh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'dh' : tmp}) - tmp = node_changed(conf, base + ['key-pair'], recursive=True) + tmp = node_changed(conf, base + ['key-pair'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'key_pair' : tmp}) - tmp = node_changed(conf, base + ['openssh'], recursive=True) + tmp = node_changed(conf, base + ['openssh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'openssh' : tmp}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True) + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'openvpn' : tmp}) @@ -198,6 +200,7 @@ def get_config(config=None): pki['system'] = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + D = get_config_diff(conf) for search in sync_search: for key in search['keys']: @@ -217,15 +220,22 @@ def get_config(config=None): if not search_dict: continue for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path = search['path'] - path_str = ' '.join(path + found_path) - print(f'PKI: Updating config: {path_str} {found_name}') + if isinstance(found_name, list) and item_name not in found_name: + continue + + if isinstance(found_name, str) and found_name != item_name: + continue + + path = search['path'] + path_str = ' '.join(path + found_path) + print(f'PKI: Updating config: {path_str} {item_name}') - if path[0] == 'interfaces': - ifname = found_path[0] + if path[0] == 'interfaces': + ifname = found_path[0] + if not D.node_changed_presence(path + [ifname]): set_dependents(path[1], conf, ifname) - else: + else: + if not D.node_changed_presence(path): set_dependents(path[1], conf) return pki diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 2b16de775..4df97d133 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -473,6 +473,22 @@ def verify(bgp): if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') + # T5833 not all AFIs are supported for VRF + if 'vrf' in bgp and 'address_family' in peer_config: + unsupported_vrf_afi = { + 'ipv4_flowspec', + 'ipv6_flowspec', + 'ipv4_labeled_unicast', + 'ipv6_labeled_unicast', + 'ipv4_vpn', + 'ipv6_vpn', + } + for afi in peer_config['address_family']: + if afi in unsupported_vrf_afi: + raise ConfigError( + f"VRF is not allowed for address-family '{afi.replace('_', '-')}'" + ) + # Throw an error if a peer group is not configured for allow range for prefix in dict_search('listen.range', bgp) or []: # we can not use dict_search() here as prefix contains dots ... diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py index b945b51f2..2a0bda91a 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -81,11 +81,6 @@ def apply(opt): value = '0' if (tmp != None) else '1' write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) - # enable/disable IPv4 directed broadcast forwarding - tmp = dict_search('disable_directed_broadcast', opt) - value = '0' if (tmp != None) else '1' - write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value) - # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 1fc813189..587309005 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -315,6 +315,20 @@ def apply(vrf): for chain, rule in nftables_rules.items(): cmd(f'nft flush chain inet vrf_zones {chain}') + # Return default ip rule values + if 'name' not in vrf: + for afi in ['-4', '-6']: + # move lookup local to pref 0 (from 32765) + if not has_rule(afi, 0, 'local'): + call(f'ip {afi} rule add pref 0 from all lookup local') + if has_rule(afi, 32765, 'local'): + call(f'ip {afi} rule del pref 32765 table local') + + if has_rule(afi, 1000, 'l3mdev'): + call(f'ip {afi} rule del pref 1000 l3mdev protocol kernel') + if has_rule(afi, 2000, 'l3mdev'): + call(f'ip {afi} rule del pref 2000 l3mdev unreachable') + # Apply FRR filters zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions diff --git a/src/etc/rsyslog.conf b/src/etc/rsyslog.conf index 9781f0835..b3f41acb6 100644 --- a/src/etc/rsyslog.conf +++ b/src/etc/rsyslog.conf @@ -15,21 +15,6 @@ $KLogPath /proc/kmsg #### GLOBAL DIRECTIVES #### ########################### -# The lines below cause all listed daemons/processes to be logged into -# /var/log/auth.log, then drops the message so it does not also go to the -# regular syslog so that messages are not duplicated - -$outchannel auth_log,/var/log/auth.log -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then :omfile:$auth_log - -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then stop - # Use traditional timestamp format. # To enable high precision timestamps, comment out the following line. # A modern-style logfile format similar to TraditionalFileFormat, buth with high-precision timestamps and timezone information @@ -60,6 +45,21 @@ $Umask 0022 # $IncludeConfig /etc/rsyslog.d/*.conf +# The lines below cause all listed daemons/processes to be logged into +# /var/log/auth.log, then drops the message so it does not also go to the +# regular syslog so that messages are not duplicated + +$outchannel auth_log,/var/log/auth.log +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then :omfile:$auth_log + +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then stop + ############### #### RULES #### ############### diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py index f34c18916..348974364 100755 --- a/src/helpers/vyos-failover.py +++ b/src/helpers/vyos-failover.py @@ -197,6 +197,7 @@ if __name__ == '__main__': proto = nexthop_config.get('check').get('type') target = nexthop_config.get('check').get('target') timeout = nexthop_config.get('check').get('timeout') + onlink = 'onlink' if 'onlink' in nexthop_config else '' # Route not found in the current routing table if not is_route_exists(route, next_hop, conf_iface, conf_metric): @@ -206,14 +207,14 @@ if __name__ == '__main__': if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} ' f'metric {conf_metric} proto failover\n###') rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} ' - f'metric {conf_metric} proto failover') + f'{onlink} metric {conf_metric} proto failover') # If something is wrong and gateway not added # Example: Error: Next-hop has invalid gateway. if rc !=0: if debug: print(f'{command} -- return-code [RC: {rc}] {next_hop} dev {conf_iface}') else: journal.send(f'ip route add {route} via {next_hop} dev {conf_iface} ' - f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) + f'{onlink} metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) else: if debug: print(f' [ TARGET_FAIL ] target checks fails for [{target}], do nothing') journal.send(f'Check fail for route {route} target {target} proto {proto} ' diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh index 0cc718938..90fa77f23 100755 --- a/src/helpers/vyos-vrrp-conntracksync.sh +++ b/src/helpers/vyos-vrrp-conntracksync.sh @@ -25,7 +25,7 @@ LOGCMD="logger -t $TAG -p $FACILITY.$LEVEL" VRRP_GRP="VRRP sync-group [$2]" FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state" -$LOGCMD "vyatta-vrrp-conntracksync invoked at `date`" +$LOGCMD "vyos-vrrp-conntracksync invoked at `date`" if ! systemctl is-active --quiet conntrackd.service; then echo "conntrackd service not running" @@ -148,7 +148,7 @@ case "$1" in *) echo UNKNOWN at `date` > $FAILOVER_STATE $LOGCMD "ERROR: `uname -n` unknown state transition for $VRRP_GRP" - echo "Usage: vyatta-vrrp-conntracksync.sh {master|backup|fault}" + echo "Usage: vyos-vrrp-conntracksync.sh {master|backup|fault}" exit 1 ;; esac diff --git a/src/init/vyos-router b/src/init/vyos-router index 06fea140d..15e37df07 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -451,6 +451,7 @@ start () touch /tmp/vyos.ifconfig.debug touch /tmp/vyos.frr.debug touch /tmp/vyos.container.debug + touch /tmp/vyos.smoketest.debug fi log_action_begin_msg "Mounting VyOS Config" diff --git a/src/migration-scripts/firewall/14-to-15 b/src/migration-scripts/firewall/14-to-15 new file mode 100755 index 000000000..735839365 --- /dev/null +++ b/src/migration-scripts/firewall/14-to-15 @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5535: Migrate <set system ip disable-directed-broadcast> to <set firewall global-options directed-broadcas [enable|disable] + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['firewall'] + +if config.exists(['system', 'ip', 'disable-directed-broadcast']): + config.set(['firewall', 'global-options', 'directed-broadcast'], value='disable') + config.delete(['system', 'ip', 'disable-directed-broadcast']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1)
\ No newline at end of file diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index bd02dc6ea..373f9e953 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -48,7 +48,7 @@ def connect(interface): if os.path.isdir(f'/sys/class/net/{interface}'): print(f'Interface {interface}: already connected!') elif check_ppp_running(interface): - print(f'Interface {interface}: connection is beeing established!') + print(f'Interface {interface}: connection is being established!') else: print(f'Interface {interface}: connecting...') call(f'systemctl restart ppp@{interface}.service') @@ -58,7 +58,7 @@ def connect(interface): else: call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces_wwan.py') else: - print(f'Unknown interface {interface}, can not connect. Aborting!') + print(f'Unknown interface {interface}, cannot connect. Aborting!') # Reaply QoS configuration config = ConfigTreeQuery() @@ -90,7 +90,7 @@ def disconnect(interface): modem = interface.lstrip('wwan') call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL) else: - print(f'Unknown interface {interface}, can not disconnect. Aborting!') + print(f'Unknown interface {interface}, cannot disconnect. Aborting!') def main(): parser = argparse.ArgumentParser() diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 9f6949fb3..ba0e3b6db 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -26,6 +26,7 @@ from os import environ from typing import Union from urllib.parse import urlparse from passlib.hosts import linux_context +from errno import ENOSPC from psutil import disk_partitions @@ -60,7 +61,8 @@ MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boo MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?' MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?' MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?' -MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user' +MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:' +MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:' MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?' MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?' MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?' @@ -74,6 +76,7 @@ MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.' MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again' MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\ 'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9' +MSG_WARN_PASSWORD_CONFIRM: str = 'The entered values did not match. Try again' CONST_MIN_DISK_SIZE: int = 2147483648 # 2 GB CONST_MIN_ROOT_SIZE: int = 1610612736 # 1.5 GB # a reserved space: 2MB for header, 1 MB for BIOS partition, 256 MB for EFI @@ -695,8 +698,14 @@ def install_image() -> None: print(MSG_WARN_IMAGE_NAME_WRONG) # ask for password - user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos', - no_echo=True) + while True: + user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True, + non_empty=True) + confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True, + non_empty=True) + if user_password == confirm: + break + print(MSG_WARN_PASSWORD_CONFIRM) # ask for default console console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, @@ -931,6 +940,16 @@ def add_image(image_path: str, vrf: str = None, username: str = '', if set_as_default: grub.set_default(image_name, root_dir) + except OSError as e: + # if no space error, remove image dir and cleanup + if e.errno == ENOSPC: + cleanup(mounts=[str(iso_path)], + remove_items=[f'{root_dir}/boot/{image_name}']) + else: + # unmount an ISO and cleanup + cleanup([str(iso_path)]) + exit(f'Error: {e}') + except Exception as err: # unmount an ISO and cleanup cleanup([str(iso_path)]) diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py index d54a67199..092873909 100755 --- a/src/op_mode/openvpn.py +++ b/src/op_mode/openvpn.py @@ -48,9 +48,12 @@ def _get_tunnel_address(peer_host, peer_port, status_file): # 10.10.2.0/25,client1,... lst = [l for l in lst[1:] if '/' not in l.split(',')[0]] - tunnel_ip = lst[0].split(',')[0] + if lst: + tunnel_ip = lst[0].split(',')[0] - return tunnel_ip + return tunnel_ip + + return 'n/a' def _get_interface_status(mode: str, interface: str) -> dict: status_file = f'/run/openvpn/{interface}.status' diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index ad2c1ada0..b1ca6ee29 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -306,7 +306,7 @@ def parse_san_string(san_string): output.append(ipaddress.IPv4Address(value)) elif tag == 'ipv6': output.append(ipaddress.IPv6Address(value)) - elif tag == 'dns': + elif tag == 'dns' or tag == 'rfc822': output.append(value) return output @@ -324,7 +324,7 @@ def generate_certificate_request(private_key=None, key_type=None, return_request subject_alt_names = None if ask_san and ask_yes_no('Do you want to configure Subject Alternative Names?'): - print("Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net") + print("Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net,rfc822:user@vyos.net") san_string = ask_input('Enter Subject Alternative Names:') subject_alt_names = parse_san_string(san_string) diff --git a/src/op_mode/uptime.py b/src/op_mode/uptime.py index d6adf6f4d..059a4c3f6 100755 --- a/src/op_mode/uptime.py +++ b/src/op_mode/uptime.py @@ -49,7 +49,7 @@ def _get_raw_data(): res = {} res["uptime_seconds"] = _get_uptime_seconds() - res["uptime"] = seconds_to_human(_get_uptime_seconds()) + res["uptime"] = seconds_to_human(_get_uptime_seconds(), separator=' ') res["load_average"] = _get_load_averages() return res |