Введение
При разработке под linux возникают задачи создания интерактивных скриптов, выполняемых при включении или завершении работы системы. В system V это делалось легко, но с systemd вносит коррективы. Зато оно умеет свои таймеры.
Зачем нужны target
Часто пишут, что target служат аналогом runlevel в system V -init. В корне не согласен. Их больше и можно разделять пакеты по группам и, к примеру, запускать одной командой группу сервисов, выполнять дополнительные действия. Кроме того, у них нет иерархии, только зависимости.
Пример target при включении(обзор возможности) с запуском интерактивного скрипта
Описание самого target:
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
Данный target запустится, когда будет запущен multi-user.target и вызовет installer.service. При этом таких сервисов может быть несколько.
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
И наконец, пример выполняемого скрипта:
#!/bin/bash
# Переходим в tty3
chvt 3
echo "Install, y/n ?"
read user_answer
Самое главное — выбрать final.target — target, к которому система должна придти при запуске. В процессе запуска systemd пройдёт по зависимостям и запустит всё нужное.
Выбрать final.target можно разными способами, я использовал для этого опцию загрузчика.
Итоговый запуск выглядит так:
- Стартует загрузчик
- Загрузчик начинает запуск прошивки, передавая параметр final.target
- Systemd начинает запуск системы. Последовательно идёт к installer.target или work.target от basic.target через их зависимости (например,multi-user.target). Последние и приводят систему к работе в нужном режиме
Подготовка прошивки к запуску
При создании прошивок всегда возникает задача восстановления состояния системы при старте и его сохранении при выключении. Под состоянием подразумеваются конфигурационные файлы, дампы базы данных, настройки интерфейсов и тд.
Systemd запускает процессу в одном таргете параллельно. Есть зависимости, которые позволяют определить последовательность запуска скриптов.
Как это работает у меня в проекте ( https://habr.com/ru/post/477008/ https://github.com/skif-web/monitor)
- Система стартует
- Запускается сервис settings_restore.service.Он проверяет наличие файла settings.txt в разделе с данными. Если его нет, то на его место кладётся эталонный файл.Далее происходит восстановление настроек системы:
- пароля администратора
- hostname,
- часового пояс
- локаль
- Определение, весь ли носитель используется. По умолчанию размер образа небольшой — для удобства копирования и записи на носитель. При старте проверяется — есть ли ещё неиспользуемое место. Если есть — диск переразбивается.
- Генерация machine-id из MAC-адреса. Это важно для получения одного и того же адреса по DHCP
- Настройки сети
- Ограничивается размер логов
- Подготавливается к работа внешний диск(если включена соответствующая опция и диск новый)
- Запускаться postgresq
- запускается сервис restore. Он нужен для подготовки самого zabbix и его базы данных:
- Проверяется, есть ли уже база данных zabbix. Если нет — создается из инициализирующих дампов(идут в поставке zabbix)
- создается список часовых поясов (нужно для их отображения в web-интерфейсе)
- Находится текущий IP, он выводится в issue (приглашение для входа в консоли)
- Меняется приглашение — появляется фраза Ready to work
- Прошивка готова к работе
Важны файлы сервисов, именно они выставляют последовательность их запуска
[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
Как видно, я поставил зависимости, что бы сначала отработал мой скрипт, а только потом поднималась сеть и стартовала СУБД.
И второй сервис(подготовка 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
Здесь немного сложнее.Запуск так же в multi-user.target, но ПОСЛЕ запуска СУБД postgresql и моего setting_restore. Но ПЕРЕД запуском служб zabbix.
Сервис с таймером для logrotate
Systemd может заменить CRON. Серьезно. Причем точность не до минуты, а до секунды(а вдруг понадобится).А можно создать монотонный таймер, вызываемый по таймауту от события.
Именно монотонный таймер, считающий время от запуска машины, я и создал.
Для этого потребуется 2 файла
logrotateTimer.service — собственно описание сервиса:
[Unit]
Description=run logrotate
[Service]
ExecStart=logrotate /etc/logrotate.conf
TimeoutSec=300
Всё просто — описание команда запуска.
Второй файл logrotateTimer.timer — вот он и задает работу таймеров:
[Unit]
Description=Run logrotate
[Timer]
OnBootSec=15min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
Что здесь есть:
- описание таймера
- Время первого запуска, начиная от загрузки систем
- период дальнейших запусков
- Зависимость от службы таймеров.Фактически, это строка и делает таймер
Интерактивный скрипт при выключении и свой таргет выключения
В другой разработке мне пришлось делать более сложный вариант выключения машины — через собственный таргет, что бы выполнить множество действий. Обычно рекомендуется создать сервис oneshot с опцией RemainAfterExit, но это не дает создать интерактивный скрипт.
А дело в том, что команды, запускаемые опцией ExecOnStop выполняются вне TTY! Проверить просто — вставьте команду tty и сохраните её вывод.
Поэтому я реализовал выключение через свой таргет. На 100% правильность не претендую, но это работает!
Как это делалось(в общих чертах):
Создал таргет my_shutdown.target, который ни от кого не зависел:
my_shutdown.target
[Unit]
Description=my shutdown
AllowIsolate=yes
Wants=my_shutdown.service
При переходе в этот таргет(через systemctl isolate my_shutdwn.target), он запускал сервис my_shutdown.service, задача которого простая — выполнить скрипт my_shutdown.sh:
[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
- Внутри этого скрипта я выполняю нужные действия. Можно в таргет добавить много скриптов, для гибкости и удобства:
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
Примечание. Использование файлов /tmp/reboot и /tmp/shutdown. Нельзя вызвать target с параметрами. Можно только service.
Но я использую target, что бы иметь гибкость в работе и гарантированный порядок выполнения действий.
Однако, самое интересное было потом. Машину же надо выключить/перезагрузить. И тут есть 2 варианта:
- Заменить команды reboot,shutdown и прочие(они все равно являются симлинками на systemctl) на свой скрипт.Внутри скрипта — переход в my_shutdown.target. А скрипты внутри таргета потом вызывают напрямую systemctl, например, systemctl reboot
- Более простой, но мне не нравящийся вариант. Во всех интерфейсах вызывать не shutdown/reboot/прочие, а напрямую вызывать таргет systemctl isolate my_shutdown.target
Я выбрал первый вариант. В systemd reboot(как и poweroff) являются симлинками на systemd.
ls -l /sbin/poweroff
lrwxrwxrwx 1 root root 14 сен 30 18:23 /sbin/poweroff -> /bin/systemctl
Поэтому их можно заменить на свои скрипты:
reboot
#!/bin/sh
touch /tmp/reboot
sudo systemctl isolate my_shutdown.target
fi
Комментарии (10)
b_oberon
05.12.2019 19:37А о чем вообще статья, какую проблему автор пытается решить? Где-то в середине статьи внезапно появилось слово "прошивка" (embedded-системы?), потом postgresq и zabbix (нет, показалось, наверное). И в конце — странная потребность выключать систему в интерактивном режиме, но через systemd.
Boozlachu Автор
06.12.2019 08:35Поясню, что и зачем применялось.
Выбор final.target и свои target — для выбора режима загрузки — live-диск, инсталлятор, нормальная работа. А также для удобной группировки своих скриптов.
Интерактивный скрипт включения — инсталлятор должен иметь возможность взаимодействовать с пользователем.
Интерактивный скрипт выключения — редко, но потребовалось запросить подтверждение от пользователя на выполнение обновления.Потому что так захотел заказчик.
Zabbix и postgresql вполне могут жить на встроенной системе. Не вижу противоречия. Собственно, такую прошивку я и описывал в другой статье — arm(2 разных платы)/x86_64 с zabbix-server и web-интерфейсом для управления.
Shumsky90
у нас есть одни люди, которые ОЧЕНЬ любят писать интерактивные инсталляционные скрипты. Из-за чего херится вся возможность автоматической инсталляции, ибо оставленный на ночь апдейт базы стопается на вопросе «are you sure that you want update database?». А тут интерактивные скрипты запуска сервисов…
Boozlachu Автор
Ну, обновлять ночью без присмотра, своими скриптами…
А что мешает написать скрипт с параметрами автоматической установки? Или параметр, указывающий файл с конфигурацией установки? Я не говорю про Tk/Tcl.
Ваш комментарий не имеет отношения к статье.
Shumsky90
1. в базах данных бывают таблички побольше чем «5 полей, 10 записей». И если делать на такой таблице alter table… Он занимает некоторое время, если коротко. Если учесть что мы не одни такие на сервере — можно представить. Плюс оракл, ага
2. мешает чОтко написанная процедура, которой мы должны следовать. В больших конторах большая бюрократия, как правило.
3. чтобы закомиттить свое — необходимо пройти такой квест, что ад по Данте покажется легкой прогулкой.
это все ответ на Ваш каммент. А по отношению к статье уточню:
1. мое имхо — интерактив в старт\стоп сервисах — лишнее. Сервис должен быть полностью работоспособен после ребута. А не идти на сервер и дергать его ручками
2. слабо представляю зачем вообще это все необходимо. Если дернули апдейт по какому-то событию (нажатие аппаратной кнопки, ребут, что-то там еще) — то он должен пойти самостоятельно, без интерактива. Ибо что, например, делать в случае no-tty девайса?
Boozlachu Автор
Моё ИМХО — думать надо что, где и как применять. Мне не приходит в голову делать скрипт запуска веб-сервиса интерактивным, не попадалось таких задач. Но инсталлятор ОС в консольном режиме — его придётся делать интерактивным.
Если не устраивает организационная сторона — лучше взять самому за ситуацию. Проблемы с процедурой — пишите и пробивайте новую. НО проблемы с организацией это не повод ненавидеть те или иные технические решения.
monah_tuk
Если напрягает, а тяга к автоматизации велика, то expect — ваш выбор:
kiriharu
expect в помощь.