Introduction
When developing for linux, there are tasks of creating interactive scripts that are executed when the system is turned on or shut down. In system V, this was done easily, but with systemd it makes adjustments. But it knows its timers.
Why do we need target
It is often written that target serves as an analogue of runlevel in system V -init. I fundamentally disagree. There are more of them and you can separate packages into groups and, for example, run a group of services with one team and perform additional actions. In addition, they have no hierarchy, only dependencies.
Target example at startup (feature overview) with launching an interactive script
Description of 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.service
This target will start when multi-user.target is launched and calls installer.service. There may be several such services.
cat installer.service [Unit] # Description=installer interactive dialog [Service] # , Type=idle # - ExecStart=/usr/bin/installer.sh # tty3 StandardInput=tty TTYPath=/dev/tty3 TTYReset=yes TTYVHangup=yes [Install] WantedBy=installer.target
And finally, an example of an executable script:
#!/bin/bash # tty3 chvt 3 echo "Install, y/n ?" read user_answer
The most important thing is to choose final.target - target, which the system should come to at startup. In the process of starting, systemd will go through the dependencies and run everything you need.
There are several ways to select final.target, I used the bootloader option for this.
The final launch looks like this:
- The bootloader starts
- The bootloader starts the firmware by passing the final.target parameter
- Systemd starts system startup. It goes sequentially to installer.target or work.target from basic.target through their dependencies (for example, multi-user.target). The latter and lead the system to work in the desired mode
Preparing the firmware for launch
When creating firmware, there is always the task of restoring the state of the system at startup and saving it when turned off. The state means configuration files, database dumps, interface settings, etc.
Systemd launches the process in one target in parallel. There are dependencies that allow you to determine the sequence of script execution.
How it works in my project ( https://habr.com/en/post/477008/ https://github.com/skif-web/monitor )
- System starts
- The settings_restore.service service starts. It checks the settings.txt file in the data section. If it is not there, then the reference file is put in its place. Next, the system settings are restored:
- admin password
- hostname
- time zone
- locale
- Determines if all media is in use. By default, the image size is small - for the convenience of copying and recording to media. At startup, it is checked whether there is still unused space. If there is, the disk is repartitioned.
- Generate machine-id from MAC address. This is important for getting the same address via DHCP.
- Network settings
- Log size is limited
- The external drive is prepared for work (if the corresponding option is enabled and the drive is new)
- Run postgresq
- the restore service starts. It is needed to prepare zabbix itself and its database:
- Checks if there is already a zabbix database. If not, it is created from initialization dumps (they are supplied with zabbix)
- a list of time zones is created (needed to display them in the web interface)
- The current IP is found, it is displayed in issue (invitation to enter the console)
- The invitation changes - the phrase Ready to work appears
- The firmware is ready to go.
Service files are important, it is they who set the sequence for their launch
[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.target
As you can see, I made the dependencies so that I would first run my script, and only then the network would rise and the DBMS would start.
And the second service (preparing 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.target
This is a bit more complicated. The launch is also in multi-user.target, but AFTER running the postgresql DBMS and my setting_restore. But BEFORE running zabbix services.
Service with a timer for logrotate
Systemd can replace CRON. Seriously. Moreover, the accuracy is not up to a minute, but up to a second (what if it is needed). And you can create a monotonous timer, called by timeout from the event.
It was the monotonous timer that counts the time from the start of the machine that I created.
This will require 2 files
logrotateTimer.service - the actual description of the service:
[Unit] Description=run logrotate [Service] ExecStart=logrotate /etc/logrotate.conf TimeoutSec=300
It's simple - a description of the launch command.
The second logrotateTimer.timer file is what sets the timers to work:
[Unit] Description=Run logrotate [Timer] OnBootSec=15min OnUnitActiveSec=15min [Install] WantedBy=timers.target
What is there:
- timer description
- First time starting from system boot
- period of further launches
- Timer Service Dependency. Actually, this is a string and makes a timer
Interactive script on shutdown and custom shutdown target
In another development, I had to make a more complex version of turning off the machine - through my own target, in order to perform many actions. It is usually recommended to create the oneshot service with the RemainAfterExit option, but this prevents the creation of an interactive script.
But the fact is that the commands launched by the ExecOnStop option are executed outside of TTY! The check is simple - insert the tty command and save its output.
Therefore, I implemented shutdown through my target. I do not pretend to be 100% correct, but it works!
How it was done (in general terms):
Created a target my_shutdown.target, which did not depend on anyone:
my_shutdown.target
[Unit] Description=my shutdown AllowIsolate=yes Wants=my_shutdown.service
When switching to this target (via systemctl isolate my_shutdwn.target), he launched the my_shutdown.service service, whose task is simple - to 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 perform the necessary actions. You can add a lot of 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/ $command
Note. Using the files / tmp / reboot and / tmp / shutdown. You cannot call target with parameters. You can only service.
But I use target in order to have flexibility in work and a guaranteed order of actions.
However, the most interesting was later. The machine must be turned off / restarted. And there are 2 options:
- Replace the reboot, shutdown, and other commands (they are still symlinks on systemctl) with your own script. Inside the script, go to my_shutdown.target. And the scripts inside the target then directly call systemctl, for example, systemctl reboot
- A simpler, but I do not like the option. In all interfaces, do not call shutdown / reboot / others, but directly call the systemctl isolate my_shutdown.target target
I chose the first option. In systemd, reboot (like poweroff) are symlinks on systemd.
ls -l /sbin/poweroff lrwxrwxrwx 1 root root 14 30 18:23 /sbin/poweroff -> /bin/systemctl
Therefore, they can be replaced with your own scripts:
reboot
#!/bin/sh touch /tmp/reboot sudo systemctl isolate my_shutdown.target fi