Введение


При разработке под 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 можно разными способами, я использовал для этого опцию загрузчика.


Итоговый запуск выглядит так:


  1. Стартует загрузчик
  2. Загрузчик начинает запуск прошивки, передавая параметр final.target
  3. Systemd начинает запуск системы. Последовательно идёт к installer.target или work.target от basic.target через их зависимости (например,multi-user.target). Последние и приводят систему к работе в нужном режиме

Подготовка прошивки к запуску


При создании прошивок всегда возникает задача восстановления состояния системы при старте и его сохранении при выключении. Под состоянием подразумеваются конфигурационные файлы, дампы базы данных, настройки интерфейсов и тд.


Systemd запускает процессу в одном таргете параллельно. Есть зависимости, которые позволяют определить последовательность запуска скриптов.


Как это работает у меня в проекте ( https://habr.com/ru/post/477008/ https://github.com/skif-web/monitor)


  1. Система стартует
  2. Запускается сервис settings_restore.service.Он проверяет наличие файла settings.txt в разделе с данными. Если его нет, то на его место кладётся эталонный файл.Далее происходит восстановление настроек системы:
    • пароля администратора
    • hostname,
    • часового пояс
    • локаль
    • Определение, весь ли носитель используется. По умолчанию размер образа небольшой — для удобства копирования и записи на носитель. При старте проверяется — есть ли ещё неиспользуемое место. Если есть — диск переразбивается.
    • Генерация machine-id из MAC-адреса. Это важно для получения одного и того же адреса по DHCP
    • Настройки сети
    • Ограничивается размер логов
    • Подготавливается к работа внешний диск(если включена соответствующая опция и диск новый)
  3. Запускаться postgresq
  4. запускается сервис restore. Он нужен для подготовки самого zabbix и его базы данных:
    • Проверяется, есть ли уже база данных zabbix. Если нет — создается из инициализирующих дампов(идут в поставке zabbix)
    • создается список часовых поясов (нужно для их отображения в web-интерфейсе)
    • Находится текущий IP, он выводится в issue (приглашение для входа в консоли)
  5. Меняется приглашение — появляется фраза Ready to work
  6. Прошивка готова к работе

Важны файлы сервисов, именно они выставляют последовательность их запуска


[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)


  1. Shumsky90
    05.12.2019 15:22
    -1

    у нас есть одни люди, которые ОЧЕНЬ любят писать интерактивные инсталляционные скрипты. Из-за чего херится вся возможность автоматической инсталляции, ибо оставленный на ночь апдейт базы стопается на вопросе «are you sure that you want update database?». А тут интерактивные скрипты запуска сервисов…


    1. Boozlachu Автор
      05.12.2019 15:57

      Ну, обновлять ночью без присмотра, своими скриптами…
      А что мешает написать скрипт с параметрами автоматической установки? Или параметр, указывающий файл с конфигурацией установки? Я не говорю про Tk/Tcl.
      Ваш комментарий не имеет отношения к статье.


      1. Shumsky90
        05.12.2019 16:10

        1. в базах данных бывают таблички побольше чем «5 полей, 10 записей». И если делать на такой таблице alter table… Он занимает некоторое время, если коротко. Если учесть что мы не одни такие на сервере — можно представить. Плюс оракл, ага
        2. мешает чОтко написанная процедура, которой мы должны следовать. В больших конторах большая бюрократия, как правило.
        3. чтобы закомиттить свое — необходимо пройти такой квест, что ад по Данте покажется легкой прогулкой.

        это все ответ на Ваш каммент. А по отношению к статье уточню:
        1. мое имхо — интерактив в старт\стоп сервисах — лишнее. Сервис должен быть полностью работоспособен после ребута. А не идти на сервер и дергать его ручками
        2. слабо представляю зачем вообще это все необходимо. Если дернули апдейт по какому-то событию (нажатие аппаратной кнопки, ребут, что-то там еще) — то он должен пойти самостоятельно, без интерактива. Ибо что, например, делать в случае no-tty девайса?


        1. Boozlachu Автор
          05.12.2019 16:35

          Моё ИМХО — думать надо что, где и как применять. Мне не приходит в голову делать скрипт запуска веб-сервиса интерактивным, не попадалось таких задач. Но инсталлятор ОС в консольном режиме — его придётся делать интерактивным.
          Если не устраивает организационная сторона — лучше взять самому за ситуацию. Проблемы с процедурой — пишите и пробивайте новую. НО проблемы с организацией это не повод ненавидеть те или иные технические решения.


    1. monah_tuk
      06.12.2019 09:00
      +2

      Если напрягает, а тяга к автоматизации велика, то expect — ваш выбор:



    1. kiriharu
      06.12.2019 15:17
      +1

      expect в помощь.


  1. b_oberon
    05.12.2019 19:37

    А о чем вообще статья, какую проблему автор пытается решить? Где-то в середине статьи внезапно появилось слово "прошивка" (embedded-системы?), потом postgresq и zabbix (нет, показалось, наверное). И в конце — странная потребность выключать систему в интерактивном режиме, но через systemd.


    1. Boozlachu Автор
      06.12.2019 08:35

      Поясню, что и зачем применялось.
      Выбор final.target и свои target — для выбора режима загрузки — live-диск, инсталлятор, нормальная работа. А также для удобной группировки своих скриптов.
      Интерактивный скрипт включения — инсталлятор должен иметь возможность взаимодействовать с пользователем.
      Интерактивный скрипт выключения — редко, но потребовалось запросить подтверждение от пользователя на выполнение обновления.Потому что так захотел заказчик.

      Zabbix и postgresql вполне могут жить на встроенной системе. Не вижу противоречия. Собственно, такую прошивку я и описывал в другой статье — arm(2 разных платы)/x86_64 с zabbix-server и web-интерфейсом для управления.


  1. AlexGluck
    05.12.2019 20:59

    А если вызывать systemctl start my_shutdown@reboot то внутри юнита мы можем выполнить exec с переменной %i которая раскроется в отличии от Шелл окружения.


    1. Boozlachu Автор
      06.12.2019 08:31

      Всё верно. Удобный способ запуска сервисов с параметром.