Запуск скрипта при выключении компьютера
Задача: Сделать автоматический запуск нашего скрипта перед перезагрузкой и выключением компьютера.
Для примера создадим скрипт выполняющий обновление системы перед выключением.
Для управления загрузкой и работой сервисов в kubuntu начиная с версии 15.04 используется systemd.
Systemd хранит свои юниты в папке /etc/systemd/system/ туда мы и положим свой новый юнит.
Создаем файл сервиса с именем poweroff_dist_upgrade.service
sudo nano /etc/systemd/system/poweroff_dist_upgrade.service
добавляем в него содержимое
[Unit]
Description=Dist Upgrade before restart and power off
After=systemd-user-sessions.service
[Install]
WantedBy=multi-user.target reboot.target poweroff.target
Unit — общая информация.
Description — описание сервиса.
After — задаёт порядок загрузки. В этом случае после старта сессии пользователя.
Service — содержит информацию о службе
ExecStop — Какой скрипт будет выполнен
Type — Типы службы. oneshot для скриптов, которые выполняют одно задание и завершаются
Install — когда юнит должен быть активирован.
multi-user.target или runlevel3.target соответствует runlevel=3 «Многопользовательский режим без графики»
Назначаем права на запуск
sudo chmod +x /etc/systemd/system/poweroff_dist_upgrade.service
Создаем сам скрипт который будет выполняться
sudo nano /usr/local/bin/distupgrade.sh
#!/bin/bash
apt update && apt dist-upgrade -f -y && apt autoremove -y
echo ‘Обновление ‘$(date ‘+%m.%d %H:%M’) >> /var/log/dist-upgrade_before_power_off.log
exit 0
Делаем скрипты выполняемым
sudo chmod +x /usr/local/bin/distupgrade.sh
Перезагружаем systemd для поиска новых или измененных юнитов:
sudo systemctl daemon-reload
Включаем наш новый юнит
sudo systemctl enable poweroff_dist_upgrade.service
Всё, теперь после этого при выключении и перезагрузки компьютера будет автоматически запускаться наш скрипт /usr/local/bin/distupgrade.sh
Для отключения используем
sudo systemctl disable poweroff_dist_upgrade.service
Systemd, интерактивные скрипты и таймеры
При разработке под linux возникают задачи создания интерактивных скриптов, выполняемых при включении или завершении работы системы. В system V это делалось легко, но с systemd вносит коррективы. Зато оно умеет свои таймеры.
Зачем нужны target
Часто пишут, что target служат аналогом runlevel в system V -init. В корне не согласен. Их больше и можно разделять пакеты по группам и, к примеру, запускать одной командой группу сервисов, выполнять дополнительные действия. Кроме того, у них нет иерархии, только зависимости.
Пример 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 запускает процессу в одном таргете параллельно. Есть зависимости, которые позволяют определить последовательность запуска скриптов.
- Система стартует
- Запускается сервис 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
- Внутри этого скрипта я выполняю нужные действия. Можно в таргет добавить много скриптов, для гибкости и удобства:
#!/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