Upgrade do Ubuntu 16.04 oznaczało zmianę upstart na systemd. Z perspektywy szarego używania zmiana nie ma ma jakiegoś odczuwalnego znaczenia ale pod spodem jest oczywiście inaczej. Ja musiałem zdecydować, co zrobić ze skryptami zadań użytkownika, czyli jak teraz uruchamiać moje pomocnicze aplikacje, tunele SSH itp.
Okazało się, że uruchamianie nie-systemowych programów pod kontrolą systemd również jest dość łatwe i przyjemne, przy pomocy nietrudnej konfiguracji można zapewnić automatyczne uruchomienie zbioru potrzebnych programów i samoczynne ich restartowanie gdyby uległy awarii.
Pisząc poprzedni artykuł wstępnie planowałem, że zachowam upstart jako pomocniczą usługę. Po przyjrzeniu się, przerobienie skryptów uznałem za mniej kłopotliwe.
Tworzenie skryptów użytkownika
Systemd standardowo wspiera zadania definiowane przez
użytkowników. Zauważa i obsługuje zadania umieszczane w katalogu
$HOME/.config/systemd/user
. Przykładowo, by zapewnić, że
zawsze gdy się zaloguję, zostanie uruchomiony tunel SSH pozwalający mi
korzystać z testowej bazy danych w firmie, stworzyłem plik
$HOME/.config/systemd/user/mk-tunel-pg-lagrange.service
o treści:
[Unit]
Description=ssh tunnel: PG_LAGRANGE
After=network.target
[Service]
Type=simple
Environment=LOCAL=5532
Environment=REMOTE=lagrange.firma.local:5432
Environment=VIA=sshproxy.firma.local
Environment=REMOTE_USER=marcin
ExecStart=/usr/bin/ssh -L ${LOCAL}:${REMOTE} -l $REMOTE_USER -N $VIA
RestartSec=3
Restart=always
[Install]
WantedBy=default.target
Konwencję zaczynania nazw od
mk-
wprowadziłem, by łatwiej odnajdywać moje zadania i odróżniać je od innych. A skrypt powyżej mógłby być krótszy, zapisyEnvironment
wykorzystałem tylko dla czytelności (podobnie jak poprzednio w zadaniu upstartowym).
Po napisaniu skryptu musiałem napisać:
$ systemctl --user daemon-reload
by systemd go zauważył (tego samego polecenia używam też, gdy jakiś skrypt zmienię — by zauważył zmiany w treści). Od tego momentu zadanie można uruchamiać i zatrzymywać ręcznie:
$ systemctl --user start mk-tunel-pg-lagrange
$ systemctl --user status mk-tunel-pg-lagrange
● mk-tunel-pg-lagrange.service - ssh tunnel: PG_LAGRANGE
Loaded: loaded (/home/marcink/DEV/cfg/systemd-dirichlet/user/mk-tunel-pg-lagrange.service; enabled; v
Active: active (running) since nie 2016-10-16 16:40:37 CEST; 4s ago
Main PID: 17639 (ssh)
CGroup: /user.slice/user-1000.slice/user@1000.service/mk-tunel-pg-lagrange.service
└─17639 /usr/bin/ssh -L 51521:lagrange.firma.local:1521 -l marcin -N sshproxy.firma.local
paź 16 16:40:37 dirichlet systemd[6089]: Started ssh tunnel: PG_LAGRANGE
$ systemctl --user stop mk-tunel-pg-lagrange
$ systemctl --user status mk-tunel-pg-lagrange
● mk-tunel-pg-lagrange.service - ssh tunnel: PG_LAGRANGE
Loaded: loaded (/home/marcink/DEV_hg/cfg/systemd-dirichlet/user/mk-tunel-pg-lagrange.service; enabled; v
Active: inactive (dead)
paź 16 16:40:37 dirichlet systemd[6089]: Started ssh tunnel: PG_LAGRANGE.
paź 16 16:45:00 dirichlet systemd[6089]: Stopping ssh tunnel: PG_LAGRANGE...
paź 16 16:45:00 dirichlet systemd[6089]: Stopped ssh tunnel: PG_LAGRANGE.
Warto zwrócić uwagę, że
start
istop
są ciche, do sprawdzenia czy program działa trzeba użyćstatus
.
Wreszcie, aby polecenie faktycznie było automatycznie uruchamiane przy uruchamianiu sesji, trzeba je aktywować:
$ systemctl --user enable mk-tunel-pg-lagrange
To ostatnie po prostu tworzy symlinka do powyższego skryptu w podkatalogu
default.target.wants
(plik
$HOME/.config/systemd/user/default.target.wants/mk-tunel-pg-lagrange.service
),
przy czym to gdzie i czy w ogóle taki symlink jest zakładany, zależy
od treści dyrektywy WantedBy
.
Dla sesji użytkownika sensownie mi działa właśnie
default.target
(oraz moje własne cele o których niżej). Nie dopatrzyłem się na razie czy/jak dopiąć moje zadanie do bardziej szczegółowych warunków.
Zaprzestać automatycznego uruchamiania można po prostu usuwając symlinka albo, bardziej uroczyście:
$ systemctl --user disable mk-tunel-pg-lagrange
Drugi z przykładów (uruchamianie RDM-a), czyli
plik ~/.config/systemd/user/mk-rtags-rdm.service
, wygląda natomiast tak:
[Unit]
Description=rtags daemon for emacs C++ completion
[Service]
Type=simple
ExecStart=/usr/local/bin/rdm \
--data-dir /data/marcink/rtags-rdm \
--log-file /logs/marcink/rdm-rtags.log \
--job-count 2 \
--rp-nice-value 5
WorkingDirectory=/data/marcink/rtags-rdm
Restart=always
RestartSec=10
Nice=10
OOMScoreAdjust=800
# Powyższe chętniej zabija ten proces gdy brakuje RAM
[Install]
WantedBy=default.target
Integracja z sesją X11
Niektóre z moich programów korzystają z sesji X11 i potrzebują widzieć identyfikujące sesję zmienne środowiskowe.
Aby je przekazać, stworzyłem plik
/etc/X11/Xsession.d/99systemd
(99
jest ważne, skrypt powinien się uruchomić późno) o treści
#/bin/sh
# Import zmiennych przydatnych w skryptach systemd-owych
systemctl --user import-environment DISPLAY XAUTHORITY
systemctl --user import-environment SSH_AUTH_SOCK GPG_AGENT_INFO
systemctl --user import-environment GTK_IM_MODULE QT_IM_MODULE QT4_IM_MODULE GTK_MODULES GTK2_MODULES
Robiąc powyższy wykaz zmiennych patrzyłem na export
-y znajdujące
się w innych skryptach leżących w /etc/X11/Xsession.d
— celem było
zapewnienie w środowisku systemd dostępności właśnie tych zmiennych,
które są udostępniane normalnemu desktopowemu środowisku.
Powyższe zadziała od następnego zalogowania, do bieżącego testu po prostu uruchomiłem ten skrypt ręcznie w terminalu.
Kontrola środowiska widzianego przez programy startowane przez systemd:
$ systemctl --user show-environment
DISPLAY=:0
GPG_AGENT_INFO=/home/marcink/.gnupg/S.gpg-agent:0:1
GTK2_MODULES=overlay-scrollbar
…
Przy tej konfiguracji mogę pod systemd odpalać nawet zupełnie desktopowe programy (robię tak teraz z Emacsem).
Inne zmienne środowiskowe
Wszelkie inne zmienne środowiskowe potrzebne konkretnym programom
definiuję sobie w skryptach systemd, czasem przy pomocy dyrektywy
Environment
(czego przykład był w powyższym skrypcie dla tunelu
SSH) ale chętniej przy pomocy EnvironementFile
:
# …
[Service]
# …
EnvironmentFile=/home/marcink/common.env
EnvironmentFile=/home/marcink/.config/systemd/user/mk-emacs.env
# …
(wymagana jest pełna ścieżka, powyższy przykład ilustruje podejście gdy część zmiennych jest współdzielona przez kilka skryptów).
Pliki środowiska to po prostu listy
nazwa=wartość
, analogicznie do /etc/environment
. Przykładowo, w tym
pierwszym mam teraz
http_proxy=http://proxy.firma.pl:8000
https_proxy=http://proxy.firma.pl:8000
no_proxy=.firma.local
ORACLE_BASE=/u01/app/oracle
ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe
NLS_LANG=polish_poland.EE8ISO8859P2
TNS_ADMIN=/etc/oracle
TUXDIR=/oracle/tuxedo12.1.1.0
CVSROOT=:pserver:marcink@cvs.firma.local:/CVSrepo
Grupowe uruchamianie
Grupa programów wspierających firmowy development nie jest mi na laptopie potrzebna stale, a jedynie gdy zajmuję się w domu pracą zawodową. Dlatego nie chcę ich uruchamiać zawsze. Z drugiej strony, paczka składa się z trzech tuneli SSH i sześciu aplikacji, uruchamianie każdej z osobna jest męczące.
Rozwiązaniem okazał się własny target. Stworzyłem plik
~/.config/systemd/user/mk-firma-devel.target
o treści:
[Unit]
Description=Aplikacje do firmowego developmentu
(i tylko tyle). Wszystkim programom z grupy wpisałem
# … normalne [Unit] i [Service] opisujące sposób uruchamiania
[Install]
WantedBy=mk-firma-devel.target
i dla każdego uruchomiłem
$ systemctl --user enable mk-nazwa-programu
(w tym wypadku powstawały symlinki w podkatalogu mk-firma-devel.target.wants
)
Od tego momentu prosta komenda
$ systemctl --user start mk-firma-devel.target
urchamia wszystkie potrzebne mi programy.
Na razie nie szukałem jeszcze, co zrobić, by również grupowo wszystkie zatrzymać.
Logi, stan działania
Wykaz działających programów i procesów, w odpowiedniej hierarchii:
$ systemctl --user status
● dirichlet
State: running
Jobs: 0 queued
Failed: 0 units
Since: nie 2016-10-16 12:12:16 CEST; 4h 52min ago
CGroup: /user.slice/user-1000.slice/user@1000.service
├─mk-emacs.service
│ ├─6853 /usr/bin/emacs
│ └─8489 /usr/bin/aspell -a -m -d pl --encoding=utf-8
├─mk-tunel-pg-lagrange.service
│ └─16058 /usr/bin/ssh -L 51521:lagrange.firma.local:1521 -l marcin -N sshproxy.firma.local
├─init.scope
│ ├─6089 /lib/systemd/systemd --user
│ └─6098 (sd-pam)
(…)
Podstawowe informacje o pojedynczym konkretnym zadaniu zawierają też informacje wypisane przez program na standardowe wyjście:
$ systemctl --user status mk-dev-appmgr
● mk-dev-appmgr.service - AppMgr Server
Loaded: loaded (/home/marcink/DEV_hg/cfg/systemd-dirichlet/user/mk-dev-appmgr.service; enabled; vendor preset: e
Active: active (running) since nie 2016-10-16 15:41:32 CEST; 3min 25s ago
Main PID: 16056 (appmgr)
CGroup: /user.slice/user-1000.slice/user@1000.service/mk-dev-appmgr.service
└─16056 /home/marcink/dev/bin/appmgr.exe
paź 16 15:41:32 dirichlet systemd[6089]: Started AppMgr.
paź 16 15:41:36 dirichlet appmgr[16056]: ---------------------------------------------------------------
paź 16 15:41:36 dirichlet appmgr[16056]: initializing (port 15010)
Bardziej szczegółowy log na różne sposoby udostępnia journalctl
. W praktyce
najbardziej przydaje się
$ journalctl -xe
(pokazuje ostatnie 1000 wierszy z zagregowanego logu wszystkich programów, od razu otwierając je w less
)
Wersjonowanie
W przygotowanie skryptów wchodzi trochę pracy, dlatego warto
zarządzić ich kodem. Po prostu zrobiłem sobie z
~/.config/systemd
repozytorium Mercurialowe.
Dokładniej, zrobiłem sobie repozytorium w
~/DEV/cfg/systemd-dirichlet
, symlinkując je jako.config/systemd
:
ln -s ~/DEV/cfg/systemd-dirichlet ~/.config/systemd
ale to jest już moja prywatna preferencja do zbierania wartych wersjonowania rzeczy w jednym miejscu. W każdym razie, taki symlink w niczym w używaniu systemd nie przeszkadza.
Pułapki i problemy
W trakcie całej konfiguracji napotkałem na dwa, pokrewne zresztą problemy.
Pierwszym było ubogie środowisko (brak zmiennych środowiskowych
definiowanych w startupie basha, brak zmiennych potrzebnych programom
graficznym) — a rozwiązaniem okazał się opisany wyżej import zmiennych
z X11 i okazyjne dyrektywy Environment
lub EnvironmentFile
w
niektórych skryptach.
Drugi kosztował mnie trochę czasu i irytacji. Wpisałem w pewnym momencie do skryptu startującego Emacsa
Environment=http_proxy="http://proxy.firma.local:8000" https_proxy="http://proxy.firma.local:8000" no_proxy=".firma.local,127.0.0.1,localhost"
i … wszelkie funkcje związane z dostępami HTTP przestały mi działać, tak w samym emacsie jak w odpalanych zeń podprocesach, z dość mistycznymi błędami (np. informacją o niepoprawnym protokole).
Okazało się, że problemem są cudzysłowy. Zapisy Environment
(a także
zapisy w treści plików wskazywanych przez EnvironentFile
) są brane w pełni
literalnie. Tu: wraz z znakami "
(owym błędnym protokołem było "http
,
błędnym portem 8000"
). Usunięcie znaków "
z wiersza Environment
rozwiązało problem.
Podobnie zresztą nie działają odwołania do zmiennych, nie można napisać
Environment=PATH=$PATH:$HOME/bin
(a raczej można, ale efektem będzie całkowicie niefunkcjonalna ścieżka
nakazująca szukanie w katalogach /$PATH
i
/$HOME/bin
). Ewentualną ścieżkę trzeba napisać jawnie, w całości
(albo wyliczyć jakimś skryptem i importować podobnie jak zmienne X11).
Dalszy potencjał
Możliwości systemd na tym się nie kończą. Na pewno interesująco wygląda możliwość aktywacji socketowej (budzenia programu gdy ktoś spróbuje się podłączyć do konkretnego portu). Powinno być też możliwe finezyjne ułożenie zależności (np. otwieranie tuneli dopiero gdy wstanie VPN z którego korzystają). Obie rzeczy planuję kiedyś przećwiczyć.
Najważniejsze komendy
I jeszcze zwarta ściągawka najważniejszych komend:
$ systemctl --user
$ systemctl --user status
$ systemctl --user status --all
Krótki i bardziej rozbudowany wykaz co działa a w wariancie z
--all
także wykaz skryptów, które istnieją ale nie działają (są
zatrzymane).
systemctl --user start jakies.service
systemctl --user status jakies.service
systemctl --user stop jakies.service
Start, weryfikacja stanu i stop konkretnej usługi.
$ journalctl -xe
Podgląd logu.
$ systemctl --user daemon-reload
Ponowne wczytanie zdefiniowanych skryptów.
$ systemctl --user enable jakies.service
$ systemctl --user disable jakies.service
Stworzenie lub usunięcie symlinków zapewniających automatyczną aktywację
(odzwierciedlających stan bloku [Install]
).
$ systemctl --user show-environment
Weryfikacja środowiska, jakie widzą odpalane programy..
alias sysu="systemctl --user"
Aaaby pisać sysu status
.
Więcej informacji
-
man systemd.unit
(większość składni) -
man systemd.service
(opis bloku [Service]) -
man systemd.special
(opis targetów itp) -
man systemd.exec
(opis środowiska uruchomieniowego i wpływających na nie parametrów)
oraz:
Many są też dostępne w sieci, np. man systemd.exec