2022/04/20

[linux] bash の -u と -e

私が bash のスクリプトで bash のオプションとしてしばしば使うのは -u と -e だ。

https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html


-u は、設定されていない変数やパラメータをエラーとして扱うというもの。

私はこの「パラメータ」だけに反応していて、つまり bash の引数である $1 とか $2 を参照しようとして引数として与えられていなかったらエラーになるんだろう、と思い込んでいた。

さっきまで。

しかしよく読むと変数も対象ではないか。

#!/bin/bash -u

$GOPATH/bin/aaa

これで GOPATH に設定し忘れて実行してもエラーになってくれる。
私の開発スタイルだと GOPATH は未設定の状態がデフォルトなので、こうやってエラーになってくれると非常に助かるのだ。

ちなみに今までは、

if [ -z "$GOPATH" ]; then
  echo no GOPATH
  exit 1
fi

みたいなのを書いていた。。。


もう1つが -e で、これは戻り値が 0 以外だったら即時終了するというもの。

まあそのままなのだが、これを付けていると本当に即時終了するので、開発中のデバッグでぐるぐる回すスクリプトなんかで使っているといつの間にか終了してしまっているということがしばしばあり、使うのを敬遠していた。

先ほど -u で気付いたようにこちらも何かあるかもしれないと読んでいると「ERRのトラップ」という言葉が出てきた。

err1.sh というファイルを作ってみた。

#!/bin/bash -eu

function mya() {
    echo MYA-
}

# trap mya ERR

function err1() {
    echo \"err1\"
    return 1
}

err1
echo end.

$ ./err1.sh
"err1"
MYA-

おお!

「trap mya ERR」をコメントアウトすると「"err1"」だけ出力された。

 

全部捕捉されるかは分からんが、助けになりそうだ。

2022/04/17

[linux] logrotateがまだ動いていないのは場所が悪かった

logrotate のことを昨日書いた。

hiro99ma blog: [linux]思ったより logrotate が難しい
https://blog.hirokuma.work/2022/04/linux-logrotate.html

VirtualBox 環境では確認しづらかったので、AWS に立てている Ubuntu で設定して、眠たかったので明日のお楽しみ、だったのだが、今朝見てみるとまだ動いていなかった。

 

かなり長くなったので結論を書くと、

  • systemd の service で動かす logrotate は /tmp や /var/tmp に mount する PrivateTmp が trueになっている
  • /tmp にログファイルを置いていたのだが、PrivateTmp=true によって /tmp に mount されたことでファイルが見えなくなっていた

である。

 


/var/lib/logrotate/status を見ると、2022年4月17日の0時(UTC)に更新されているものがあった(これを書いているのは2022年4月17日 11時(JST))ので、少なくとも 1回は動作していることがわかる。

うーん、ありそうなのはこんなところか。

  • systemctrl の timer が hourly で動いていない
  • logrotate の設定が hourly で動いていない
  • logrotate は実行したけれどもローテートしなくて良いと判断された

 

まず、自分で追加した logrotate の設定をなかったことにするため、 /var/lib/logrotate/status の中から自分のアプリの行を削除した。
これで前回動作したという記憶がなくなったはずだ。

この状態で logrotate を実行してみよう。

$ cat /var/lib/logrotate/status | grep aaaa
$ sudo logrotate /etc/logrotate.d/aaaa
$ cat /var/lib/logrotate/status | grep aaaa
"/tmp/aaaa/aaaa.log" 2022-4-17-2:0:0

うむ、status ファイルに記録が残った。
日本時間11時・・・UTC 2時なので「2:0:0」でよかろう。
なお logrotate の設定ファイルはこんな感じだ。

/tmp/aaaa/aaaa.log {
    missingok
    hourly
    rotate 12
    copytruncate
    compress
}

aaaa.log はアプリがオープンしたまま書き込むので copytruncate でコピーしてオリジナルは ftruncate してサイズだけ切り詰めてもらう・・・という動作をしてほしい。

あと 1時間待って、systemctl の timer が自発的にやってくれるのを待とう。


放置している間に情報を探しておこう。

How To Manage Logfiles with Logrotate on Ubuntu 20.04 | DigitalOcean
https://www.digitalocean.com/community/tutorials/how-to-manage-logfiles-with-logrotate-on-ubuntu-20-04#step-3-setting-up-an-example-config

んん?
システムの logrotate は1日1回しか動かないから hourly は動かない・・・?
このサイトの人は、自分のアプリ用に logrotate の設定を用意して、ユーザレベルの cron に logrotate を実行するよう書き加えている。 

 

まあ、私が hourly にしているのもさっさと結果を確認したいからであって、時間ごとにログをローテートさせたいわけではないので、本当に daily 以上の間隔じゃないと動作しなかったとしても別に問題ではない。

ただこのアプリは最終的には systemctl で起動させようと思っているので、logrotate も systemctl の方でやってほしい。


さて、あれから1時間が経過した。

/var/lib/logrotate/status に変化はなかった・・・。
systemctl list-timers の方は LAST の時間が更新されていたので、タイマーは動いたのだと思う。
journalctl -u logrotate でも Succeeded になってるしね。

 

となると、やはりシステムでは logrotate が daily 以上の間隔という話が真実味を帯びてくる。
もしかすると /etc/logrotate.conf の設定がそうなっているのかと思ったのだが、そこは weekly だったので関係ないだろう。

では、/lib/systemd/system/logrotate.service を /etc/systemd/system/ にコピーして、logrotate を -v で実行させよう。
そうすれば journalctl でももう少しログが出てくれるんじゃなかろうか。


さらに1時間待った。
logrotate -v のおかげで journalctl で詳細が出てきた。

Apr 17 04:00:12 logrotate: rotating pattern: /tmp/aaaa/aaaa.log  hourly (12 rotations)
Apr 17 04:00:12 logrotate: empty log files are rotated, old logs are removed
Apr 17 04:00:12 logrotate: switching euid to 0 and egid to 4
Apr 17 04:00:12 logrotate: considering log /tmp/aaaa/aaaa.log
Apr 17 04:00:12 logrotate:   log /tmp/aaaa/aaaa.log does not exist -- skipping

does not exist ???
ls すると見えるんですけど ???
sudo logrotate -d で実行してもエラーになりませんし !!!

... すまん、動揺してしまった。
「empty log files are rotated」の方を気にしないといけない気がする。
前の方で logrotate の動作確認をしたので、そのときに truncate されてファイルサイズがゼロになったままだったのだ。
だからきっと「(ファイル自体はあるかもしれんが)ローテートすべきファイルがない」という意味の「does not exist」なのではなかろうか。
ファイルサイズがゼロにならないようにしてもう1時間待ってみよう。

 

なお、設定ファイルの方は root:root がよいそうだ。
これは私の方もそうしていた。

logrotateの設定ファイル、オーナーが重要 - Qiita
https://qiita.com/abetomo/items/59ee7d55e5458a33988c

ログを格納しているディレクトリのパーミッションが影響することもあるそうだ。
私のはこのエラーログではない。

logrotate ~ ログがローテートされないときに~ | GMOインターネットグループ 次世代システム研究室
https://recruit.gmo.jp/engineer/jisedai/blog/logrotate/

エラーログはそうではなかったのだが、ディレクトリが ubuntu:ubuntu だ!
そういえばアプリは AWS の EC2 に立てた Ubuntu そのままのユーザで動かしているのでそうなっている。

does not exist がサイズが原因だったとして、その次はアカウントの方でエラーになるんじゃなかろうか。
conf ファイルを変更しておきたいところだが、あと 10分くらいで timer の時間になるので待っておこう。

 

「empty log files are rotated」はここ。
LOG_FLAG_IFEMPTY がセットされているということは conf に "ifempty" を書いているということかと思ったのだが、私は書いてない。 "notifempty" もあるので、デフォルトは "ifempty" ということか? なら空ファイルでもローテーションするということではないのか??

https://github.com/logrotate/logrotate/blob/4c52ff5a1ddd0c34096824d786e6a68e20c18136/logrotate.c#L2350

そうならば、ログファイルのサイズを増やした次タイミングでも失敗するに違いない。


1時間経った。
「does not exist」は変わらず。。。
ログファイルはサイズそのままだし、ローテートもされていない。

 

「does not exist」はここ。

https://github.com/logrotate/logrotate/blob/4c52ff5a1ddd0c34096824d786e6a68e20c18136/logrotate.c#L1394

missingok だから return 0 になっているだけだ。

それよりも lstat() が ENOENT になっているせいだということがわかる。
まあ、ENOENT は「ファイルやディレクトリがありません」だろう。
しかし %s しているファイル名と lstat() しているファイルは同じものだから実物の有無以外で ENOENT になっているはずだ。

 

ログの「switching euid to 0 and egid to 4」は、ユーザID を 0、グループID を 4にするということだろう。

https://github.com/logrotate/logrotate/blob/4c52ff5a1ddd0c34096824d786e6a68e20c18136/logrotate.c#L145-L146

conf で su を設定するとそれを使うということだろう。
未設定の場合は

id コマンドで調べると、0 は root、 4 は sync だった。
ならば、ログのファイルやディレクトリが ubuntu:ubuntu なので、logrotate が root:sync に切り替えるとアクセスできないので ENOENT になった、だろうか。

そういうわけで、 conf に「su ubuntu ubuntu」を追加して次のタイミングを待とう。


1時間経過。

はい、変わらず。
su を指定したのに「switching euid to 0 and egid to 4」ってのも変な話よね。
どこかもっと違うところで間違えている気もする。

 

が、せめて単独でコマンドを実行したときと同じ現象であれば調べる気力が湧くのだけど、試せるのが1時間に1回となると非常に効率が悪い。
まずはコマンドラインなどで同じことができるようにしよう。

先ほど /etc/systemd/system にコピーした logrotate.service をリネームして自分の名前にする(aaaa.service とか)。
ExecStart で logrotate の設定ファイルを /etc/logrotate.d/ においた自分の設定ファイルにする。
そして、

sudo systemctl start aaaa

これで、

journalctl -u aaaa

を見てみると、ほら、does not exist に!
これで検証できる。


さて、does not exist にはなったのだが、。

rotating pattern: /tmp/aaaa/aaaa.log  hourly (12 rotations)
empty log files are rotated, old logs are removed
switching euid to 1000 and egid to 1000
considering log /tmp/aaaa/aaaa.log
  log /tmp/aaaa/aaaa.log does not exist -- skipping
switching euid to 0 and egid to 0

そう、su を書いたのだからこうなってほしかったのだよ、さっきも。
それはそれで納得できないし、ここでは切り替えたにもかかわらず does not exist だし。

 

直接 logrotate コマンドを実行すると成功するので、そのログの違いから探っていこう。

rotating pattern: /tmp/aaaa/aaaa.log  hourly (12 rotations)
empty log files are rotated, old logs are removed
switching euid to 1000 and egid to 1000
considering log /tmp/aaaa/aaaa.log
  Now: 2022-04-17 06:43
  Last rotated at 2022-04-17 02:00
  log needs rotating

色を付けたメッセージは skipping の少しあとにある。
やっぱり lstat() をなんとかしないといかんという結論になる。

 

ちなみにまだ status ファイルに記述がない場合はこういうログになる。

considering log /tmp/aaaa/aaaa.log
Creating new state

そして /var/lib/logrotate/status ファイルに追加されて、実際のログファイルには変更無し。
続けて実行しても、何もならなかった(1時間経っていないからか)。

 

そもそも、コマンドラインで実行すると成功して systemctl だと失敗するというなら、service に原因を求めるのが筋だろう。

GitHub のサンプルはこうだった。

https://github.com/logrotate/logrotate/blob/master/examples/logrotate.service

そして Ubuntu の方は下の方がこうなっていた。

PrivateDevices=true
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectSystem=full
RestrictRealtime=true

自分で書くときには指定したことがないオプションだ。
そしてこれらをコメントアウトして systemctl start すると実行するではないか! ローテートされるじゃないか!

もうちょっと前に気付いてもよかったんじゃないかと思わなくもないが、まあよかろう。
私は未来を愛しているのだ。


では、これらのオプションが何かを調べる。
hardening options らしい。

https://www.freedesktop.org/software/systemd/man/systemd.exec.html

PrivateTmp

If true, sets up a new file system namespace for the executed processes and mounts private /tmp/ and /var/tmp/ directories inside it that are not shared by processes outside of the namespace. This is useful to secure access to temporary files of the process, but makes sharing between processes via /tmp/ or /var/tmp/ impossible. If true, all temporary files created by a service in these directories will be removed after the service is stopped. Defaults to false.

ああ、そういうことね・・・。
/tmp に mount してから実行するので /tmp 以下に置いていたファイルは not exist になるということか。

 

/tmp にログをおいたのは、出来心だった。
ちょうどログを出力する実装をしているときに見たサイトが /tmp の下に置いていたのでまねしただけ。
それがこんな悲劇を生もうとは・・・。

いや、私も /var/log とかに格好良く(?)ログファイルを置きたいのだけど、権限が通常ユーザだと足りないので /tmp はちょうど良かったのだ。
仮にログファイルを置くなら、まだユーザディレクトリの中の方がましだったようだ。

2022/04/16

[linux]思ったより logrotate が難しい

現在進行形だ。

常時起動するアプリを作って、ログをファイルに保存するようにした。
アプリの起動時にログファイルを O_APPEND で開いて、あとはそこに書き込むだけである。

そうなると、起動している時間が長いとログファイルが肥大してしまって困ったことになるだろう。
直近の数日分だけあればよいのだが、アプリのログ出力をがんばる気力がない。

そういうときに使うのが logrotate コマンドだ。
そういうときじゃなくても、ログファイルを管理するのに便利だから覚えておきたいところだ。


logrotate 自体は単なるコマンドで、実行されたら設定ファイル置き場にある各アプリの設定ファイルに従ってログファイルを操作する。

誰かが実行しないと動かないので、そこは systemctl やら cron やらを使うようになっている。
Ubuntu 20.04 ではどちらにも仕掛けてあって、 systemctl のタイマーが動いているなら systemctl から、そうでなければ cron から実行するようになっている。

 

と、さも詳しげに書いているのだが、実はまだうまく動かせていない・・・。
直接 logrotate コマンドを動かすと自アプリのログファイルが整理されたのだが、自動に任せると何も行われていないのだ。


まず、 systemctl か cron かを見ていこう。

わかりやすいのは cron の方だと思う。
デフォルトのままなら /etc/cron.daily/logrotate にスクリプトがあるだろう。

$ cat /etc/cron.daily/logrotate 
#!/bin/sh

# skip in favour of systemd timer
if [ -d /run/systemd/system ]; then
    exit 0
fi

# this cronjob persists removals (but not purges)
if [ ! -x /usr/sbin/logrotate ]; then
    exit 0
fi

/usr/sbin/logrotate /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
    /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit $EXITVALUE

スクリプトの通り、cron で実行しない条件が2つある(青文字のところ)。

  • /run/systemd/system ディレクトリがある場合
  • /usr/sbin/logrotate という実行ファイルがない場合

手元の Lubuntu 20.04 はどちらも一致しないので、cron では動かないことになる。

 

では systemctl で動いてくれるのだろう。

$ systemctl list-timers logrotate
NEXT                        LEFT          LAST                        PASSED    UNIT            ACTIVATES       
Sun 2022-04-17 00:00:00 JST 2h 22min left Sat 2022-04-16 20:48:31 JST 48min ago logrotate.timer logrotate.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

改行されて見づらいが、これを書いているのが 2022-04-16 の 21時半くらいで、 list-timers の NEXT には 2022-04-17 となっているから、次回は明日の 0時に動いてくれるということなのだろう。
なので、タイマーとしては少なくとも動いていると考えて良かろう。

では、systemctl として logrotate が動いているのかどうか。

$ journalctl -u logrotate
...(略)...
Apr 10 00:00:14 あああ systemd[1]: Finished Rotate log files.
-- Reboot --
Apr 11 23:16:00 あああ systemd[1]: Starting Rotate log files...
Apr 11 23:16:01 あああ systemd[1]: logrotate.service: Succeeded.
Apr 11 23:16:01 あああ systemd[1]: Finished Rotate log files.
-- Reboot --
Apr 16 20:48:31 あああ systemd[1]: Starting Rotate log files...
Apr 16 20:48:32 あああ systemd[1]: logrotate.service: Succeeded.
Apr 16 20:48:32 あああ systemd[1]: Finished Rotate log files.

うーん、いま見ている Ubuntu は VirtualBox で動かしていて、あまり長い時間動作させないのだった。
よくわからんが、少なくともエラーにはなっていないので大丈夫じゃないかな。

ただ、起動時に動かしたことしかわからない。
そもそも動作する周期はどうなっているのか?

$ cat /etc/systemd/system/timers.target.wants/logrotate.timer 
[Unit]
Description=Daily rotation of log files
Documentation=man:logrotate(8) man:logrotate.conf(5)

[Timer]
OnCalendar=daily
AccuracySec=12h
Persistent=true

[Install]
WantedBy=timers.target

一日1回か。

しかしこの logrotate.timer にはコマンドも何もないので、service ファイルがあるのか。

$ find /etc/systemd/ -name "*logrotate*"
/etc/systemd/system/timers.target.wants/logrotate.timer

/etc/systemd にはないやん。

しかし /lib/systemd にはあった。

$ find /lib/systemd/ -name "*logrotate*"
/lib/systemd/system/logrotate.service
/lib/systemd/system/logrotate.timer

どうも、 /lib/systemd の方がオリジナルで、 /etc/systemd にもあればそちらが優先されるそうだ。

/usr/lib/systemd/system/と/etc/systemd/system/ - tsunokawaのはてなダイアリー
https://tsunokawa.hatenablog.com/entry/2018/10/05/013034

/lib/systemd の方は

ExecStart=/usr/sbin/logrotate /etc/logrotate.conf

となっていたので、systemctl としても logrotate は呼び出しているはずだ。


では、次は logrotate 自体を見ていこう。

/var/log には logrotate の付いたファイルがなかった。
その代わり、logrotate を実行したときの状態ファイルはあった。

$ ls -l /var/lib/logrotate/status
-rw-r--r-- 1 root root 1624 Apr 16 20:48 /var/lib/logrotate/status

たぶん VirtualBox を立ち上げたときの時刻だろう。 cat で見てもそのくらいの時間になっている項目がいくつかあった。

なので、systemctl から logrotate が実行されていないということはないと思う。
起動時だけ別ルートで呼び出すとかやっている可能性もなくはないが、あんまりそんなことはしないんじゃないかと思う。

 

うーん、 systemctl が起動したときには logrotate が動いている、 logrotate を単独で実行したときにはローテートが実行される、となると、 systemctl がタイマーのところで動いてくれていないということになるのか。


ならば再び systemctl に戻ろう。
timer はこの辺りだそうだ。

systemd/タイマー - ArchWiki
https://wiki.archlinux.jp/index.php/Systemd/%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC

logrotate.timer で実行に関係がありそうなのはこの辺りか。

[Timer]
OnCalendar=daily
AccuracySec=12h
Persistent=truen

OnCalendar はカレンダーイベントに合わせて動作するための設定項目。
まあ、これは見たままだろう。

 

AccuracySec はこのページにないな。

systemd.timer(5) — Arch manual pages
https://man.archlinux.org/man/systemd.timer.5

"accuracy"は「正確さ」。
値のデフォルト値は 1分。

Within this time window, the expiry time will be placed at a host-specific, randomized, but stable position that is synchronized between all local timer units.

などと書いてあるので、その範囲内でランダムな時間で実行するということか?
デフォルトなら 1分以内ということだろう。
そしてここの設定なら・・・12時間以内??

なるほど、ログを処理するだけだからあせることはないということか。
システムが暇なときにやってくれれば良いよ、というところなのだろう。

そして私がログがローテートしないと思ったのも、OnCalendar を "hourly" にして daemon-reload し、時間になるまで待って比較的すぐに確認していたからだろう。

毎時実行なのに 12時間以内にやればいいよとなったら、いつ実行されるかわからんわな。