ミルク色の記録

やったこと、やってみたこと

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.imgupdate-initramfs -c -k $(uname -r)で作りなおした。

ファイル名はinitrd.img-x.y.z-v7+のようになるので、/boot/config.txtinitramfs設定も修正しておいた。

また、スクリプトを見るに$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.txtinitramfs設定は古いままになってしまうので、 そこはひと手間かけてやる必要がありそう。

/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.txtinitramfs設定を書き換える。

これで改めて確認し、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-initramfsbackup_initramfsbackup_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の更新までできた。

パッチファイル

それぞれのバージョン用に作ったパッチをインストールスクリプトともにリポジトリに置いておいた。

github.com

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の再起動したくないので、何か手はないかと調べていたらフォーラムで次の記事を見つけた。

lb.raspberrypi.org

どうやら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 の同期が取得される度にディスクに現在の時刻を保存し、それを使って起動時にシステム時刻を修正することができ、時刻が常に正しいわけではないときでも、それらのシステムで時刻がモノトニックに進むことを保証します。

引用: systemd-timesyncd - ArchWiki

ただし、これだと自分で作ったアプリケーションをいつ起動するのかによってシステム時刻の正確な日時が取れるタイミングが微妙な感じになる。

サンプルアプリケーションで検証してみる

たとえば、次のようなアプリケーションを用意する。起動すると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を作った。

github.com

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で公開した。

github.com

CORSとは

Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使って、あるオリジン (ドメイン) で動いているウェブアプリケーションが、異なるオリジンのサーバーのリソースにアクセスできるようにする仕組み

引用: MDN Web docs オリジン間リソース共有 (CORS)

雑に説明すると、サーバ側で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に指定できる文字コードの種類はPHPmbstringがサポートしてるやつ。

補足

認証キーとオリジンでアクセス制限するようにしているけど、これは気休め程度。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のように直接接続できるので、いろいろな機器に応用できる。

引用: USB On-The-Go - Wikipedia

調べてみたら手持ちのRaspberry PI B系では構造上できないようなので、Raspberry PI Zero WHを買ってみた。送料とか色々考えてとりあえずAmazonでポチリ。

シリアル通信したり、ネットワークにしたり、キーボードにしたりと色々できるみたいだけど、とりあえず今回はストレージとして使う方法を試してみた。

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はLinuxGUIアプリケーションを書く...のは回避して、ブラウザでできるようにして自分の手持ちスキル側に寄せた(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 offchromiumkioskモードで起動し、余計なものを色々出さないようにするためにオプションをモリモリつける。

マウスカーソルが表示されないようにする

画面にマウスカーソルが表示されないように、/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秒くらい。

f:id:ushiboy:20190608135913g:plain
動作デモ

まとめ

フロントエンドとかバックエンドとか、諸々のソースコードはこちら。OSのイメージは前に作ったrootfsのReadOnly環境や、extraパーティションを作ったやつにした。今後もっと機能つけて遊んでみるかも。

github.com

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を起動すると自動でサイズ拡張してくれる。

f:id:ushiboy:20190504132840p:plain
expand-extrafs

これはRaspbianのSSH有効化の仕様を参考にした(あっちはsystemd使ってやっているけど)。

動かすアプリケーション環境を出荷前まで準備しておいたOSをイメージファイルとして保存しておいて、SDカードに焼いて初回起動で自動拡張する。

export-compact-os-image

SDカードからextrafsパーティションの最後までをエクスポートするコマンドとして、export-compact-os-imageを作った。

これは単純にSDカード全体をddコマンドでエクスポートしただけだと、イメージファイルのサイズがそのSDカードのサイズになってしまうことを防ぐために作成した。例えば8GBのSDカードであってもメーカー毎とか、商品毎とかでSDカードの容量が微妙に異なることがあるので、あえて小さいイメージとして作成しておけるようにして、書き込むSDカードを制限しないことが目的。

開発時や環境構築時はextrafsパーティションを拡張せずにOSを作って、このコマンドでエクスポートしてイメージファイルにする。

f:id:ushiboy:20190504132923p:plain
export-compact-os-image

とりあえずこれでextrafsパーティションを使う準備ができた。