Systemd, scripts interativos e temporizadores

Systemd, scripts interativos e temporizadores

Introdução

Ao desenvolver para Linux, surgem tarefas de criação de scripts interativos que são executados ao ligar ou desligar o sistema. No System V, isso era fácil, mas com o systemd, há ajustes. No entanto, ele possui seus próprios temporizadores.

Por que os targets são necessários

Frequentemente, diz-se que os targets são análogos aos runlevels no System V init. Discordo plenamente. Eles são mais numerosos e podem agrupar pacotes, permitindo, por exemplo, iniciar um grupo de serviços com um único comando e realizar ações adicionais. Além disso, não têm hierarquia, apenas dependências.

Exemplo de target ao ligar (visão geral das possibilidades) com o início de um script interativo

Descrição do próprio target:

cat installer.target
[Unit]
Description=Meu instalador
Requires=multi-user.target 
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target 
AllowIsolate=yes
Wants=installer.service

Esse target será iniciado quando o multi-user.target for ativado e chamará o installer.service. Podem haver vários serviços desse tipo.

cat installer.service
[Unit]
# descrição
Description=diálogo interativo do instalador

[Service]
# Executar uma vez, quando o restante estiver iniciado
Type=idle
# Comando de inicialização - chamada do script
ExecStart=\/usr\/bin\/installer.sh
# Interação interativa com o usuário através do tty3
StandardInput=tty
TTYPath=\/dev\/tty3
TTYReset=yes
TTYVHangup=yes

[Install]
WantedBy=installer.target

E finalmente, um exemplo do script executado:

#!/bin/bash
# Переходим в tty3
chvt 3
echo "Install, y/n ?"
read user_answer

O mais importante é escolher o final.target - o target ao qual o sistema deve chegar durante a inicialização. Durante o processo de inicialização, o systemd percorrerá as dependências e iniciará tudo o que for necessário.
Existem diferentes maneiras de escolher o final.target; eu usei para isso uma opção do gerador de inicialização.

A inicialização final se parece com isto:

  1. O gerador de inicialização é iniciado
  2. O gerador começa a iniciar o firmware, passando o parâmetro final.target
  3. O systemd inicia o sistema. Ele segue sequencialmente para o installer.target ou work.target a partir do basic.target através de suas dependências (por exemplo, multi-user.target). Estas últimas levam o sistema a funcionar no modo desejado.

Preparação do firmware para o início

Ao criar firmwares, sempre surge a tarefa de restaurar o estado do sistema na inicialização e de mantê-lo ao desligar. O estado refere-se a arquivos de configuração, dumps de banco de dados, configurações de interfaces, etc.

O systemd inicia processos em um target em paralelo. Existem dependências que permitem determinar a sequência de execução dos scripts.

Como isso funciona no meu projeto ( https://habr.com/ru/post/477008/ https://github.com/skif-web/monitor)

  1. O sistema é iniciado
  2. O serviço settings_restore.service está sendo iniciado. Ele verifica a presença do arquivo settings.txt na partição de dados. Se não estiver presente, um arquivo padrão é colocado em seu lugar. Em seguida, as configurações do sistema são restauradas:
    • senha de administrador
    • hostname,
    • fuso horário
    • localidade
    • Verificação se todo o dispositivo está sendo utilizado. Por padrão, o tamanho da imagem é pequeno — para facilitar a cópia e gravação no dispositivo. Ao iniciar, é verificado se há espaço não utilizado. Se houver, o disco será reorganizado.
    • Geração do machine-id a partir do endereço MAC. Isso é importante para obter o mesmo endereço via DHCP
    • Configurações de rede
    • O tamanho dos logs é limitado
    • Preparando o disco externo para uso (se a opção correspondente estiver ativada e o disco for novo)
  3. Iniciando o postgresq
  4. o serviço restore está sendo iniciado. Ele é necessário para preparar o Zabbix e seu banco de dados:
    • Verifica se já existe um banco de dados Zabbix. Se não houver, ele é criado a partir dos dumps iniciais (que acompanham o Zabbix)
    • uma lista de fusos horários é criada (necessária para a exibição na interface web)
    • O IP atual é localizado e exibido na issue (convite para entrar no console)
  5. O prompt é alterado — a frase 'Ready to work' aparece
  6. A firmware está pronta para uso

Os arquivos de serviço são importantes, pois definem a sequência de inicialização

[Unit]
Description=restaurar configurações do sistema
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

Como pode ser visto, coloquei as dependências para que meu script fosse executado primeiro, e somente depois a rede fosse levantada e o SGBD fosse iniciado.

E o segundo serviço (preparação do 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

Aqui é um pouco mais complicado. A inicialização também está em multi-user.target, mas APÓS a inicialização do SGBD PostgreSQL e do meu settings_restore. Mas ANTES do início dos serviços do Zabbix.

Serviço com um timer para logrotate

O Systemd pode substituir o CRON. Sério. Além disso, a precisão não é até o minuto, mas até o segundo (caso precise). Pode-se criar um timer monotônico, acionado por um timeout de um evento.
Foi exatamente um timer monotônico, contando o tempo desde a inicialização da máquina, que eu criei.
Para isso, serão necessários 2 arquivos
logrotateTimer.service — a descrição do serviço em si:

[Unit]
Description=executar logrotate

[Service]
ExecStart=logrotate \/etc\/logrotate.conf
TimeoutSec=300

É simples — descrição do comando de execução.
O segundo arquivo logrotateTimer.timer — ele define o funcionamento dos timers:

[Unit]
Description=Executar logrotate

[Timer]
OnBootSec=15min
OnUnitActiveSec=15min

[Install]
WantedBy=timers.target

O que há aqui:

  • descrição do timer
  • Tempo para a primeira execução, a partir do carregamento do sistema
  • período para execuções subsequentes
  • Dependência do serviço de timers. Na verdade, esta linha é o que faz o timer funcionar

Script interativo na desligamento e seu target de desligamento

Em outro desenvolvimento, tive que criar uma variante mais complexa para desligar a máquina — através de meu próprio target, para executar várias ações. Normalmente, recomenda-se criar um serviço oneshot com a opção RemainAfterExit, mas isso não permite criar um script interativo.

O fato é que os comandos executados pela opção ExecOnStop são executados fora do TTY! É fácil verificar — insira o comando tty e salve sua saída.

Portanto, eu implementei o desligamento através do meu próprio target. Não reivindico 100% de precisão, mas funciona!
Como isso foi feito (de forma geral):
Criei o target my_shutdown.target, que não dependia de ninguém:
my_shutdown.target

[Unit]
Description=meu desligamento
AllowIsolate=yes
Wants=my_shutdown.service 

Ao mudar para esse target (via systemctl isolate my_shutdown.target), ele iniciava o serviço my_shutdown.service, cuja tarefa era simples — executar o script my_shutdown.sh:

[Unit]
Description=MEU desligamento

[Service]
Type=oneshot
ExecStart=\/usr\/bin\/my_shutdown.sh
StandardInput=tty
TTYPath=\/dev\/tty3
TTYReset=yes
TTYVHangup=yes

WantedBy=my_shutdown.target

  • Dentro deste script, realizo as ações necessárias. É possível adicionar muitos scripts ao target, para flexibilidade e conveniência:

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

Nota. Uso dos arquivos \/tmp\/reboot e \/tmp\/shutdown. Não é possível chamar um target com parâmetros. Apenas serviços.

Mas eu uso um target para ter flexibilidade no trabalho e uma ordem garantida para a execução das ações.

No entanto, o mais interessante veio depois. Você precisa desligar/reiniciar a máquina. E aqui há 2 opções:

  • Substituir os comandos reboot, shutdown e outros (que são links simbólicos para systemctl) pelo seu próprio script. Dentro do script — mudar para my_shutdown.target. E os scripts dentro do target chamam diretamente o systemctl, por exemplo, systemctl reboot
  • Uma opção mais simples, mas que não me agrada. Em todas as interfaces, não invoque shutdown/reboot, mas chame diretamente o target systemctl isolate my_shutdown.target

Escolhi a primeira opção. No systemd, reboot (assim como poweroff) são links simbólicos para systemd.

ls -l \/sbin\/poweroff 
lrwxrwxrwx 1 root root 14 set 30 18:23 \/sbin\/poweroff -> \/bin\/systemctl

Portanto, podem ser substituídos por seus próprios scripts:
reiniciar

#!/bin/sh
    touch /tmp/reboot
    sudo systemctl isolate my_shutdown.target
fi

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster