summaryrefslogtreecommitdiff
path: root/src/system
diff options
context:
space:
mode:
Diffstat (limited to 'src/system')
-rw-r--r--src/system/grub_update.py112
-rw-r--r--src/system/keepalived-fifo.py194
-rw-r--r--src/system/normalize-ip43
-rw-r--r--src/system/on-dhcp-event.sh98
-rw-r--r--src/system/on-dhcpv6-event.sh87
-rw-r--r--src/system/post-upgrade3
-rw-r--r--src/system/standalone_root_pw_reset178
-rw-r--r--src/system/uacctd_stop.py68
-rw-r--r--src/system/vyos-config-cloud-init.py169
-rw-r--r--src/system/vyos-event-handler.py168
-rw-r--r--src/system/vyos-system-update-check.py70
11 files changed, 1190 insertions, 0 deletions
diff --git a/src/system/grub_update.py b/src/system/grub_update.py
new file mode 100644
index 0000000..5a05341
--- /dev/null
+++ b/src/system/grub_update.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This file is part of VyOS.
+#
+# VyOS is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# VyOS 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
+# VyOS. If not, see <https://www.gnu.org/licenses/>.
+
+from pathlib import Path
+from sys import exit
+
+from vyos.system import disk, grub, image, compat, SYSTEM_CFG_VER
+from vyos.template import render
+
+
+def cfg_check_update() -> bool:
+ """Check if GRUB structure update is required
+
+ Returns:
+ bool: False if not required, True if required
+ """
+ current_ver = grub.get_cfg_ver()
+ if current_ver and current_ver >= SYSTEM_CFG_VER:
+ return False
+
+ return True
+
+
+if __name__ == '__main__':
+ if image.is_live_boot():
+ exit(0)
+
+ if image.is_running_as_container():
+ exit(0)
+
+ # Skip everything if update is not required
+ if not cfg_check_update():
+ exit(0)
+
+ # find root directory of persistent storage
+ root_dir = disk.find_persistence()
+
+ # read current GRUB config
+ grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}'
+ vars = grub.vars_read(grub_cfg_main)
+ modules = grub.modules_read(grub_cfg_main)
+ vyos_menuentries = compat.parse_menuentries(grub_cfg_main)
+ vyos_versions = compat.find_versions(vyos_menuentries)
+ unparsed_items = compat.filter_unparsed(grub_cfg_main)
+ # compatibilty for raid installs
+ search_root = compat.get_search_root(unparsed_items)
+ common_dict = {}
+ common_dict['search_root'] = search_root
+ # find default values
+ default_entry = vyos_menuentries[int(vars['default'])]
+ default_settings = {
+ 'default': grub.gen_version_uuid(default_entry['version']),
+ 'bootmode': default_entry['bootmode'],
+ 'console_type': default_entry['console_type'],
+ 'console_num': default_entry['console_num'],
+ 'console_speed': default_entry['console_speed']
+ }
+ vars.update(default_settings)
+
+ # create new files
+ grub_cfg_vars = f'{root_dir}/{grub.CFG_VYOS_VARS}'
+ grub_cfg_modules = f'{root_dir}/{grub.CFG_VYOS_MODULES}'
+ grub_cfg_platform = f'{root_dir}/{grub.CFG_VYOS_PLATFORM}'
+ grub_cfg_menu = f'{root_dir}/{grub.CFG_VYOS_MENU}'
+ grub_cfg_options = f'{root_dir}/{grub.CFG_VYOS_OPTIONS}'
+
+ Path(image.GRUB_DIR_VYOS).mkdir(exist_ok=True)
+ grub.vars_write(grub_cfg_vars, vars)
+ grub.modules_write(grub_cfg_modules, modules)
+ grub.common_write(grub_common=common_dict)
+ render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {})
+ render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {})
+
+ # create menu entries
+ for vyos_ver in vyos_versions:
+ boot_opts = None
+ for entry in vyos_menuentries:
+ if entry.get('version') == vyos_ver and entry.get(
+ 'bootmode') == 'normal':
+ boot_opts = entry.get('boot_opts')
+ grub.version_add(vyos_ver, root_dir, boot_opts)
+
+ # update structure version
+ cfg_ver = compat.update_cfg_ver(root_dir)
+ grub.write_cfg_ver(cfg_ver, root_dir)
+
+ if compat.mode():
+ compat.render_grub_cfg(root_dir)
+ else:
+ render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {})
+
+ # sort inodes (to make GRUB read config files in alphabetical order)
+ grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS}')
+ grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}')
+
+ exit(0)
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
new file mode 100644
index 0000000..2473380
--- /dev/null
+++ b/src/system/keepalived-fifo.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-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/>.
+
+import os
+import time
+import signal
+import argparse
+import threading
+import re
+import logging
+
+from queue import Queue
+from logging.handlers import SysLogHandler
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import cmd
+from vyos.utils.dict import dict_search
+from vyos.utils.commit import commit_in_progress
+
+# configure logging
+logger = logging.getLogger(__name__)
+logs_format = logging.Formatter('%(filename)s: %(message)s')
+logs_handler_syslog = SysLogHandler('/dev/log')
+logs_handler_syslog.setFormatter(logs_format)
+logger.addHandler(logs_handler_syslog)
+logger.setLevel(logging.DEBUG)
+
+mdns_running_file = '/run/mdns_vrrp_active'
+mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns_repeater.py'
+
+# class for all operations
+class KeepalivedFifo:
+ # init - read command arguments
+ def __init__(self):
+ logger.info('Starting FIFO pipe for Keepalived')
+ # define program arguments
+ cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False)
+ cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe')
+ # parse arguments
+ cmd_args = cmd_args_parser.parse_args()
+
+ self._config_load()
+ self.pipe_path = cmd_args.PIPE
+
+ # create queue for messages and events for syncronization
+ self.message_queue = Queue(maxsize=100)
+ self.stopme = threading.Event()
+ self.message_event = threading.Event()
+
+ # load configuration
+ def _config_load(self):
+ # For VRRP configuration to be read, the commit must be finished
+ count = 1
+ while commit_in_progress():
+ if ( count <= 20 ):
+ logger.debug(f'Attempt to load keepalived configuration aborted due to a commit in progress (attempt {count}/20)')
+ else:
+ logger.error(f'Forced keepalived configuration loading despite a commit in progress ({count} wait time expired, not waiting further)')
+ break
+ count += 1
+ time.sleep(1)
+
+ try:
+ base = ['high-availability', 'vrrp']
+ conf = ConfigTreeQuery()
+ if not conf.exists(base):
+ raise ValueError()
+
+ # Read VRRP configuration directly from CLI
+ self.vrrp_config_dict = conf.get_config_dict(base,
+ key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')
+ except Exception as err:
+ logger.error(f'Unable to load configuration: {err}')
+
+ # run command
+ def _run_command(self, command):
+ logger.debug(f'Running the command: {command}')
+ try:
+ cmd(command)
+ except OSError as err:
+ logger.error(f'Unable to execute command "{command}": {err}')
+
+ # create FIFO pipe
+ def pipe_create(self):
+ if os.path.exists(self.pipe_path):
+ logger.info(f'PIPE already exist: {self.pipe_path}')
+ else:
+ os.mkfifo(self.pipe_path)
+
+ # process message from pipe
+ def pipe_process(self):
+ logger.debug('Message processing start')
+ regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE)
+ while self.stopme.is_set() is False:
+ # wait for a new message event from pipe_wait
+ self.message_event.wait()
+ try:
+ # clear mesage event flag
+ self.message_event.clear()
+ # get all messages from queue and try to process them
+ while self.message_queue.empty() is not True:
+ message = self.message_queue.get()
+ logger.debug(f'Received message: {message}')
+ notify_message = regex_notify.search(message)
+ # try to process a message if it looks valid
+ if notify_message:
+ n_type = notify_message.group('type')
+ n_name = notify_message.group('name')
+ n_state = notify_message.group('state')
+ logger.info(f'{n_type} {n_name} changed state to {n_state}')
+ # check and run commands for VRRP instances
+ if n_type == 'INSTANCE':
+ if os.path.exists(mdns_running_file):
+ cmd(mdns_update_command)
+
+ tmp = dict_search(f'group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
+ # check and run commands for VRRP sync groups
+ elif n_type == 'GROUP':
+ if os.path.exists(mdns_running_file):
+ cmd(mdns_update_command)
+
+ tmp = dict_search(f'sync_group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
+ # mark task in queue as done
+ self.message_queue.task_done()
+ except Exception as err:
+ logger.error(f'Error processing message: {err}')
+ logger.debug('Terminating messages processing thread')
+
+ # wait for messages
+ def pipe_wait(self):
+ logger.debug('Message reading start')
+ self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK)
+ while self.stopme.is_set() is False:
+ # sleep a bit to not produce 100% CPU load
+ time.sleep(0.250)
+ try:
+ # try to read a message from PIPE
+ message = os.read(self.pipe_read, 500)
+ if message:
+ # split PIPE content by lines and put them into queue
+ for line in message.decode().strip().splitlines():
+ self.message_queue.put(line)
+ # set new message flag to start processing
+ self.message_event.set()
+ except Exception as err:
+ # ignore the "Resource temporarily unavailable" error
+ if err.errno != 11:
+ logger.error(f'Error receiving message: {err}')
+
+ logger.debug('Closing FIFO pipe')
+ os.close(self.pipe_read)
+
+# handle SIGTERM signal to allow finish all messages processing
+def sigterm_handle(signum, frame):
+ logger.info('Ending processing: Received SIGTERM signal')
+ fifo.stopme.set()
+ thread_wait_message.join()
+ fifo.message_event.set()
+ thread_process_message.join()
+
+signal.signal(signal.SIGTERM, sigterm_handle)
+
+# init our class
+fifo = KeepalivedFifo()
+# try to create PIPE if it is not exist yet
+# It looks like keepalived do it before the script will be running, but if we
+# will decide to run this not from keepalived config, then we may get in
+# trouble. So it is betteer to leave this here.
+fifo.pipe_create()
+# create and run dedicated threads for reading and processing messages
+thread_wait_message = threading.Thread(target=fifo.pipe_wait)
+thread_process_message = threading.Thread(target=fifo.pipe_process)
+thread_wait_message.start()
+thread_process_message.start()
diff --git a/src/system/normalize-ip b/src/system/normalize-ip
new file mode 100644
index 0000000..08f922a
--- /dev/null
+++ b/src/system/normalize-ip
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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/>.
+#
+#
+
+# Normalizes IPv6 addresses so that they can be passed to iproute2,
+# since iproute2 will not take an address with leading zeroes for an argument
+
+import re
+import sys
+import ipaddress
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Argument required")
+ sys.exit(1)
+
+ address_string, prefix_length = re.match(r'(.+)/(.+)', sys.argv[1]).groups()
+
+ try:
+ address = ipaddress.IPv6Address(address_string)
+ normalized_address = address.compressed
+ except ipaddress.AddressValueError:
+ # It's likely an IPv4 address, do nothing
+ normalized_address = address_string
+
+ print("{0}/{1}".format(normalized_address, prefix_length))
+ sys.exit(0)
+
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
new file mode 100644
index 0000000..47c2762
--- /dev/null
+++ b/src/system/on-dhcp-event.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+#
+# Copyright (C) 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/>.
+#
+#
+
+if [ $# -lt 1 ]; then
+ echo Invalid args
+ logger -s -t on-dhcp-event "Invalid args \"$@\""
+ exit 1
+fi
+
+action=$1
+hostsd_client="/usr/bin/vyos-hostsd-client"
+
+get_subnet_domain_name () {
+ python3 <<EOF
+from vyos.kea import kea_get_active_config
+from vyos.utils.dict import dict_search_args
+
+config = kea_get_active_config('4')
+shared_networks = dict_search_args(config, 'arguments', f'Dhcp4', 'shared-networks')
+
+found = False
+
+if shared_networks:
+ for network in shared_networks:
+ for subnet in network[f'subnet4']:
+ if subnet['id'] == $1:
+ for option in subnet['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+ found = True
+
+ if not found:
+ for option in network['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+EOF
+}
+
+case "$action" in
+ lease4_renew|lease4_recover)
+ exit 0
+ ;;
+
+ lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address
+ client_ip=$LEASE4_ADDRESS
+ $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
+ exit 0
+ ;;
+
+ leases4_committed) # process committed leases (added/renewed/recovered)
+ for ((i = 0; i < $LEASES4_SIZE; i++)); do
+ client_ip_var="LEASES4_AT${i}_ADDRESS"
+ client_mac_var="LEASES4_AT${i}_HWADDR"
+ client_name_var="LEASES4_AT${i}_HOSTNAME"
+ client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID"
+
+ client_ip=${!client_ip_var}
+ client_mac=${!client_mac_var}
+ client_name=${!client_name_var%.}
+ client_subnet_id=${!client_subnet_id_var}
+
+ if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "host-$client_mac" | tr : -)
+ fi
+
+ client_domain=$(get_subnet_domain_name $client_subnet_id)
+
+ if [[ -n "$client_domain" ]] && ! [[ $client_name =~ .*$client_domain$ ]]; then
+ client_name="$client_name.$client_domain"
+ fi
+
+ $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ done
+
+ exit 0
+ ;;
+
+ *)
+ logger -s -t on-dhcp-event "Invalid command \"$1\""
+ exit 1
+ ;;
+esac
diff --git a/src/system/on-dhcpv6-event.sh b/src/system/on-dhcpv6-event.sh
new file mode 100644
index 0000000..cbb3709
--- /dev/null
+++ b/src/system/on-dhcpv6-event.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+#
+# Copyright (C) 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/>.
+#
+#
+
+if [ $# -lt 1 ]; then
+ echo Invalid args
+ logger -s -t on-dhcpv6-event "Invalid args \"$@\""
+ exit 1
+fi
+
+action=$1
+
+case "$action" in
+ lease6_renew|lease6_recover)
+ exit 0
+ ;;
+
+ lease6_release|lease6_expire|lease6_decline)
+ ifname=$QUERY6_IFACE_NAME
+ lease_addr=$LEASE6_ADDRESS
+ lease_prefix_len=$LEASE6_PREFIX_LEN
+
+ if [[ "$LEASE6_TYPE" != "IA_PD" ]]; then
+ exit 0
+ fi
+
+ logger -s -t on-dhcpv6-event "Processing route deletion for ${lease_addr}/${lease_prefix_len}"
+ route_cmd="sudo -n /sbin/ip -6 route del ${lease_addr}/${lease_prefix_len}"
+
+ # the ifname is not always present, like in LEASE6_VALID_LIFETIME=0 updates,
+ # but 'route del' works either way. Use interface only if there is one.
+ if [[ "$ifname" != "" ]]; then
+ route_cmd+=" dev ${ifname}"
+ fi
+ route_cmd+=" proto static"
+ eval "$route_cmd"
+
+ exit 0
+ ;;
+
+ leases6_committed)
+ for ((i = 0; i < $LEASES6_SIZE; i++)); do
+ ifname=$QUERY6_IFACE_NAME
+ requester_link_local=$QUERY6_REMOTE_ADDR
+ lease_type_var="LEASES6_AT${i}_TYPE"
+ lease_ip_var="LEASES6_AT${i}_ADDRESS"
+ lease_prefix_len_var="LEASES6_AT${i}_PREFIX_LEN"
+
+ lease_type=${!lease_type_var}
+
+ if [[ "$lease_type" != "IA_PD" ]]; then
+ continue
+ fi
+
+ lease_ip=${!lease_ip_var}
+ lease_prefix_len=${!lease_prefix_len_var}
+
+ logger -s -t on-dhcpv6-event "Processing PD route for ${lease_addr}/${lease_prefix_len}. Link local: ${requester_link_local} ifname: ${ifname}"
+
+ sudo -n /sbin/ip -6 route replace ${lease_ip}/${lease_prefix_len} \
+ via ${requester_link_local} \
+ dev ${ifname} \
+ proto static
+ done
+
+ exit 0
+ ;;
+
+ *)
+ logger -s -t on-dhcpv6-event "Invalid command \"$1\""
+ exit 1
+ ;;
+esac
diff --git a/src/system/post-upgrade b/src/system/post-upgrade
new file mode 100644
index 0000000..41b7c01
--- /dev/null
+++ b/src/system/post-upgrade
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+chown -R root:vyattacfg /config
diff --git a/src/system/standalone_root_pw_reset b/src/system/standalone_root_pw_reset
new file mode 100644
index 0000000..c82cea3
--- /dev/null
+++ b/src/system/standalone_root_pw_reset
@@ -0,0 +1,178 @@
+#!/bin/bash
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Bob Gilligan <gilligan@vyatta.com>
+# Description: Standalone script to set the admin passwd to new value
+# value. Note: This script can ONLY be run as a standalone
+# init program by grub.
+#
+# **** End License ****
+
+# The Vyatta config file:
+CF=/opt/vyatta/etc/config/config.boot
+
+# Admin user name
+ADMIN=vyos
+
+set_encrypted_password() {
+ sed -i \
+ -e "/ user $1 {/,/encrypted-password/s/encrypted-password .*\$/encrypted-password \"$2\"/" $3
+}
+
+
+# How long to wait for user to respond, in seconds
+TIME_TO_WAIT=30
+
+change_password() {
+ local user=$1
+ local pwd1="1"
+ local pwd2="2"
+
+ until [ "$pwd1" == "$pwd2" ]
+ do
+ read -p "Enter $user password: " -r -s pwd1
+ echo
+ read -p "Retype $user password: " -r -s pwd2
+ echo
+
+ if [ "$pwd1" != "$pwd2" ]
+ then echo "Passwords do not match"
+ fi
+ done
+
+ # set the password for the user then store it in the config
+ # so the user is recreated on the next full system boot.
+ local epwd=$(mkpasswd --method=sha-512 "$pwd1")
+ # escape any slashes in resulting password
+ local eepwd=$(sed 's:/:\\/:g' <<< $epwd)
+ set_encrypted_password $user $eepwd $CF
+}
+
+# System is so messed up that doing anything would be a mistake
+dead() {
+ echo $*
+ echo
+ echo "This tool can only recover missing admininistrator password."
+ echo "It is not a full system restore"
+ echo
+ echo -n "Hit return to reboot system: "
+ read
+ /sbin/reboot -f
+}
+
+echo "Standalone root password recovery tool."
+echo
+#
+# Check to see if we are running in standalone mode. We'll
+# know that we are if our pid is 1.
+#
+if [ "$$" != "1" ]; then
+ echo "This tool can only be run in standalone mode."
+ exit 1
+fi
+
+#
+# OK, now we know we are running in standalone mode. Talk to the
+# user.
+#
+echo -n "Do you wish to reset the admin password? (y or n) "
+read -t $TIME_TO_WAIT response
+if [ "$?" != "0" ]; then
+ echo
+ echo "Response not received in time."
+ echo "The admin password will not be reset."
+ echo "Rebooting in 5 seconds..."
+ sleep 5
+ echo
+ /sbin/reboot -f
+fi
+
+response=${response:0:1}
+if [ "$response" != "y" -a "$response" != "Y" ]; then
+ echo "OK, the admin password will not be reset."
+ echo -n "Rebooting in 5 seconds..."
+ sleep 5
+ echo
+ /sbin/reboot -f
+fi
+
+echo -en "Which admin account do you want to reset? [$ADMIN] "
+read admin_user
+ADMIN=${admin_user:-$ADMIN}
+
+echo "Starting process to reset the admin password..."
+
+echo "Re-mounting root filesystem read/write..."
+mount -o remount,rw /
+
+if [ ! -f /etc/passwd ]
+then dead "Missing password file"
+fi
+
+if [ ! -d /opt/vyatta/etc/config ]
+then dead "Missing VyOS config directory /opt/vyatta/etc/config"
+fi
+
+# Leftover from V3.0
+if grep -q /opt/vyatta/etc/config /etc/fstab
+then
+ echo "Mounting the config filesystem..."
+ mount /opt/vyatta/etc/config/
+fi
+
+if [ ! -f $CF ]
+then dead "$CF file not found"
+fi
+
+if ! grep -q 'system {' $CF
+then dead "$CF file does not contain system settings"
+fi
+
+if ! grep -q ' login {' $CF
+then
+ # Recreate login section of system
+ sed -i -e '/system {/a\
+ login {\
+ }' $CF
+fi
+
+if ! grep -q " user $ADMIN " $CF
+then
+ echo "Recreating administrator $ADMIN in $CF..."
+ sed -i -e "/ login {/a\\
+ user $ADMIN {\\
+ authentication {\\
+ encrypted-password \$6$IhbXHdwgYkLnt/$VRIsIN5c2f2v4L2l4F9WPDrRDEtWXzH75yBswmWGERAdX7oBxmq6m.sWON6pO6mi6mrVgYBxdVrFcCP5bI.nt.\\
+ plaintext-password \"\"\\
+ }\\
+ level admin\\
+ }" $CF
+fi
+
+echo "Saving backup copy of config.boot..."
+cp $CF ${CF}.before_pwrecovery
+sync
+
+echo "Setting the administrator ($ADMIN) password..."
+change_password $ADMIN
+
+echo $(date "+%b%e %T") $(hostname) "Admin password changed" \
+ | tee -a /var/log/auth.log >>/var/log/messages
+
+sync
+
+echo "System will reboot in 10 seconds..."
+sleep 10
+/sbin/reboot -f
diff --git a/src/system/uacctd_stop.py b/src/system/uacctd_stop.py
new file mode 100644
index 0000000..a1b5733
--- /dev/null
+++ b/src/system/uacctd_stop.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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/>.
+
+# Control pmacct daemons in a tricky way.
+# Pmacct has signal processing in a main loop, together with packet
+# processing. Because of this, while it is waiting for packets, it cannot
+# handle the control signal. We need to start the systemctl command and then
+# send some packets to pmacct to wake it up
+
+from argparse import ArgumentParser
+from socket import socket, AF_INET, SOCK_DGRAM
+from sys import exit
+from time import sleep
+
+from psutil import Process
+
+
+def stop_process(pid: int, timeout: int) -> None:
+ """Send a signal to uacctd
+ and then send packets to special address predefined in a firewall
+ to unlock main loop in uacctd and finish the process properly
+
+ Args:
+ pid (int): uacctd PID
+ timeout (int): seconds to wait for a process end
+ """
+ # find a process
+ uacctd = Process(pid)
+ uacctd.terminate()
+
+ # create a socket
+ trigger = socket(AF_INET, SOCK_DGRAM)
+
+ first_cycle: bool = True
+ while uacctd.is_running() and timeout:
+ print('sending a packet to uacctd...')
+ trigger.sendto(b'WAKEUP', ('127.0.254.0', 1))
+ # do not sleep during first attempt
+ if not first_cycle:
+ sleep(1)
+ timeout -= 1
+ first_cycle = False
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument('process_id',
+ type=int,
+ help='PID file of uacctd core process')
+ parser.add_argument('timeout',
+ type=int,
+ help='time to wait for process end')
+ args = parser.parse_args()
+ stop_process(args.process_id, args.timeout)
+ exit()
diff --git a/src/system/vyos-config-cloud-init.py b/src/system/vyos-config-cloud-init.py
new file mode 100644
index 0000000..0a6c1f9
--- /dev/null
+++ b/src/system/vyos-config-cloud-init.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+from concurrent.futures import ProcessPoolExecutor
+from pathlib import Path
+from subprocess import run, TimeoutExpired
+from sys import exit
+
+from psutil import net_if_addrs, AF_LINK
+from systemd.journal import JournalHandler
+from yaml import safe_load
+
+from vyos.template import render
+
+# define a path to the configuration file and template
+config_file = '/etc/cloud/cloud.cfg.d/20_vyos_network.cfg'
+template_file = 'system/cloud_init_networking.j2'
+
+
+def check_interface_dhcp(iface_name: str) -> bool:
+ """Check DHCP client can work on an interface
+
+ Args:
+ iface_name (str): interface name
+
+ Returns:
+ bool: check result
+ """
+ dhclient_command: list[str] = [
+ 'dhclient', '-4', '-1', '-q', '--no-pid', '-sf', '/bin/true', iface_name
+ ]
+ check_result = False
+ # try to get an IP address
+ # we use dhclient behavior here to speedup detection
+ # if dhclient receives a configuration and configure an interface
+ # it switch to background
+ # If no - it will keep running in foreground
+ try:
+ run(['ip', 'l', 'set', iface_name, 'up'])
+ run(dhclient_command, timeout=5)
+ check_result = True
+ except TimeoutExpired:
+ pass
+ finally:
+ run(['ip', 'l', 'set', iface_name, 'down'])
+
+ logger.info(f'DHCP server was found on {iface_name}: {check_result}')
+ return check_result
+
+
+def dhclient_cleanup() -> None:
+ """Clean up after dhclients
+ """
+ run(['killall', 'dhclient'])
+ leases_file: Path = Path('/var/lib/dhcp/dhclient.leases')
+ leases_file.unlink(missing_ok=True)
+ logger.debug('cleaned up after dhclients')
+
+
+def dict_interfaces() -> dict[str, str]:
+ """Return list of available network interfaces except loopback
+
+ Returns:
+ list[str]: a list of interfaces
+ """
+ interfaces_dict: dict[str, str] = {}
+ ifaces = net_if_addrs()
+ for iface_name, iface_addresses in ifaces.items():
+ # we do not need loopback interface
+ if iface_name == 'lo':
+ continue
+ # check other interfaces for MAC addresses
+ for iface_addr in iface_addresses:
+ if iface_addr.family == AF_LINK and iface_addr.address:
+ interfaces_dict[iface_name] = iface_addr.address
+ continue
+
+ logger.debug(f'found interfaces: {interfaces_dict}')
+ return interfaces_dict
+
+
+def need_to_check() -> bool:
+ """Check if we need to perform DHCP checks
+
+ Returns:
+ bool: check result
+ """
+ # if cloud-init config does not exist, we do not need to do anything
+ ci_config_vyos = Path('/etc/cloud/cloud.cfg.d/20_vyos_custom.cfg')
+ if not ci_config_vyos.exists():
+ logger.info(
+ 'No need to check interfaces: Cloud-init config file was not found')
+ return False
+
+ # load configuration file
+ try:
+ config = safe_load(ci_config_vyos.read_text())
+ except:
+ logger.error('Cloud-init config file has a wrong format')
+ return False
+
+ # check if we have in config configured option
+ # vyos_config_options:
+ # network_preconfigure: true
+ if not config.get('vyos_config_options', {}).get('network_preconfigure'):
+ logger.info(
+ 'No need to check interfaces: Cloud-init config option "network_preconfigure" is not set'
+ )
+ return False
+
+ return True
+
+
+if __name__ == '__main__':
+ # prepare logger
+ logger = logging.getLogger(__name__)
+ logger.addHandler(JournalHandler(SYSLOG_IDENTIFIER=Path(__file__).name))
+ logger.setLevel(logging.INFO)
+
+ # we need to give udev some time to rename all interfaces
+ # this is placed before need_to_check() call, because we are not always
+ # need to preconfigure cloud-init, but udev always need to finish its work
+ # before cloud-init start
+ run(['udevadm', 'settle'])
+ logger.info('udev finished its work, we continue')
+
+ # do not perform any checks if this is not required
+ if not need_to_check():
+ exit()
+
+ # get list of interfaces and check them
+ interfaces_dhcp: list[dict[str, str]] = []
+ interfaces_dict: dict[str, str] = dict_interfaces()
+
+ with ProcessPoolExecutor(max_workers=len(interfaces_dict)) as executor:
+ iface_check_results = [{
+ 'dhcp': executor.submit(check_interface_dhcp, iface_name),
+ 'append': {
+ 'name': iface_name,
+ 'mac': iface_mac
+ }
+ } for iface_name, iface_mac in interfaces_dict.items()]
+
+ dhclient_cleanup()
+
+ for iface_check_result in iface_check_results:
+ if iface_check_result.get('dhcp').result():
+ interfaces_dhcp.append(iface_check_result.get('append'))
+
+ # render cloud-init config
+ if interfaces_dhcp:
+ logger.debug('rendering cloud-init network configuration')
+ render(config_file, template_file, {'ifaces_list': interfaces_dhcp})
+
+ exit()
diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py
new file mode 100644
index 0000000..dd27930
--- /dev/null
+++ b/src/system/vyos-event-handler.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import json
+import re
+import select
+
+from copy import deepcopy
+from os import getpid, environ
+from pathlib import Path
+from signal import signal, SIGTERM, SIGINT
+from sys import exit
+from systemd import journal
+
+from vyos.utils.dict import dict_search
+from vyos.utils.process import run
+
+# Identify this script
+my_pid = getpid()
+my_name = Path(__file__).stem
+
+# handle termination signal
+def handle_signal(signal_type, frame):
+ if signal_type == SIGTERM:
+ journal.send('Received SIGTERM signal, stopping normally',
+ SYSLOG_IDENTIFIER=my_name)
+ if signal_type == SIGINT:
+ journal.send('Received SIGINT signal, stopping normally',
+ SYSLOG_IDENTIFIER=my_name)
+ exit(0)
+
+
+# Class for analyzing and process messages
+class Analyzer:
+ # Initialize settings
+ def __init__(self, config: dict) -> None:
+ self.config = {}
+ # Prepare compiled regex objects
+ for event_id, event_config in config.items():
+ script = dict_search('script.path', event_config)
+ # Check for arguments
+ if dict_search('script.arguments', event_config):
+ script_arguments = dict_search('script.arguments', event_config)
+ script = f'{script} {script_arguments}'
+ # Prepare environment
+ environment = deepcopy(environ)
+ # Check for additional environment options
+ if dict_search('script.environment', event_config):
+ for env_variable, env_value in dict_search(
+ 'script.environment', event_config).items():
+ environment[env_variable] = env_value.get('value')
+ # Create final config dictionary
+ pattern_raw = event_config['filter']['pattern']
+ pattern_compiled = re.compile(
+ rf'{event_config["filter"]["pattern"]}')
+ pattern_config = {
+ pattern_compiled: {
+ 'pattern_raw':
+ pattern_raw,
+ 'syslog_id':
+ dict_search('filter.syslog-identifier', event_config),
+ 'pattern_script': {
+ 'path': script,
+ 'environment': environment
+ }
+ }
+ }
+ self.config.update(pattern_config)
+
+ # Execute script safely
+ def script_run(self, pattern: str, script_path: str,
+ script_env: dict) -> None:
+ try:
+ run(script_path, env=script_env)
+ journal.send(
+ f'Pattern found: "{pattern}", script executed: "{script_path}"',
+ SYSLOG_IDENTIFIER=my_name)
+ except Exception as err:
+ journal.send(
+ f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}',
+ SYSLOG_IDENTIFIER=my_name)
+
+ # Analyze a message
+ def process_message(self, message: dict) -> None:
+ for pattern_compiled, pattern_config in self.config.items():
+ # Check if syslog id is presented in config and matches
+ syslog_id = pattern_config.get('syslog_id')
+ if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id:
+ continue
+ if pattern_compiled.fullmatch(message['MESSAGE']):
+ # Add message to environment variables
+ pattern_config['pattern_script']['environment'][
+ 'message'] = message['MESSAGE']
+ # Run script
+ self.script_run(
+ pattern=pattern_config['pattern_raw'],
+ script_path=pattern_config['pattern_script']['path'],
+ script_env=pattern_config['pattern_script']['environment'])
+
+
+if __name__ == '__main__':
+ # Parse command arguments and get config
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to even-handler configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config_path = Path(args.config)
+ config = json.loads(config_path.read_text())
+ # Create an object for analazyng messages
+ analyzer = Analyzer(config)
+ except Exception as err:
+ print(
+ f'Configuration file "{config_path}" does not exist or malformed: {err}'
+ )
+ exit(1)
+
+ # Prepare for proper exitting
+ signal(SIGTERM, handle_signal)
+ signal(SIGINT, handle_signal)
+
+ # Set up journal connection
+ data = journal.Reader()
+ data.seek_tail()
+ data.get_previous()
+ p = select.poll()
+ p.register(data, data.get_events())
+
+ journal.send(f'Started with configuration: {config}',
+ SYSLOG_IDENTIFIER=my_name)
+
+ while p.poll():
+ if data.process() != journal.APPEND:
+ continue
+ for entry in data:
+ message = entry['MESSAGE']
+ pid = -1
+ try:
+ pid = entry['_PID']
+ except Exception as ex:
+ journal.send(f'Unable to extract PID from message entry: {entry}', SYSLOG_IDENTIFIER=my_name)
+ continue
+ # Skip empty messages and messages from this process
+ if message and pid != my_pid:
+ try:
+ analyzer.process_message(entry)
+ except Exception as err:
+ journal.send(f'Unable to process message: {err}',
+ SYSLOG_IDENTIFIER=my_name)
diff --git a/src/system/vyos-system-update-check.py b/src/system/vyos-system-update-check.py
new file mode 100644
index 0000000..c874f1e
--- /dev/null
+++ b/src/system/vyos-system-update-check.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import json
+import jmespath
+
+from pathlib import Path
+from sys import exit
+from time import sleep
+
+from vyos.utils.process import call
+
+import vyos.version
+
+motd_file = Path('/run/motd.d/10-vyos-update')
+
+
+if __name__ == '__main__':
+ # Parse command arguments and get config
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to system-update-check configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config_path = Path(args.config)
+ config = json.loads(config_path.read_text())
+ except Exception as err:
+ print(
+ f'Configuration file "{config_path}" does not exist or malformed: {err}'
+ )
+ exit(1)
+
+ url_json = config.get('url')
+ local_data = vyos.version.get_full_version_data()
+ local_version = local_data.get('version')
+
+ while True:
+ remote_data = vyos.version.get_remote_version(url_json)
+ if remote_data:
+ url = jmespath.search('[0].url', remote_data)
+ remote_version = jmespath.search('[0].version', remote_data)
+ if local_version != remote_version and remote_version:
+ call(f'wall -n "Update available: {remote_version} \nUpdate URL: {url}"')
+ # MOTD used in /run/motd.d/10-update
+ motd_file.parent.mkdir(exist_ok=True)
+ motd_file.write_text(f'---\n'
+ f'Current version: {local_version}\n'
+ f'Update available: \033[1;34m{remote_version}\033[0m\n'
+ f'---\n')
+ # Check every 12 hours
+ sleep(43200)