
Einführung
Bei der Entwicklung unter Linux gibt es die Aufgabe, interaktive Skripte zu erstellen, die beim Starten oder Herunterfahren des Systems ausgeführt werden. In System V war das einfach, aber mit systemd gibt es Änderungen. Dafür kann es eigene Timer erstellen.
Wozu sind Targets notwendig
Es wird oft gesagt, dass Targets das Äquivalent zu Runlevels in System V -init sind. Dem widerspreche ich entschieden. Es gibt sie in größerer Zahl, und man kann Pakete in Gruppen aufteilen und beispielsweise eine Gruppe von Diensten mit einem Befehl starten und zusätzliche Aktionen durchführen. Außerdem haben sie keine Hierarchie, nur Abhängigkeiten.
Beispiel eines Targets beim Einschalten (Überblick über die Möglichkeiten) mit dem Start eines interaktiven Skripts
Beschreibung des Targets selbst:
cat installer.target
[Unit]
Description=Mein Installer
Requires=multi-user.target
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target
AllowIsolate=yes
Wants=installer.serviceDieses Target wird gestartet, wenn multi-user.target aufgerufen wird und installer.service anstoßt. Dabei kann es mehrere solcher Dienste geben.
cat installer.service
[Unit]
# Beschreibung
Description=Installer interaktiver Dialog
[Service]
# Einmal starten, wenn alles andere gestartet wird
Type=idle
# Startbefehl - Skriptaufruf
ExecStart=\/usr\/bin\/installer.sh
# Interaktive Interaktion mit dem Benutzer über tty3
StandardInput=tty
TTYPath=\/dev\/tty3
TTYReset=yes
TTYVHangup=yes
[Install]
WantedBy=installer.targetUnd schließlich ein Beispiel des ausgeführten Skripts:
#!/bin/bash
# Переходим в tty3
chvt 3
echo "Install, y/n ?"
read user_answerDas Wichtigste ist, final.target auszuwählen - das Target, auf das das System beim Starten gelangen soll. Während des Startvorgangs wird systemd durch die Abhängigkeiten gehen und alles Notwendige starten.
Das final.target kann auf verschiedene Arten ausgewählt werden, ich habe dafür die Loader-Option verwendet.
Der endgültige Start sieht so aus:
- Der Bootloader startet
- Der Bootloader beginnt den Firmwarestart, indem er den Parameter final.target übergibt
- Systemd beginnt mit dem Start des Systems. Es geht nacheinander zu installer.target oder work.target von basic.target über deren Abhängigkeiten (zum Beispiel multi-user.target). Letztere bringen das System in den gewünschten Betriebsmodus.
Vorbereitung der Firmware für den Start
Bei der Erstellung von Firmwares entsteht immer die Aufgabe, den Zustand des Systems beim Start wiederherzustellen und beim Herunterfahren zu speichern. Unter Zustand versteht man Konfigurationsdateien, Datenbankdumps, Schnittstelleneinstellungen usw.
Systemd startet Prozesse in einem Target parallel. Es gibt Abhängigkeiten, die die Reihenfolge des Starts der Skripte bestimmen.
So funktioniert es in meinem Projekt ( )
- Das System startet
- Der Dienst settings_restore.service wird gestartet. Er überprüft das Vorhandensein der Datei settings.txt im Datenteil. Falls diese nicht vorhanden ist, wird die Standarddatei an deren Stelle platziert. Danach erfolgt die Wiederherstellung der Systemeinstellungen:
- Administratorpasswort
- Hostname,
- Zeitzone
- Locale
- Es wird festgestellt, ob das gesamte Medium verwendet wird. Standardmäßig ist die Größe des Abbilds klein – um das Kopieren und Schreiben auf das Medium zu erleichtern. Beim Start wird geprüft, ob noch ungenutzter Speicher vorhanden ist. Wenn ja, wird die Festplatte neu partitioniert.
- Generierung der machine-id aus der MAC-Adresse. Dies ist wichtig, um die gleiche Adresse über DHCP zu erhalten.
- Netzwerkeinstellungen
- Die Größe der Protokolle wird begrenzt
- Es wird ein externes Laufwerk vorbereitet (wenn die entsprechende Option aktiviert ist und das Laufwerk neu ist)
- PostgreSQL wird gestartet
- Der Dienst restore wird gestartet. Er ist erforderlich, um Zabbix und dessen Datenbank vorzubereiten:
- Es wird überprüft, ob bereits eine Datenbank für Zabbix vorhanden ist. Wenn nicht, wird sie aus den initialen Dumps erstellt (die in der Zabbix-Lieferung enthalten sind)
- Eine Liste der Zeitzonen wird erstellt (benötigt für die Anzeige im Web-Interface)
- Die aktuelle IP wird ermittelt und im Issue (Einladung zum Anmelden in der Konsole) angezeigt.
- Die Eingabeaufforderung ändert sich – der Satz 'Bereit zum Arbeiten' erscheint.
- Die Firmware ist betriebsbereit.
Die Dateien der Dienste sind wichtig, sie bestimmen die Reihenfolge ihres Starts.
[Unit]
Description=Systemeinstellungen wiederherstellen
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.targetWie zu sehen, habe ich Abhängigkeiten festgelegt, damit zuerst mein Skript ausgeführt wird und danach das Netzwerk gestartet und die DBMS in Betrieb genommen wird.
Und der zweite Dienst (Vorbereitung 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.targetHier ist es etwas komplizierter. Der Start erfolgt ebenfalls im multi-user.target, jedoch NACH dem Start der PostgreSQL-Datenbank und meinem settings_restore. ABER VOR dem Start der Zabbix-Dienste.
Der Dienst mit einem Timer für logrotate
Systemd kann CRON ersetzen. Ernsthaft. Und die Genauigkeit ist nicht auf Minuten, sondern auf Sekunden (falls benötigt). Man kann einen monotonen Timer erstellen, der basierend auf einem Timeoutereignis aufgerufen wird.
Genau diesen monotonen Timer, der die Zeit seit dem Start des Rechners zählt, habe ich erstellt.
Dafür werden 2 Dateien benötigt.
logrotateTimer.service – beschreibt den Dienst:
[Unit]
Description=logrotate ausführen
[Service]
ExecStart=logrotate \/etc\/logrotate.conf
TimeoutSec=300Alles einfach – Beschreibung des Startbefehls.
Die zweite Datei logrotateTimer.timer – sie legt die Timeraktivierung fest:
[Unit]
Description=logrotate ausführen
[Timer]
OnBootSec=15min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.targetWas gibt es hier:
- Beschreibung des Timers
- Zeit des ersten Starts, beginnend mit dem Laden des Systems
- Zeitraum weiterer Starts
- Abhängigkeit vom Timer-Dienst. Tatsächlich ist dies die Zeile, die den Timer ausführt
Interaktives Skript beim Herunterfahren und dein Ziel für das Herunterfahren
In einer anderen Entwicklung musste ich eine kompliziertere Variante zum Herunterfahren der Maschine erstellen - über ein eigenes Ziel, um viele Aktionen auszuführen. Normalerweise wird empfohlen, einen Oneshoot-Dienst mit der Option RemainAfterExit zu erstellen, aber das erlaubt nicht, ein interaktives Skript zu erstellen.
Das liegt daran, dass die mit der Option ExecOnStop ausgeführten Befehle außerhalb von TTY ausgeführt werden! Zu überprüfen ist einfach - füge den Befehl tty ein und speichere dessen Ausgabe.
Deshalb habe ich das Herunterfahren über mein Ziel realisiert. Für 100 % Richtigkeit beanspruche ich nicht, aber es funktioniert!
So wurde es gemacht (in groben Zügen):
Ich habe das Ziel my_shutdown.target erstellt, das von niemandem abhängt:
my_shutdown.target
[Unit]
Description=my shutdown
AllowIsolate=yes
Wants=my_shutdown.service Beim Übergang zu diesem Ziel (über systemctl isolate my_shutdown.target) startete es den Dienst my_shutdown.service, dessen Aufgabe einfach ist - das Skript my_shutdown.sh auszuführen:
[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- Innerhalb dieses Skripts führe ich die erforderlichen Aktionen aus. Man kann viele Skripte zum Ziel hinzufügen, um Flexibilität und Komfort zu gewährleisten:
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/
$commandHinweis. Verwendung der Dateien \/tmp\/reboot und \/tmp\/shutdown. Es ist nicht möglich, ein Ziel mit Parametern aufzurufen. Nur einen Dienst.
Aber ich benutze ein Ziel, um Flexibilität in der Arbeit und die garantierte Reihenfolge der Ausführung der Aktionen zu haben.
Das Interessanteste geschah jedoch später. Die Maschine muss schließlich ausgeschaltet\/neugestartet werden. Und hier gibt es 2 Möglichkeiten:
- Die Befehle reboot, shutdown und andere (die ohnehin Symlinks zu systemctl sind) durch mein Skript ersetzen. Innerhalb des Skripts erfolgt der Übergang zu my_shutdown.target. Und die Skripte innerhalb des Ziels rufen dann direkt systemctl auf, z.B. systemctl reboot.
- Eine einfachere, aber mir nicht gefallende Variante. In allen Schnittstellen nicht shutdown\/reboot\/sonstige aufrufen, sondern direkt das Ziel systemctl isolate my_shutdown.target anrufen.
Ich habe die erste Variante gewählt. In systemd sind reboot (wie auch poweroff) Symlinks auf systemd.
ls -l \/sbin\/poweroff
lrwxrwxrwx 1 root root 14 Sep 30 18:23 \/sbin\/poweroff -> \/bin\/systemctlDeshalb können sie durch eigene Skripte ersetzt werden:
neu starten
#!/bin/sh
touch /tmp/reboot
sudo systemctl isolate my_shutdown.target
fiQuelle: habr.com
