Raspbianのinitramfsをカーネルのアップデートに追従するようにした
Raspberry PIでrootfsのReadOnly化やUSB OTGとかで試してきて、ちょっとだけinitramfs使ってやりたいことやれるようになった。
だけど、apt-get upgrade
でカーネルが更新されると、既存のinitramfsを作りなおさなければならないことに気がついた。
しばらくはカーネルの更新があるたびに手作業で作りなおしていたんだけど、これなんとかならんかな…と思っていた。
ある日PCのubuntuでパッケージの更新をしていた時に、流れてくる出力にinitramfsを作りなおしているらしき表示があることに気がついた。 で、もしかしてaptの更新で動くフックがあるんじゃないかと思って探してみた。
対象はRaspbian Busterの2019-07-10のやつ。
initramfs-toolsのフック
目的のフックは/etc/kernel/postinst.d/initramfs-tools
にあった。
中身は次のようになっていた。
#!/bin/sh -e version="$1" bootopt="" command -v update-initramfs >/dev/null 2>&1 || exit 0 # passing the kernel version is required if [ -z "${version}" ]; then echo >&2 "W: initramfs-tools: ${DPKG_MAINTSCRIPT_PACKAGE:-kernel package} did not pass a version number" exit 2 fi # exit if kernel does not need an initramfs if [ "$INITRD" = 'No' ]; then exit 0 fi # absolute file name of kernel image may be passed as a second argument; # create the initrd in the same directory if [ -n "$2" ]; then bootdir=$(dirname "$2") bootopt="-b ${bootdir}" fi # avoid running multiple times if [ -n "$DEB_MAINT_PARAMS" ]; then eval set -- "$DEB_MAINT_PARAMS" if [ -z "$1" ] || [ "$1" != "configure" ]; then exit 0 fi fi # we're good - create initramfs. update runs do_bootloader # shellcheck disable=SC2086 INITRAMFS_TOOLS_KERNEL_HOOK=1 update-initramfs -c -k "${version}" ${bootopt} >&2
ざっと見た感じ、この仕組みに乗っかるならupdate-initramfs
で作ったほうが良さそう(自分はmkinitramfs
コマンドで作っていた)。
とりあえずmkinitramfs
で作っておいたinitrd.img
をupdate-initramfs -c -k $(uname -r)
で作りなおした。
ファイル名はinitrd.img-x.y.z-v7+
のようになるので、/boot/config.txt
のinitramfs
設定も修正しておいた。
また、スクリプトを見るに$INITRD
の値が設定されていないと更新はキャンセルされそう。
そこでこの値を設定する方法を探した。これは/etc/default/raspberrypi-kernel
にあった。
中身こんな感じ。
# Defaults for raspberrypi-kernel # Uncomment the following line to enable generation of # /boot/initrd.img-KVER files (requires initramfs-tools) #INITRD=Yes # Uncomment the following line to enable generation of # /boot/initrd(7).img files (requires rpi-initramfs-tools) #RPI_INITRD=Yes
rpi-initramfs-tools
ってなんだ?と思ったけど、特にそういうパッケージがあるわけではないみたい。
今のところ取り置きされているような感じなのだろうか…?とりあえずINITRD=Yes
のコメントアウトを解除した。
その状態でapt-get upgrade
でカーネルの更新をしてやると、出力に次のような表示が出てきて新しいカーネルバージョンでinitramfsが作りなおされていることが解る。
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66+ /boot/kernel.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66+ /boot/kernel.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7+ /boot/kernel7.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7+ /boot/kernel7.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66-v7+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7l+ /boot/kernel7l.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7l+ /boot/kernel7l.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66-v7l+
実際に/boot
の中身を見てみると
$ ls /boot/initrd.img-* /boot/initrd.img-4.19.66+ /boot/initrd.img-4.19.66-v7+ /boot/initrd.img-4.19.66-v7l+
となって、新たなバージョンのinitramfsができていることがわかった(手で作った旧バージョンは削除されていた)。
ただし、Raspbianの場合このままだと/boot/config.txt
のinitramfs
設定は古いままになってしまうので、
そこはひと手間かけてやる必要がありそう。
/etc/kernel/postinst.d/initramfs-tools
をカスタマイズ
ひとまず/etc/kernel/postinst.d/initramfs-tools
の末尾に次の処理を追加した。
# replace /boot/config.txt current_version="$(uname -r)" old_version_number=${current_version%-*} if echo $version | grep -Pq "^\d+\.\d+\.\d+\+$"; then old_version="$old_version_number+" else old_version="$old_version_number-${version#*-}" fi sed -i "s/initramfs initrd.img-$old_version/initramfs initrd.img-$version/g" "$bootdir/config.txt"
現在動作しているOSのバージョンを調べて更新対象のライブラリバージョンで/boot/config.txt
のinitramfs
設定を書き換える。
これで改めて確認し、Busterではアップデートに追従できるようになった。
Stretchに対応する
上記の修正をStretchでもやってみたところ、次のような出力が表示されて作りなおしに失敗した。
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66+ /boot/kernel.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66+ /boot/kernel.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7+ /boot/kernel7.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7+ /boot/kernel7.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66-v7+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7l+ /boot/kernel7l.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7l+ /boot/kernel7l.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66-v7l+ WARNING: missing /lib/modules/4.19.66-v7l+ Ensure all necessary drivers are built into the linux image! depmod: ERROR: could not open directory /lib/modules/4.19.66-v7l+: No such file or directory depmod: FATAL: could not search modules: No such file or directory depmod: WARNING: could not open /var/tmp/mkinitramfs_L5yf77/lib/modules/4.19.66-v7l+/modules.order: No such file or directory depmod: WARNING: could not open /var/tmp/mkinitramfs_L5yf77/lib/modules/4.19.66-v7l+/modules.builtin: No such file or directory *** 中略 *** Processing triggers for initramfs-tools (0.130) ... ln: failed to create hard link '/boot/initrd.img-4.19.66-v7l+.dpkg-bak' => '/boot/initrd.img-4.19.66-v7l+': Operation not permitted cp: error writing '/boot/initrd.img-4.19.66-v7l+.dpkg-bak': No space left on device dpkg: error processing package initramfs-tools (--configure): subprocess installed post-installation script returned error exit status 1 Processing triggers for libc-bin (2.24-11+deb9u4) ... Errors were encountered while processing: initramfs-tools E: Sub-process /usr/bin/dpkg returned an error code (1)
メッセージを読みながら調べてみると、更新処理がRaspberry PI4用のv7l+系のinitramfsを作ろうとするが、
StretchはRaspberry PI4に対応していないのでv7l+用のモジュールライブラリが見つからずにdepmodのエラーが出ている。失敗に終わったinitrd.img-4.19.66-v7l+ファイルはそのまま/boot
に残留する。
更にバックアップを取って新しく作ろうとするので、/boot
パーティションのディスクスペースが圧迫されて作成できずに落ちる…ということらしい。
そこで/etc/kernel/postinst.d/initramfs-tools
の$INITRD
のチェックの次に、下のような処理を追加した。
# skip ignore version lib if [ ! -e "/lib/modules/$version" ]; then echo "Not exist module libraries. [$version]" exit 0 fi
これでv7l+のような対応していないライブラリバージョンの生成はスキップされる。
また、/boot
パーティションの空き容量が深刻なので、/usr/sbin/update-initramfs
のbackup_initramfs
とbackup_booted_initramfs
の方にも手を入れた。
# backup initramfs while running backup_initramfs() { [ ! -r "${initramfs}" ] && return 0 #initramfs_bak="${initramfs}.dpkg-bak" initramfs_bak="/var/tmp/$(basename initramfs).dpkg-bak" [ -r "${initramfs_bak}" ] && rm -f "${initramfs_bak}" #ln -f "${initramfs}" "${initramfs_bak}" \ # || cp -a "${initramfs}" "${initramfs_bak}" mv "${initramfs}" "${initramfs_bak}" verbose "Keeping ${initramfs_bak}" } # keep booted initramfs backup_booted_initramfs() { #initramfs_bak="${initramfs}.dpkg-bak" initramfs_bak="/var/tmp/$(basename initramfs).dpkg-bak" *** 以下略 ***
デフォルトでは/boot
パーティションに作成されていたバックアップファイルを/var/tmp
に作成するように変更し、
既存バージョンのファイルを直接バックアップファイルにすることで、既存バージョンのファイルと、バックアップファイル、新しく作成するファイルで一時的に3つできてしまうことを避けるようにした。
これでapt-get upgrade
でカーネルの更新をして結果は次のようになった。
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66+ /boot/kernel.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66+ /boot/kernel.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7+ /boot/kernel7.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7+ /boot/kernel7.img /etc/kernel/postinst.d/initramfs-tools: update-initramfs: Generating /boot/initrd.img-4.19.66-v7+ run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 4.19.66-v7l+ /boot/kernel7l.img run-parts: executing /etc/kernel/postinst.d/initramfs-tools 4.19.66-v7l+ /boot/kernel7l.img /etc/kernel/postinst.d/initramfs-tools: Not exist module libraries. [4.19.66-v7l+] ** 以下省略 **
4.19.66-v7l+
用のライブラリは存在しないのでスキップされ、/boot
パーティション内でファイル容量食い過ぎることもないので、エラーになることなくinitramfsの更新までできた。
パッチファイル
RaspbianでOS再起動せずにネットワーク再起動で設定反映できるようにした
Raspbianのいつのバージョンだったか忘れたけど、固定IPアドレスの設定を/etc/dhcpcd.conf
に行うようになったあたりで、変更したネットワーク設定をサービス再起動で適用する方法がよくわからなくなった。
とりあえずdhcpcd.conf
で設定しているのだからdhcpcdを再起動してみたが反映されない。
$ sudo systemctl restart dhcpcd Warning: The unit file, source configuration file or drop-ins of dhcpcd.service changed on disk. Run 'systemctl daemon-reload' to reload units.
警告が出ているので、daemon-reload
を行ってからdhcpcdを再起動してみたが、警告がでなくなっただけで反映されない。
$ sudo systemctl daemon-reload $ sudo systemctl resart dhcpcd
networkingを再起動してみても反映されない。
$ sudo systemctl restart networking
OS再起動すれば反映されるけど、ネットワーク設定の変更だけでOSの再起動したくないので、何か手はないかと調べていたらフォーラムで次の記事を見つけた。
どうやらip addr flush dev
で一度インターフェイスの設定を飛ばしてあげれば良いらしい。
フォーラムの記事ではPythonでスクリプト書かれていたけど、参考にしてシェルスクリプトでコマンドを作ってみた。
restart-resize コマンド
全てのインターフェイスの再起動だけではなくて、指定したインターフェイスだけの再起動もできるようにした。
書いたスクリプトは次の通り。
#!/bin/bash # # restart-network applies network interface configuration changes without restarting the OS. # This implementation is based on the method described in the following forum article: # # https://lb.raspberrypi.org/forums/viewtopic.php?t=199860 # # [Usage] # # * When restarting all network interfaces (the lo interface is excluded) # # $ sudo restart-network # # * When restarting the specified network interface # # $ sudo restart-network eth0 # check_privilage() { if [ "`whoami`" != "root" ]; then echo "Require root privilege" exit 1 fi } stop_dhcpcd() { systemctl daemon-reload systemctl stop dhcpcd } flush_ip_address() { ip addr flush dev $1 } flush_ip_addresses() { DEVICES=(`ls /sys/class/net`) for DEVICE in ${DEVICES[@]} do if [ "$DEVICE" != "lo" ]; then flush_ip_address $DEVICE fi done } start_dhcpcd() { systemctl start dhcpcd } check_privilage TARGET="all" if [ $# -eq 1 ]; then TARGET=$1 fi stop_dhcpcd if [ $TARGET == "all" ]; then flush_ip_addresses else flush_ip_address $TARGET fi start_dhcpcd
これを使って、全てのインターフェイスを再起動する場合は次のように実行する。
$ sudo restart-network
指定したインターフェイスだけ再起動する場合は次のように実行する。
$ sudo restart-network eth0
これでいちいちOS再起動しなくて良くなった。
Raspberry PIでNTPの同期が取れてからアプリケーションのサービスを起動するようにした
Raspberry PIにはRTC(リアルタイムクロック)が搭載されていないので、ボード上に日時を保存しておいてOSのシステム時計に反映する方法はデフォルトではない。
ただ、昨今のRaspbianはsystemd-timesyncdによって、ファイルシステムに最後の日時を保存しておいてそこから復元し、その後NTPで同期するみたいな動きになっている。なので、OSを起動すると毎回とりあえず1970年から開始…みたいな感じにはならない。
systemd-timesyncd はネットワークを介してシステム時刻を同期させるために追加されたデーモンです。 Raspberry Pi や組み込みデバイスなどの RTC を載せてないシステムのために、新しい NTP の同期が取得される度にディスクに現在の時刻を保存し、それを使って起動時にシステム時刻を修正することができ、時刻が常に正しいわけではないときでも、それらのシステムで時刻がモノトニックに進むことを保証します。
ただし、これだと自分で作ったアプリケーションをいつ起動するのかによってシステム時刻の正確な日時が取れるタイミングが微妙な感じになる。
サンプルアプリケーションで検証してみる
たとえば、次のようなアプリケーションを用意する。起動すると1秒おきに現在時刻をログファイルに記録する仕様になっている。
from datetime import datetime from logging import getLogger, basicConfig, DEBUG import time logger = getLogger(__name__) basicConfig(level=DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', filename='./app.log') try: while True: logger.info("current:%s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S")) time.sleep(1) except KeyboardInterrupt: pass
これを起動順は指定せずに、単純にsystemdにサービスとして登録する。systemd用の設定ファイルを/etc/systemd/system/myapp.service
として次のように用意する。
[Unit] Description = My Application [Service] ExecStart = /home/pi/app/venv/bin/python app.py WorkingDirectory = /home/pi/app User=pi Restart=always Type=simple [Install] WantedBy = multi-user.target
これをsystemctl
で有効化する。
$ sudo systemctl enable myapp.service
OSを起動してアプリケーションが起動すると次のようなログができあがる。
2019-08-14 11:25:04,274 __main__ INFO current:2019-08-14 11:25:04 2019-08-14 11:25:05,278 __main__ INFO current:2019-08-14 11:25:05 2019-08-14 11:25:06,282 __main__ INFO current:2019-08-14 11:25:06 2019-08-14 11:25:07,285 __main__ INFO current:2019-08-14 11:25:07 2019-08-14 11:25:08,288 __main__ INFO current:2019-08-14 11:25:08 2019-08-14 11:25:09,291 __main__ INFO current:2019-08-14 11:25:09 2019-08-14 11:25:10,294 __main__ INFO current:2019-08-14 11:25:10 *** 中略 *** 2019-08-14 11:25:25,405 __main__ INFO current:2019-08-14 11:25:25 2019-08-14 11:25:26,409 __main__ INFO current:2019-08-14 11:25:26 2019-08-14 11:25:27,412 __main__ INFO current:2019-08-14 11:25:27 2019-08-14 11:25:28,415 __main__ INFO current:2019-08-14 11:25:28 2019-08-14 11:25:29,418 __main__ INFO current:2019-08-14 11:25:29 2019-08-14 11:25:30,427 __main__ INFO current:2019-08-14 11:25:30 2019-08-14 11:27:48,380 __main__ INFO current:2019-08-14 11:27:48 2019-08-14 11:27:49,383 __main__ INFO current:2019-08-14 11:27:49 2019-08-14 11:27:50,386 __main__ INFO current:2019-08-14 11:27:50 2019-08-14 11:27:51,389 __main__ INFO current:2019-08-14 11:27:51 2019-08-14 11:27:52,398 __main__ INFO current:2019-08-14 11:27:52
ログのタイムスタンプを見ていくと、11:25:04に起動したアプリケーションがログを記録して行き、11:25:30のあたりでNTPによってシステム時計が調整されて、つぎから11:27:48として正確な時刻で記録されていく。
これだと、起動してからNTPの同期が完了するまでの間のdatetime.now()
やログのタイムスタンプはあてにならないことになる。
アプリケーションの起動をsystemd-timesyncdの後にしてみる
NTPの同期をするのがsystemd-timesyncdなので、アプリケーションのsystemdの設定を次のように、After=systemd-timesyncd.service
と起動順の指定をしてサービス登録する。
これでsystemd-timesyncdサービスが起動してからアプリケーションが起動するようになる。
[Unit] Description = My Application After=systemd-timesyncd.service [Service] ExecStart = /home/pi/app/venv/bin/python app.py WorkingDirectory = /home/pi/app User=pi Restart=always Type=simple [Install] WantedBy = multi-user.target
が、これで改めて記録したログを観察すると次のようになる。
2019-08-14 11:39:33,913 __main__ INFO current:2019-08-14 11:39:33 2019-08-14 11:39:34,917 __main__ INFO current:2019-08-14 11:39:34 2019-08-14 11:39:35,921 __main__ INFO current:2019-08-14 11:39:35 *** 中略 *** 2019-08-14 11:39:57,052 __main__ INFO current:2019-08-14 11:39:57 2019-08-14 11:39:58,055 __main__ INFO current:2019-08-14 11:39:58 2019-08-14 11:39:59,059 __main__ INFO current:2019-08-14 11:39:59 2019-08-14 11:40:00,063 __main__ INFO current:2019-08-14 11:40:00 2019-08-14 11:43:03,507 __main__ INFO current:2019-08-14 11:43:03 2019-08-14 11:43:04,510 __main__ INFO current:2019-08-14 11:43:04 2019-08-14 11:43:05,514 __main__ INFO current:2019-08-14 11:43:05 2019-08-14 11:43:06,517 __main__ INFO current:2019-08-14 11:43:06 2019-08-14 11:43:07,521 __main__ INFO current:2019-08-14 11:43:07
11:39:33に起動してログを取り始め、11:40:00のあたりでNTPの同期が反映されて、11:43:03から正確な時刻で記録されていく。
syslogみるとmyapp.serviceの起動より後にsystemd-timesyncdで時刻同期されているのがわかる。
Aug 14 11:39:31 raspberrypi systemd[1]: Started My Application. Aug 14 11:39:31 raspberrypi systemd[1]: Started Daily man-db regeneration. Aug 14 11:39:31 raspberrypi systemd[1]: Reached target Timers. Aug 14 11:39:31 raspberrypi systemd[1]: Started triggerhappy global hotkey daemon. Aug 14 11:39:31 raspberrypi systemd[1]: Started System Logging Service. *** 中略 *** Aug 14 11:43:02 raspberrypi systemd-timesyncd[274]: Synchronized to time server for the first time 133.243.238.243:123 (ntp.nict.jp).
これはsystemd-timesyncdの起動タイミングとNTPで同期されるタイミングが異なるためで、NTPが同期されたタイミングを正しく待つ必要がある。
NTPの同期が完了されるまで待ってアプリケーションを起動する
これを実現するためにwait-timesyncを作った。
systemd-timesyncdが同期したらタイムスタンプを更新する/var/lib/systemd/clock
ファイルの更新日時を監視するようになっている。
#!/bin/bash TARGET=${WAIT_CLOCK_TARGET:-"/var/lib/systemd/clock"} TIMEOUT=${WAIT_TIMEOUT:-60} CHANGE=0 LAST_TS=`stat -c %Y $TARGET` for i in `seq 1 $TIMEOUT` ; do TS=`stat -c %Y $TARGET` if [ $LAST_TS != $TS ]; then CHANGE=1 echo "timesyncd clock updated [$i sec waited]" break fi sleep 1 done if [ $CHANGE == 0 ]; then echo "timeout watch timesyncd clock update" fi
これを次の/etc/systemd/system/wait-timesync.service
でsystemdにサービス登録する。
[Unit] Description = Wait timesyncd After=systemd-timesyncd.service [Service] ExecStart = /usr/local/bin/wait-timesync EnvironmentFile = /etc/default/wait-timesync Type=oneshot [Install] WantedBy = multi-user.target
監視対象のclockファイルは場所が違う場合があるので、次のように環境変数で指定するようにしてある。同期待ちのタイムアウト(秒)も環境変数で指定する。これは、/etc/default/wait-timesync
ファイルとして設置し、参照する。
WAIT_CLOCK_TARGET=/var/lib/systemd/clock WAIT_TIMEOUT=60
これを利用してmyapp.serviceのsystemd設定を変更し、After=wait-timesync.service
として再設定する。
[Unit] Description = My Application After=wait-timesync.service [Service] ExecStart = /home/pi/app/venv/bin/python app.py WorkingDirectory = /home/pi/app User=pi Restart=always Type=simple [Install] WantedBy = multi-user.target
改めてOSを起動してログファイルを確認すると次のようになり、途中で同期されてタイムスタンプが変わることはなくなる。
2019-08-14 11:49:08,968 __main__ INFO current:2019-08-14 11:49:08 2019-08-14 11:49:09,971 __main__ INFO current:2019-08-14 11:49:09 2019-08-14 11:49:10,975 __main__ INFO current:2019-08-14 11:49:10 2019-08-14 11:49:11,980 __main__ INFO current:2019-08-14 11:49:11 2019-08-14 11:49:12,984 __main__ INFO current:2019-08-14 11:49:12 2019-08-14 11:49:13,989 __main__ INFO current:2019-08-14 11:49:13 2019-08-14 11:49:14,993 __main__ INFO current:2019-08-14 11:49:14 2019-08-14 11:49:15,997 __main__ INFO current:2019-08-14 11:49:15
このとき、syslogを見ると次のようになっている。
Aug 14 11:46:04 raspberrypi systemd[1]: Starting Wait timesyncd... Aug 14 11:46:04 raspberrypi systemd[1]: Starting OpenBSD Secure Shell server... Aug 14 11:46:04 raspberrypi systemd[1]: Starting Permit User Sessions... *** 中略 *** Aug 14 11:49:06 raspberrypi systemd-timesyncd[281]: Synchronized to time server for the first time 133.243.238.243:123 (ntp.nict.jp). Aug 14 11:49:07 raspberrypi wait-timesync[469]: timesyncd clock updated [21 sec waited] Aug 14 11:49:07 raspberrypi systemd[1]: wait-timesync.service: Succeeded. Aug 14 11:49:07 raspberrypi systemd[1]: Started Wait timesyncd. Aug 14 11:49:07 raspberrypi systemd[1]: Started My Application.
アプリケーションの起動がwait-timesyncの完了後になり、NTPの同期後になっていることがわかる。ちなみにNTPの同期にはwait-timesyncが起動してから21秒かかっていることが分かる。
アプリケーションの起動が待たされることにはなるけど、RTCをRaspberry PIに載せるまでは行かないけどタイムスタンプは正確にしときたいみたいな用途に使えると思う。
極力バックエンドを書かずに異なるドメインのWebサイトからデータを取得するCORS Proxyというのを作った
作ったと言っても、実際コードを書いたのは2年近く前だったのだけど。
当時書いてたアプリのために作っておいたのをずっと放置していたが、最近ちょっと使う用途があったので久しぶりに触ったら何もかも忘れてて(README書いてなかった)えらい苦労した。改めて使い方調べながらREADMEまとめたので、ついでに気づいたバグとか直してGitHubで公開した。
CORSとは
Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使って、あるオリジン (ドメイン) で動いているウェブアプリケーションが、異なるオリジンのサーバーのリソースにアクセスできるようにする仕組み
雑に説明すると、サーバ側でAccess-Control-Allow-Origin
ヘッダーをレスポンスで返してなんやかんやするやつ。
今回作ったやつは追加のリクエストヘッダーをいくつか必要としているので、Access-Control-Allow-Headers
ヘッダーのレスポンスやプリフライトリクエスト(OPTIONSメソッドのリクエストが本命の前に実行される)への対応も必要になっている。
使い方
環境準備
Herokuで動かすようにしてあるので、リポジトリをクローンしたらHerokuアプリケーション作って、認証用の環境変数を設定してデプロイする。
$ git clone https://github.com/ushiboy/cors-proxy $ cd cors-proxy $ heroku create
認証用のキーはbin/generate_key
でランダムに生成する。
$ bin/generate_key [Key]: tV4VWOcq0HbJ... [Digest]: $2y$10$do7go...
認証用の環境変数はAUTH_KEY_DIGEST
に生成したDigest
の値を、ALLOW_ORIGIN
に許可するオリジン(複数指定する場合はカンマで区切る)を設定する。
$ heroku config:set AUTH_KEY_DIGEST='$2y$10$do7go...' ALLOW_ORIGIN=http://myapp.com
プッシュしてデプロイする。
$ git push heroku master
Webフロントエンド側からの利用
デプロイ先のHerokuアプリケーションのURLに、生成した認証用のKey
の方をAuthorization
リクエストヘッダーにBearer
としてつけて、クエリストリングのq
パラメータに取得対象のURLを指定してリクエストを発行する。
const q = encodeURIComponent('http://www.example.com/data.txt'); const result = await fetch(`https://<your_app_name>.herokuapp.com/?q=${q}`, { headers: { 'Authorization': `Bearer tV4VWOcq0HbJ...` } });
これでプリフライトリクエストのあとに正しく取得ができれば動作はOK。
取得対象が古のCP932とかCP51932とかUTF-8ではない場合はX-From-Charset
ヘッダーをつけてリクエストすることで、UTF-8にエンコードして取得できる。
const q = encodeURIComponent('http://www.example.com/data.txt'); const result = await fetch(`https://<your_app_name>.herokuapp.com/?q=${q}`, { headers: { 'Authorization': `Bearer tV4VWOcq0HbJ...`, 'X-From-Charset': 'CP51932' } });
X-From-Charset
に指定できる文字コードの種類はPHPのmbstringがサポートしてるやつ。
補足
認証キーとオリジンでアクセス制限するようにしているけど、これは気休め程度。Webフロントエンド以外のところでやろうと思えばどうにでもできるので、フロントエンドのコードにベタ書きして使うかは要検討。
自分はHerokuアプリケーションのURLと認証キーをローカルストレージに設定するようにしてコードにベタ書きはせずに使ってる。
GETしかできないものなので、そこまで気にしなくても良いかも...という気もしないでもない。
とりあえずちょっと外部Webサイトのリソースも使いたいWebアプリ書くとかだったら、フロントエンド側はGithub Pagesでホストしてそこから利用するとかできると思う。
Raspberry PI ZeroのUSB OTGで遊んだ
Raspberry PI Zeroのセットアップで、電源供給用じゃない方のマイクロUSBポートとホストPCを繋いで仮想ネットワーク経由で接続する云々の情報をみて、どういう仕組みでやっているんだろうと調べてみたら、Raspberry PI ZeroではUSB OTG(On-The-Go)が使えるということを知った。
USB OTG (On-The-Go)
USB OTGはAndroidのUSBポートにマウスとかキーボード繋げばマウスやキーボードとして動くけど、同じポートでPCに繋いだ時はPC側でストレージとして見えるみたいなアレっぽい。
USB On-The-Go(略してUSB OTG)は、USB機器どうしを直接接続するインタフェース規格である。パソコン等をホストとせずに、動作時にホスト機器を動的に切り替える機能を拡張したもの。 IEEE 1394のように直接接続できるので、いろいろな機器に応用できる。
調べてみたら手持ちのRaspberry PI B系では構造上できないようなので、Raspberry PI Zero WHを買ってみた。送料とか色々考えてとりあえずAmazonでポチリ。
Raspberry Pi Zero WH GPIOピンヘッダー ハンダ付け済み Wi-Fi & Bluetooth&ヒートシンク付
- 出版社/メーカー: Raspberrypi
- メディア: エレクトロニクス
- この商品を含むブログを見る
シリアル通信したり、ネットワークにしたり、キーボードにしたりと色々できるみたいだけど、とりあえず今回はストレージとして使う方法を試してみた。
USB OTGでマスストレージ
環境は以前に作成したRaspbian Stretch Liteの2019-04-08バージョンのパーティションをいじって、bootとrootfsの他にextrafsを追加したもの。今出ているBusterの2019-06-20バージョンはとりあえず保留。
環境準備
まずはUSB OTGを利用するために,/boot/config.txt
に次のUSB OTGの有効化を追加する。
dtoverlay=dwc2
反映するためにOSを再起動する。
続いて、ストレージとして利用するためのパーティションを作成する。イメージファイルとかでも良いらしいけど、今回は専用パーティションを作って行う。
現状のパーティションが次のとおり。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 7.4 GiB, 7932477440 bytes, 15493120 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xc1dc39e5 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 96042 87851 42.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 6389759 6291456 3G 83 Linux /dev/mmcblk0p3 6389760 8388607 1998848 976M 83 Linux
これに1GBのFAT32パーティションを追加して次の様にした。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 7.4 GiB, 7932477440 bytes, 15493120 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xc1dc39e5 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 96042 87851 42.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 6389759 6291456 3G 83 Linux /dev/mmcblk0p3 6389760 8388607 1998848 976M 83 Linux /dev/mmcblk0p4 8388608 10485759 2097152 1G b W95 FAT32
パーティションを切った後にOSを再起動して、vfatでフォーマットする。
$ sudo mkfs -t vfat /dev/mmcblk0p4
これでパーティションの準備は完了。
マスストレージの有効化と解除
環境を準備して次のコマンドを実行すると接続しているホストPC側にUSBストレージとして認識させることができる。 パラメータのオプションは linux/mass-storage.txt at master · torvalds/linux · GitHub を参考にした。
色々試していたらWindowsでUSBストレージとして認識させるにはremovable=y
が必要だった。
$ sudo modprobe g_mass_storage file=/dev/mmcblk0p4 removable=y
lsmodで確認すると次の様に表示される。
$ lsmod | grep g_mass_storage g_mass_storage 4651 0 usb_f_mass_storage 38658 2 g_mass_storage libcomposite 49673 2 g_mass_storage,usb_f_mass_storage
dmesgを確認すると次のようなログが確認できる。
[ 46.534849] Mass Storage Function, version: 2009/09/11 [ 46.534873] LUN: removable file: (no medium) [ 46.535068] LUN: removable file: /dev/mmcblk0p4 [ 46.535077] Number of LUNs=1 [ 46.537657] g_mass_storage gadget: Mass Storage Gadget, version: 2009/09/11 [ 46.537675] g_mass_storage gadget: userspace failed to provide iSerialNumber [ 46.537681] g_mass_storage gadget: g_mass_storage ready [ 46.537695] dwc2 20980000.usb: bound driver g_mass_storage [ 46.654687] dwc2 20980000.usb: new device is high-speed [ 47.346817] dwc2 20980000.usb: new device is high-speed [ 47.414698] dwc2 20980000.usb: new device is high-speed [ 47.632787] dwc2 20980000.usb: new address 1 [ 47.657761] g_mass_storage gadget: high-speed config #1: Linux File-Backed Storage
マスストレージを解除するには次のコマンドを実行する。
$ sudo modprobe -r g_mass_storage
OS起動時にマスストレージを有効にしてみる
準備が整ったので、OS起動時に自動的にマスストレージを有効にできるようにしてみる。
最初に思いつくのは単純な方法で/etc/rc.local
に起動コマンドを書いておくというもの。
/etc/rc.local
を使った方法
/etc/rc.local
を次の様に編集する。
#!/bin/sh -e # # rc.local # # This script is executed at the end of each multiuser runlevel. # Make sure that the script will "exit 0" on success or any other # value on error. # # In order to enable or disable this script just change the execution # bits. # # By default this script does nothing. # Print the IP address _IP=$(hostname -I) || true if [ "$_IP" ]; then printf "My IP address is %s\n" "$_IP" fi # for MASS Storage modprobe g_mass_storage file=/dev/mmcblk0p4 removable=y exit 0
これで再起動して動作確認。
dmesgでマスストレージが有効化されたタイミングを見てみる。
[ 25.047610] Mass Storage Function, version: 2009/09/11 [ 25.047631] LUN: removable file: (no medium) [ 25.047818] LUN: removable file: /dev/mmcblk0p4 [ 25.047827] Number of LUNs=1 [ 25.061717] g_mass_storage gadget: Mass Storage Gadget, version: 2009/09/11 [ 25.061738] g_mass_storage gadget: userspace failed to provide iSerialNumber [ 25.061744] g_mass_storage gadget: g_mass_storage ready [ 25.061758] dwc2 20980000.usb: bound driver g_mass_storage [ 25.179253] dwc2 20980000.usb: new device is high-speed [ 25.558496] dwc2 20980000.usb: new device is high-speed [ 25.684040] dwc2 20980000.usb: new device is high-speed [ 25.761661] dwc2 20980000.usb: new address 1 [ 25.785904] g_mass_storage gadget: high-speed config #1: Linux File-Backed Storage
/etc/rc.local
だとOS起動の最後のほうで動くので、25秒くらいかかってるのがわかる。
これだと、ホストPC側に繋いでからUSBストレージとして認識されるまでが結構待たされる気がする。
systemdでサービス化する方法
次にsystemdでoneshotなサービスを作って実行する方法でやってみる。
/etc/rc.local
は元に戻しておいて、/lib/systemd/system/storage.service
を次の様に作る。
[Unit] Description = MASS Storage Service After = local-fs.target [Service] ExecStart = /sbin/modprobe g_mass_storage file=/dev/mmcblk0p4 Type = oneshot [Install] WantedBy = multi-user.target
作ったサービスを有効化して、再起動する。
$ sudo systemctl enable storage.service
再びdmesgでマスストレージが有効化されたタイミングを見てみる。
[ 15.075042] Mass Storage Function, version: 2009/09/11 [ 15.075070] LUN: removable file: (no medium) [ 15.075312] LUN: file: /dev/mmcblk0p4 [ 15.075326] Number of LUNs=1 [ 15.083453] g_mass_storage gadget: Mass Storage Gadget, version: 2009/09/11 [ 15.083479] g_mass_storage gadget: userspace failed to provide iSerialNumber [ 15.083488] g_mass_storage gadget: g_mass_storage ready [ 15.083507] dwc2 20980000.usb: bound driver g_mass_storage [ 15.203126] dwc2 20980000.usb: new device is high-speed [ 15.225530] uart-pl011 20201000.serial: no DMA platform data [ 15.582358] dwc2 20980000.usb: new device is high-speed [ 15.707818] dwc2 20980000.usb: new device is high-speed [ 15.785478] dwc2 20980000.usb: new address 1 [ 15.809602] g_mass_storage gadget: high-speed config #1: Linux File-Backed Storage
これだと15秒くらいで有効化されている。だいたい10秒くらい縮まったか。
initramfsで有効化する方法
そもそもSDカードのパーティションへのアクセスとmodprobeが使えるタイミングなら良いわけで、initramfsでやってしまえば良いのでは...?と思ったのでやってみる。
systemdのサービスは無効化しておく。
$ sudo systemctl disable storage.service
そして、/etc/initramfs-tools/hooks/usbstorage
を作る。
#!/bin/sh . /usr/share/initramfs-tools/scripts/functions . /usr/share/initramfs-tools/hook-functions manual_add_modules g_mass_storage
作った/etc/initramfs-tools/hooks/usbstorage
に実行権限を与えて、オーナーを変えておく。
$ sudo chmod +x /etc/initramfs-tools/hooks/usbstorage $ sudo chown root.root /etc/initramfs-tools/hooks/usbstorage
次に/etc/initramfs-tools/scripts/local-bottom/usbstorage
を作る。
#!/bin/sh PREREQ="" prereqs() { echo "$PREREQ" } case $1 in prereqs) prereqs exit 0 ;; esac . /scripts/functions log_begin_msg "Setting up usbstorage:" modprobe g_mass_storage file=/dev/mmcblk0p4 removable=y log_end_msg "Done..." exit 0
こちらも実行権限を与えて、オーナーを変えておく。
$ sudo chmod +x /etc/initramfs-tools/scripts/local-bottom/usbstorage $ sudo chown root.root /etc/initramfs-tools/scripts/local-bottom/usbstorage
これらを使ってinitramfsを作る。
$ sudo mkinitramfs -o /boot/initrd.gz
/boot/config.txt
を編集し、末尾に次のinitramfs有効化を追加する。
# enable initramfs initramfs initrd.gz
改めて再起動して動作を確認する。
dmesgを見ると,
[ 4.672923] Mass Storage Function, version: 2009/09/11 [ 4.672945] LUN: removable file: (no medium) [ 4.673187] LUN: removable file: /dev/mmcblk0p4 [ 4.673318] Number of LUNs=1 [ 4.679278] g_mass_storage gadget: Mass Storage Gadget, version: 2009/09/11 [ 4.679300] g_mass_storage gadget: userspace failed to provide iSerialNumber [ 4.679308] g_mass_storage gadget: g_mass_storage ready [ 4.679328] dwc2 20980000.usb: bound driver g_mass_storage [ 4.795750] dwc2 20980000.usb: new device is high-speed [ 5.172263] dwc2 20980000.usb: new device is high-speed [ 5.250174] dwc2 20980000.usb: new address 1 [ 5.274662] g_mass_storage gadget: high-speed config #1: Linux File-Backed Storage
起動してから5秒くらいで有効化されるようになった。30秒近く待たされるより速くなったと思う。
Webプログラマの手持ちスキルを駆使してRaspberry PIに電源OFFボタンをつけた
回路組んで基盤作ったりとかできないので、Raspberry PI用のLCD購入して、UI作って電源OFFボタンをつけた。
UIはLinuxのGUIアプリケーションを書く...のは回避して、ブラウザでできるようにして自分の手持ちスキル側に寄せた(Electron使うって手もあったかもだけど)。
使ったLCD
Quimatの3.5インチタッチスクリーン。
仕組み
localhostからのみアクセス可能にしたWebサーバをサービスとして動かしつつ、 GUIでユーザーを自動ログインにしておいて、電源投入されたらブラウザをkioskモードで起動してUI表示する。
UIのPower Offボタンをクリック(タッチ)したらWEB API投げてWebサーバ側でシャットダウンする。
GUI周りの環境構築
Raspberry PIのフォーラムの記事を参考にした。
ウィンドウマネージャはRPDやLXDEではなく一番シンプルなOpenboxを利用した。
Xサーバインストール
まずはフォーラムの記事の「Advanced - Custom Desktop Environment using Openbox WM」に従ってXサーバを推奨インストール無しでインストールする。
$ sudo apt-get install -y --no-install-recommends xserver-xorg
Openboxとlightdm、ブラウザとかのインストール
続いて、Openboxと自動ログインで使用するlightdm、Chromiumブラウザとフォントをインストールする。
lightdmは合わせてaccountsservice
を入れる。これを入れておかないとlightdmがError getting user list from org.freedesktop.Accounts: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.Accounts was not provided by any .service files
みたいなエラーを履いたりする。
$ sudo apt-get install -y openbox lightdm accountsservice chromium-browser fonts-noto
自動ログイン後のブラウザのデフォルト起動設定
自動ログインさせるpiユーザーの.config/openbox/autostart
を作成し、次の設定を行う。
xset -dpms & xset s off & chromium-browser --noerrdialogs --kiosk --incognito --disable-translate --fast --fast-start --disable-infobars --disable-features=TranslateUI --disk-cache-dir=/dev/null --no-first-run --no-default-browser-check --disk-cache-size=0 --media-cache-size=0 --disable-bookmark-reordering --disable-extensions --disable-dev-tools --disable-sync http://localhost:8080/ &
ディスプレイの省電力モードをオフにするためにxset -dpms
、スクリーンセーバーをオフにするためにxset s off
、chromiumはkioskモードで起動し、余計なものを色々出さないようにするためにオプションをモリモリつける。
マウスカーソルが表示されないようにする
画面にマウスカーソルが表示されないように、/etc/lightdm/lightdm.conf
を編集して#xserver-command=X
とコメントアウトされている箇所を次のように書き換える。
xserver-command=X -nocursor
自動ログインを有効化
raspi-configで3 Boot Options
->B1 Desktop / CLI
->B4 Desktop Autologin
を設定する。
動作デモ
見づらいGIFになっちゃったけど、こんな感じで動く。Raspberry PI 3 B+を使って、電源投入からブラウザの起動完了まで30秒くらい。
まとめ
フロントエンドとかバックエンドとか、諸々のソースコードはこちら。OSのイメージは前に作ったrootfsのReadOnly環境や、extraパーティションを作ったやつにした。今後もっと機能つけて遊んでみるかも。
Raspbianに追加したextrafsパーティションのためのツールを作った
以前にRaspbianのデフォルトOSイメージにextrafsパーティションを追加する手順をまとめて、スクリプト化した。
今回は追加したextrafsパーティションをSDカードの最大容量まで拡張するツール(expand-extrafs)と、SDカードの違いで起こる問題を考慮してOSイメージをエクスポートするツール(export-compact-os-image)を作った。
expand-extrafs
extrafsパーティションを任意のタイミングでSDカードの最大容量まで拡張するためのツールとして、expand-extrafsを作った。
事前にサービスとしてRaspbianに登録しておけば、bootパーティションにexpandfs
ファイルを置いてOSを起動すると自動でサイズ拡張してくれる。
これはRaspbianのSSH有効化の仕様を参考にした(あっちはsystemd使ってやっているけど)。
動かすアプリケーション環境を出荷前まで準備しておいたOSをイメージファイルとして保存しておいて、SDカードに焼いて初回起動で自動拡張する。
export-compact-os-image
SDカードからextrafsパーティションの最後までをエクスポートするコマンドとして、export-compact-os-imageを作った。
これは単純にSDカード全体をddコマンドでエクスポートしただけだと、イメージファイルのサイズがそのSDカードのサイズになってしまうことを防ぐために作成した。例えば8GBのSDカードであってもメーカー毎とか、商品毎とかでSDカードの容量が微妙に異なることがあるので、あえて小さいイメージとして作成しておけるようにして、書き込むSDカードを制限しないことが目的。
開発時や環境構築時はextrafsパーティションを拡張せずにOSを作って、このコマンドでエクスポートしてイメージファイルにする。
とりあえずこれでextrafsパーティションを使う準備ができた。