
Introduction
When developing for Linux, there are tasks related to creating interactive scripts that run on system startup or shutdown. In System V, this was easy to achieve, but with systemd, adjustments are needed. However, it has its own timers.
What are targets for
It is often stated that targets serve as an analogue of runlevels in System V init. I fundamentally disagree. There are more of them, and packages can be categorized into groups, allowing you to start a group of services with a single command and perform additional actions. Moreover, they lack hierarchy, only dependencies exist.
Example of a target on startup (overview of capabilities) with the launch of an interactive script
Description of the target itself:
cat installer.target
[Unit]
Description=My installer
Requires=multi-user.target
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target
AllowIsolate=yes
Wants=installer.serviceThis target will start when multi-user.target is activated, which will invoke installer.service. There can be multiple such services.
cat installer.service
[Unit]
# description
Description=installer interactive dialog
[Service]
# Run once, when the rest is started
Type=idle
# Command to execute - call the script
ExecStart=/usr/bin/installer.sh
# Interactive interaction with the user through tty3
StandardInput=tty
TTYPath=/dev/tty3
TTYReset=yes
TTYVHangup=yes
[Install]
WantedBy=installer.targetAnd finally, an example of the executed script:
#!/bin/bash
# Переходим в tty3
chvt 3
echo "Install, y/n ?"
read user_answerThe most important thing is to choose final.target — the target that the system should reach upon booting. During the boot process, systemd will follow the dependencies and start everything necessary.
You can choose final.target in various ways; I used the boot loader option for this.
The final startup looks like this:
- The boot loader starts up
- The boot loader begins loading the firmware, passing the final.target parameter
- Systemd starts the system. It sequentially progresses to installer.target or work.target from basic.target through their dependencies (e.g., multi-user.target). The latter leads the system to operate in the desired mode.
Preparing the firmware for launch
When creating firmware, there is always the task of restoring the system state at startup and preserving it upon shutdown. The state refers to configuration files, database dumps, interface settings, etc.
Systemd starts processes within a single target in parallel. There are dependencies that allow determining the order of script execution.
How this works in my project ( )
- The system starts
- The settings_restore.service starts. It checks for the existence of the settings.txt file in the data section. If it is not there, a reference file is placed instead. The system settings are then restored:
- administrator password
- hostname,
- time zone
- locale
- Determining whether the entire media is used. By default, the image size is small — for ease of copying and writing to the media. Upon starting, it checks if there is any unused space. If there is, the disk is repartitioned.
- Generating machine-id from the MAC address. This is important for receiving the same address via DHCP
- Network settings
- Limiting log size
- Preparing the external disk (if the corresponding option is enabled and the disk is new)
- Starting postgresq
- The restore service is starting. It is necessary for preparing zabbix and its database:
- It checks if the zabbix database already exists. If not, it is created from the initializing dumps (included with zabbix)
- A list of time zones is created (needed for displaying them in the web interface)
- The current IP is found and displayed in the issue (invitation to log in to the console)
- The prompt changes — the phrase Ready to work appears
- The firmware is ready for operation
Service files are important; they define the order of their startup
[Unit]
Description=restore system settings
Before=network.service prepare.service postgresql.service systemd-networkd.service systemd-resolved.service
[Service]
Type=oneshot
ExecStart=\/usr\/bin\/settings_restore.sh
[Install]
WantedBy=multi-user.targetAs you can see, I set dependencies so that my script runs first, and only then does the network come up and the DBMS starts.
And the second service (preparation of zabbix)
#!/bin/sh
[Unit]
Description=monitor prepare system
After=postgresql.service settings_restore.service
Before=zabbix-server.service zabbix-agent.service
[Service]
Type=oneshot
ExecStart=/usr/bin/prepare.sh
[Install]
WantedBy=multi-user.targetThis one is a bit more complicated. It also starts in multi-user.target, but AFTER the startup of the postgresql DBMS and my setting_restore. But BEFORE the services of zabbix start.
A service with a timer for logrotate
Systemd can replace CRON. Seriously. Moreover, the precision is not to the minute, but to the second (just in case it’s needed). A monotonic timer can also be created that is called based on a timeout from an event.
The monotonic timer, counting time from machine startup, is what I created.
For this, 2 files will be required
logrotateTimer.service — the actual service description:
[Unit]
Description=run logrotate
[Service]
ExecStart=logrotate \/etc\/logrotate.conf
TimeoutSec=300It's straightforward — the command description.
The second file logrotateTimer.timer — this one indeed sets the timer's operation:
[Unit]
Description=Run logrotate
[Timer]
OnBootSec=15min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.targetWhat is included here:
- Timer description
- Time of the first start, starting from system load
- Subsequent start period
- Dependency on timer services. Essentially, this line makes the timer work
Interactive script during shutdown and its shutdown target
In another development, I had to create a more complex machine shutdown method — through a custom target, to execute multiple actions. It is generally recommended to create an oneshot service with the RemainAfterExit option, but this doesn't allow for an interactive script.
The issue is that the commands executed by the ExecOnStop option run outside of TTY! It's easy to check — insert the tty command and save its output.
So, I implemented the shutdown via my custom target. I don't claim 100% accuracy, but it works!
How it was done (in general terms):
I created the my_shutdown.target that had no dependencies:
my_shutdown.target
[Unit]
Description=my shutdown
AllowIsolate=yes
Wants=my_shutdown.service When transitioning to this target (via systemctl isolate my_shutdown.target), it triggered the my_shutdown.service, whose task is simple — execute the my_shutdown.sh script:
[Unit]
Description=MY shutdown
[Service]
Type=oneshot
ExecStart=/usr/bin/my_shutdown.sh
StandardInput=tty
TTYPath=/dev/tty3
TTYReset=yes
TTYVHangup=yes
WantedBy=my_shutdown.target- Inside this script, I execute the necessary actions. You can add many scripts to the target for flexibility and convenience:
my_shutdown.sh
#!/bin/bash --login
if [ -f /tmp/reboot ];then
command="systemctl reboot"
elif [ -f /tmp/shutdown ]; then
command="systemctl poweroff"
fi
#Вот здесь нужные команды
#Например, cp /home/user/data.txt /storage/user/
$commandNote. Using the files /tmp/reboot and /tmp/shutdown. You cannot invoke the target with parameters. You can only use service.
But I use the target to have flexibility in operations and guaranteed execution order.
However, the most interesting part came later. The machine needs to be shut down/rebooted. And there are 2 options:
- Replace the reboot, shutdown, and other commands (which are still symlinks to systemctl) with your script. Inside the script — transition to my_shutdown.target. And the scripts inside the target then directly invoke systemctl, for example, systemctl reboot.
- A simpler option, but one I don't like. In all interfaces, call not shutdown/reboot/others but directly invoke the target systemctl isolate my_shutdown.target.
I chose the first option. In systemd, reboot (like poweroff) are symlinks to systemd.
ls -l /sbin/poweroff
lrwxrwxrwx 1 root root 14 Sep 30 18:23 /sbin/poweroff -> /bin/systemctlTherefore, they can be replaced with your scripts:
reboot
#!/bin/sh
touch /tmp/reboot
sudo systemctl isolate my_shutdown.target
fiSource: habr.com
