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パーティションを使う準備ができた。
RaspbianのrootfsをReadOnlyにした
fsprotectとroot-ro
Raspberry PIでRaspbianを使っている時、rootfsのReadOnly化については、fsprotectを使った方法を過去に試したことがあった。
fsprotectはdebianパッケージとしてインストールして、/boot/cmdline.txt
にfsprotect
パラメータを追加するだけで設定できるのだけど、
Raspbianではaufsのパッチを当ててカーネルを再構築する必要があり、非力なRaspberry PIでカーネルビルドするの辛いのでPCでクロスコンパイルってことになって、前提条件を達成する作業が結構大変だった。
で、他の方法を調べていたら、最近root-roというものを知った。
root-roはoverlayfsを利用しているので、カーネルの再構築が不要で簡単に導入できる。
ただ、/boot/config.txt
にinitramfs initrd.gz
を設定して、initramfsで起動する = ReadOnly
、initramfsで起動しない = 通常
となっていて、
このあたりは常にinitramfsで起動し、cmdlineパラメータでReadOnlyの有効/無効を切り替えられるfsprotectの方が好みだった。
また、root-roは/boot
パーティションもデフォルトでReadOnlyにしていて、
fsprotectは追加設定で/boot
もReadOnlyにできるがデフォルトではなっていないという違いがあった。
fsprotectを使っていた時はデフォルトのままで使っていたので、rootfsのReadOnlyを一時的に解除したいときは/boot/cmdline.txt
をちょっと編集して再起動するだけで良かった。
root-roもremountして/mnt/boot-ro
を編集すれば良いという手はあるが、/boot
ではなく/mnt/boot-ro
を意識する必要があり、個人的には/boot
はReadOnlyじゃないほうが好みだった。
で、どうせRaspbianでしか使うつもりないしということもあり、好みを満たすために1から自分で作ってみた。
readonlyfs
作ったのはこれ。
仕組みはoverlayfsを利用してinitramfsでrootfsのマウントをtmpfsを使ったupperdir、lowerdir、workdirを組み合わせたものに取り替えるというもの。
ReadOnlyの有効/無効の切り替えはcmdlineパラメータにreadonlyfs
があるかどうかで行う。
とりあえずこれを適用したものとしていないものを用意して、Diskへの書き込みをログ取りしてみた。
ReadOnly化していないRaspbianでは次のようになり、Diskへの書き込みが発生していることがわかる。
ReadOnly化したRaspbianでは次のようになり、Diskへの書き込みが発生していないことがわかる。
最低限の実装なので、tmpfsのサイズ決めたり、/boot
パーティションをReadOnlyにする機能はつけていない。
とりあえずこれでしばらく様子見。
RaspbianのOSイメージのパーティションを分割した
動機
公式で配布されているRaspbian OSイメージはbootとrootfsのパーティションのみで構成されていて、 Raspberry PIに入れて起動するとrootfsが自動でSDカード全体まで拡張される。
そのまま常時稼働で運用していると、OSのログやtmpやswapの利用でSDカードへの書き込みがチクチク行われて、 NANDフラッシュの書き込み上限を迎えてある日ディスクが壊れることになる。
NANDフラッシュはSLC、MLC、TLCで書き込み上限が異なるので、なるべく上限の多いタイプを選ぶべきだけど、 それでも上限があることには変わりないし、工業用のSLCのSDカードとか結構高価。
なのでなるべくディスクへの書き込みは最小限にしたい。
また、電源ぶち切りとか停電とか考えると、そもそもrootfsはReadOnlyにしたい。
ただし、ReadOnlyにすると設定ファイルの書き換えみたいな最低限の書き込みや変更ができなくなってしまうので、 書き込みができる領域が別にほしいということになる。
というわけで、公式のRaspbian OSイメージをカスタマイズして、bootとrootfsの他にもう一つパーティションを追加する手順をまとめた。
作業環境とRaspbian OSイメージのバージョン
作業環境は今回もLinux PC。
Raspbianのイメージは(もう新しいの出てるけど)2018-11-13-raspbian-stretch-lite.img
を利用した。
カスタマイズ手順
作業は次のように行った。
カスタマイズ用のOSイメージを作ってパーティションの状況確認
まずカスタマイズ用に公式イメージのコピーを作成した。
$ cp 2018-11-13-raspbian-stretch-lite.img 2018-11-13-raspbian-stretch-lite-custom.img
次にfdiskで現在のイメージのパーティション状況を確認した。
$ fdisk -l 2018-11-13-raspbian-stretch-lite-custom.img Disk 2018-11-13-raspbian-stretch-lite-custom.img: 1.8 GiB, 1866465280 bytes, 3645440 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ 2018-11-13-raspbian-stretch-lite-custom.img1 8192 98045 89854 43.9M c W95 FAT32 (LBA) 2018-11-13-raspbian-stretch-lite-custom.img2 98304 3645439 3547136 1.7G 83 Linux
デバイス2018-11-13-raspbian-stretch-lite-custom.img1
がbootパーティションで、
2018-11-13-raspbian-stretch-lite-custom.img2
がrootfsパーティションに当たる。
OSイメージのファイルサイズを増やす
パーティションを追加できるようにするために、まずtruncateを使ってOSイメージのファイルサイズを増やす。
$ truncate -s 4GiB 2018-11-13-raspbian-stretch-lite-custom.img
とりあえず4GBまで増やした。
改めてfdiskでイメージの状況を確認する。
$ fdisk -l 2018-11-13-raspbian-stretch-lite-custom.img Disk 2018-11-13-raspbian-stretch-lite-custom.img: 4 GiB, 4294967296 bytes, 8388608 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ 2018-11-13-raspbian-stretch-lite-custom.img1 8192 98045 89854 43.9M c W95 FAT32 (LBA) 2018-11-13-raspbian-stretch-lite-custom.img2 98304 3645439 3547136 1.7G 83 Linux
イメージのサイズが1.8GBから4GBまで増えたことが確認できる。
rootfsのパーティションサイズを変更
次にrootfsのパーティションサイズを拡張する。
fdiskの対話モードでカスタムイメージを開いて、dコマンドで一度rootfsのパーティションを削除し、nコマンドで改めて作りなおす。
この時新たに作るパーティションのFirst sectorはもともとの値(今回であれば98304)を指定する。
Last sectorは今回はrootfsを3GB固定にするので+3G
を指定した。
$ fdisk 2018-11-13-raspbian-stretch-lite-custom.img Welcome to fdisk (util-linux 2.27.1). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. コマンド (m でヘルプ): d パーティション番号 (1,2, default 2): 2 Partition 2 has been deleted. コマンド (m でヘルプ): n Partition type p primary (1 primary, 0 extended, 3 free) e extended (container for logical partitions) Select (default p): p パーティション番号 (2-4, default 2): 2 First sector (2048-8388607, default 2048): 98304 Last sector, +sectors or +size{K,M,G,T,P} (98304-8388607, default 8388607): +3G Created a new partition 2 of type 'Linux' and of size 3 GiB.
作りなおしたrootfsパーティションをpコマンドで確認すると次のようになる。
コマンド (m でヘルプ): p Disk 2018-11-13-raspbian-stretch-lite-custom.img: 4 GiB, 4294967296 bytes, 8388608 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ 2018-11-13-raspbian-stretch-lite-custom.img1 8192 98045 89854 43.9M c W95 FAT32 (LBA) 2018-11-13-raspbian-stretch-lite-custom.img2 98304 6389759 6291456 3G 83 Linux
書き込み用領域のパーティションを追加
次に書き込み領域にする予定のパーティションをnコマンドで追加する。
First sectorは2018-11-13-raspbian-stretch-lite-custom.img2
の最後のセクタ + 1の値(今回であれば6389760)とし、
Last sectorは指定せずにイメージファイルのサイズいっぱいまでを使う。
コマンド (m でヘルプ): n Partition type p primary (2 primary, 0 extended, 2 free) e extended (container for logical partitions) Select (default p): p パーティション番号 (3,4, default 3): 3 First sector (2048-8388607, default 2048): 6389760 Last sector, +sectors or +size{K,M,G,T,P} (6389760-8388607, default 8388607): Created a new partition 3 of type 'Linux' and of size 976 MiB.
追加したパーティションをpコマンドで確認する。
コマンド (m でヘルプ): p Disk 2018-11-13-raspbian-stretch-lite-custom.img: 4 GiB, 4294967296 bytes, 8388608 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ 2018-11-13-raspbian-stretch-lite-custom.img1 8192 98045 89854 43.9M c W95 FAT32 (LBA) 2018-11-13-raspbian-stretch-lite-custom.img2 98304 6389759 6291456 3G 83 Linux 2018-11-13-raspbian-stretch-lite-custom.img3 6389760 8388607 1998848 976M 83 Linux
wコマンドでパーティションの変更を反映してfdiskの対話モードを抜ける。
コマンド (m でヘルプ): w The partition table has been altered. Syncing disks.
書き込み用領域のフォーマットとラベル付け
追加したパーティションはまだフォーマットしていないので、フォーマットするためにイメージをLinux PCでマウントできるようにする。 これにはkpartxを利用してイメージファイル内のパーティションをデバイスにマッピングする。
$ sudo kpartx -av 2018-11-13-raspbian-stretch-lite-custom.img add map loop0p1 (253:0): 0 89854 linear 7:0 8192 add map loop0p2 (253:1): 0 6291456 linear 7:0 98304 add map loop0p3 (253:2): 0 1998848 linear 7:0 6389760
各パーティションは/dev/mapper
下にマッピングされる。
$ ls /dev/mapper control loop0p1 loop0p2 loop0p3
/dev/mapper/loop0p1
がboot、/dev/mapper/loop0p2
がrootfs、/dev/mapper/loop0p3
が今回追加したパーティションに当たる。
/dev/mapper/loop0p3
をEXT4でフォーマットする。
$ sudo mkfs -t ext4 /dev/mapper/loop0p3 mke2fs 1.42.13 (17-May-2015) Discarding device blocks: done Creating filesystem with 249856 4k blocks and 62464 inodes Filesystem UUID: 828efd40-40d9-42f9-85f7-5d39b1154840 Superblock backups stored on blocks: 32768, 98304, 163840, 229376 Allocating group tables: done Writing inode tables: done Creating journal (4096 blocks): done Writing superblocks and filesystem accounting information: done
わかりやすいようにパーティションにラベルをつける。
$ sudo e2label /dev/mapper/loop0p3 extrafs
ラベルがついたか確認。
$ sudo e2label /dev/mapper/loop0p3 extrafs
rootfsの自動リサイズを無効化・書き込み用領域のマウントを設定
bootとrootfsのファイルに手を加えるために、それぞれのマウントポイントを作成する。
$ mkdir boot $ mkdir rootfs
bootをマウントする。
$ sudo mount /dev/mapper/loop0p1 boot
rootfsをマウントする。
$ sudo mount /dev/mapper/loop0p2 rootfs
boot/cmdline.txt
を編集し、末尾のinit=/usr/lib/raspi-config/init_resize.sh
を削除して、初回起動時のrootfsの自動拡張を無効にする。
$ sudo vi boot/cmdline.txt`
編集後のboot/cmdline.txt`は次のようになる。
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=7ee80803-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet
rootfs/etc/fstab
を編集し、extrafsパーティションが起動時にマウントされるように設定する。
末尾に/dev/mmcblk0p3 /extra ext4 defaults,noatime 0 1
を追加する。
$ sudo vi rootfs/etc/fstab
編集後のrootfs/etc/fstab
は次のようになる。
proc /proc proc defaults 0 0 PARTUUID=7ee80803-01 /boot vfat defaults 0 2 PARTUUID=7ee80803-02 / ext4 defaults,noatime 0 1 /dev/mmcblk0p3 /extra ext4 defaults,noatime 0 1
他のパーティションがPARTUUID指定なのでちょっと歪だけどとりあえずこれで行く。
rootfs内にextrafs用のマウントポイントを追加しておく。
$ sudo mkdir rootfs/extra
bootとrootfsをアンマウントする。
$ sudo umount boot $ sudo umount rootfs
$ sudo kpartx -d 2018-11-13-raspbian-stretch-lite-custom.img
カスタムイメージの動作確認
出来上がったカスタムイメージをSDカードに書き込む。
$ sudo dd if=2018-11-13-raspbian-stretch-lite-custom.img of=/dev/mmcblk0 bs=1M 4096+0 レコード入力 4096+0 レコード出力 4294967296 bytes (4.3 GB, 4.0 GiB) copied, 776.849 s, 5.5 MB/s
書き込み完了したSDカードをRaspberry PIに入れて起動する。
起動したRaspbianにログインし、fdiskでパーティションの状況を確認する。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 7.4 GiB, 7969177600 bytes, 15564800 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: 0x7ee80803 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 6389759 6291456 3G 83 Linux /dev/mmcblk0p3 6389760 8388607 1998848 976M 83 Linux
mountの状況を確認する。
$ mount /dev/mmcblk0p2 on / type ext4 (rw,noatime,data=ordered) devtmpfs on /dev type devtmpfs (rw,relatime,size=217616k,nr_inodes=54404,mode=755) sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) proc on /proc type proc (rw,relatime) tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000) tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755) tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k) tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755) cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) sunrpc on /run/rpc_pipefs type rpc_pipefs (rw,relatime) debugfs on /sys/kernel/debug type debugfs (rw,relatime) systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=35,pgrp=1,timeout=0,minproto=5,maxproto=5,direct) mqueue on /dev/mqueue type mqueue (rw,relatime) configfs on /sys/kernel/config type configfs (rw,relatime) /dev/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro) /dev/mmcblk0p3 on /extra type ext4 (rw,noatime,data=ordered) tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=44384k,mode=700,uid=1000,gid=1000)
/dev/mmcblk0p3 on /extra type ext4 (rw,noatime,data=ordered)
となっていれば/extra
に追加したパーティションがマウントされていることが確認できる。
まとめ
とりあえず分割までできた。
この作業をいちいち手でやるのが面倒になったので、作成用スクリプト作った。
$ sudo ./create-extra-partition <path to>/YYYY-MM-DD-raspbian-stretch-lite.img
みたいな感じで作成できる。
Raspbianの初期起動でのrootfsパーティション自動拡張の仕組みを調べた
環境
試したRaspbianのイメージは2018-11-13-raspbian-stretch-lite.img
。母艦はLinux PCで調べた。
きっかけ
Raspbian Jessieのいつ頃からか忘れたけど、イメージをSDカードに焼いてRaspberry PIに入れて起動すると、 自動でrootfsのパーティションがSDカードの容量全体まで拡張されるようになった。
RaspbianのOSイメージをSDカードに焼いた直後のパーティションはfdiskで確認すると、次のようになっている。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 3645439 3547136 1.7G 83 Linux
これをRaspberry PIに入れて起動すると、自動拡張のメッセージが表示されて、再起動後に次のようになる。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 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: 0x7273b820 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 7744511 7646208 3.7G 83 Linux
/dev/mmcblk0p2
の容量が1.7Gから3.7Gに広がっているのがわかる。
これはこれで便利なのだけれど、rootfsとは別のパーティション作ったり色々やりたいことがあるので、 そもそもこれはどうやって実現しているのだろうと思って調べてみた。
/boot/cmdline.txt
を調べる
ブート時になにかやっているのだから/boot/cmdline.txt
をとりあえず見てみようと思い、
イメージをSDカードに焼いてLinux PCからマウントしてファイルを覗いてみた。
/boot/cmdline.txt
の中身
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=7ee80803-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh
末尾に、なんかそのものズバリになりそうなinit=/usr/lib/raspi-config/init_resize.sh
を発見。
/usr/lib/raspi-config/init_resize.sh
を調べる
今度はrootfsをマウントして/usr/lib/raspi-config/init_resize.sh
の中身を覗いてみた。
init_resize.shの中身は次の通り。
#!/bin/sh reboot_pi () { umount /boot mount / -o remount,ro sync if [ "$NOOBS" = "1" ]; then if [ "$NEW_KERNEL" = "1" ]; then reboot -f "$BOOT_PART_NUM" else echo "$BOOT_PART_NUM" > "/sys/module/${BCM_MODULE}/parameters/reboot_part" fi fi echo b > /proc/sysrq-trigger sleep 5 exit 0 } check_commands () { if ! command -v whiptail > /dev/null; then echo "whiptail not found" sleep 5 return 1 fi for COMMAND in grep cut sed parted fdisk findmnt partprobe; do if ! command -v $COMMAND > /dev/null; then FAIL_REASON="$COMMAND not found" return 1 fi done return 0 } check_noobs () { if [ "$BOOT_PART_NUM" = "1" ]; then NOOBS=0 else NOOBS=1 fi } get_variables () { ROOT_PART_DEV=$(findmnt / -o source -n) ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3) ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4) ROOT_DEV="/dev/${ROOT_DEV_NAME}" ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition") BOOT_PART_DEV=$(findmnt /boot -o source -n) BOOT_PART_NAME=$(echo "$BOOT_PART_DEV" | cut -d "/" -f 3) BOOT_DEV_NAME=$(echo /sys/block/*/"${BOOT_PART_NAME}" | cut -d "/" -f 4) BOOT_PART_NUM=$(cat "/sys/block/${BOOT_DEV_NAME}/${BOOT_PART_NAME}/partition") OLD_DISKID=$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p') check_noobs ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size") TARGET_END=$((ROOT_DEV_SIZE - 1)) PARTITION_TABLE=$(parted -m "$ROOT_DEV" unit s print | tr -d 's') LAST_PART_NUM=$(echo "$PARTITION_TABLE" | tail -n 1 | cut -d ":" -f 1) ROOT_PART_LINE=$(echo "$PARTITION_TABLE" | grep -e "^${ROOT_PART_NUM}:") ROOT_PART_START=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 2) ROOT_PART_END=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 3) if [ "$NOOBS" = "1" ]; then EXT_PART_LINE=$(echo "$PARTITION_TABLE" | grep ":::;" | head -n 1) EXT_PART_NUM=$(echo "$EXT_PART_LINE" | cut -d ":" -f 1) EXT_PART_START=$(echo "$EXT_PART_LINE" | cut -d ":" -f 2) EXT_PART_END=$(echo "$EXT_PART_LINE" | cut -d ":" -f 3) fi } fix_partuuid() { DISKID="$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p')" sed -i "s/${OLD_DISKID}/${DISKID}/g" /etc/fstab sed -i "s/${OLD_DISKID}/${DISKID}/" /boot/cmdline.txt } check_variables () { if [ "$NOOBS" = "1" ]; then if [ "$EXT_PART_NUM" -gt 4 ] || \ [ "$EXT_PART_START" -gt "$ROOT_PART_START" ] || \ [ "$EXT_PART_END" -lt "$ROOT_PART_END" ]; then FAIL_REASON="Unsupported extended partition" return 1 fi fi if [ "$BOOT_DEV_NAME" != "$ROOT_DEV_NAME" ]; then FAIL_REASON="Boot and root partitions are on different devices" return 1 fi if [ "$ROOT_PART_NUM" -ne "$LAST_PART_NUM" ]; then FAIL_REASON="Root partition should be last partition" return 1 fi if [ "$ROOT_PART_END" -gt "$TARGET_END" ]; then FAIL_REASON="Root partition runs past the end of device" return 1 fi if [ ! -b "$ROOT_DEV" ] || [ ! -b "$ROOT_PART_DEV" ] || [ ! -b "$BOOT_PART_DEV" ] ; then FAIL_REASON="Could not determine partitions" return 1 fi } check_kernel () { local MAJOR=$(uname -r | cut -f1 -d.) local MINOR=$(uname -r | cut -f2 -d.) if [ "$MAJOR" -eq "4" ] && [ "$MINOR" -lt "9" ]; then return 0 fi if [ "$MAJOR" -lt "4" ]; then return 0 fi NEW_KERNEL=1 } main () { get_variables if ! check_variables; then return 1 fi check_kernel if [ "$NOOBS" = "1" ] && [ "$NEW_KERNEL" != "1" ]; then BCM_MODULE=$(grep -e "^Hardware" /proc/cpuinfo | cut -d ":" -f 2 | tr -d " " | tr '[:upper:]' '[:lower:]') if ! modprobe "$BCM_MODULE"; then FAIL_REASON="Couldn't load BCM module $BCM_MODULE" return 1 fi fi if [ "$ROOT_PART_END" -eq "$TARGET_END" ]; then reboot_pi fi if [ "$NOOBS" = "1" ]; then if ! parted -m "$ROOT_DEV" u s resizepart "$EXT_PART_NUM" yes "$TARGET_END"; then FAIL_REASON="Extended partition resize failed" return 1 fi fi if ! parted -m "$ROOT_DEV" u s resizepart "$ROOT_PART_NUM" "$TARGET_END"; then FAIL_REASON="Root partition resize failed" return 1 fi partprobe "$ROOT_DEV" fix_partuuid return 0 } mount -t proc proc /proc mount -t sysfs sys /sys mount -t tmpfs tmp /run mkdir -p /run/systemd mount /boot mount / -o remount,rw sed -i 's| init=/usr/lib/raspi-config/init_resize.sh||' /boot/cmdline.txt if ! grep -q splash /boot/cmdline.txt; then sed -i "s/ quiet//g" /boot/cmdline.txt fi sync echo 1 > /proc/sys/kernel/sysrq if ! check_commands; then reboot_pi fi if main; then whiptail --infobox "Resized root filesystem. Rebooting in 5 seconds..." 20 60 sleep 5 else sleep 5 whiptail --msgbox "Could not expand filesystem, please try raspi-config or rc_gui.\n${FAIL_REASON}" 20 60 fi reboot_pi
partedを使って$ROOT_DEV
をゴニョゴニョしている部分があるのでビンゴっぽい。
whiptail --infobox "Resized root filesystem. Rebooting in 5 seconds..." 20 60
ってのがリサイズ完了メッセージを出すやつみたい。
面白いのは174行目から178行目あたり。
sed -i 's| init=/usr/lib/raspi-config/init_resize.sh||' /boot/cmdline.txt if ! grep -q splash /boot/cmdline.txt; then sed -i "s/ quiet//g" /boot/cmdline.txt fi sync
/boot/cmdline.txt
からinit=/usr/lib/raspi-config/init_resize.sh
を消して次から実行されないようにしている。
自動拡張を無効にしてみる
とりあえず/boot/cmdline.txt
からinit=/usr/lib/raspi-config/init_resize.sh
を削除して、
Raspberry PIに入れて起動してみた。
初回起動を終えた後に改めてfdiskで確認すると、自動拡張されていないことが確認できる。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 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: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 3645439 3547136 1.7G 83 Linux
この状態で、raspi-configの7 Advanced Options -> A1 Expand Filesystem
すれば、昔のRaspbianのように手動で拡張できる。
resize2fsの実行はどうなっているか調べる
通常、パーティションをリサイズしたあとはOSを再起動してからresize2fsをしなければいけないはずなんだけど、 それはどこでやっているのだろうと思ってraspi-configのExpand Filesystem機能を調べてみた。
raspi-config自体はgithubにリポジトリがあったのでそこのraspi-configのソースを読んだ。
Expand Filesystem機能はdo_expand_rootfs
で実装されていて、その中のコードを読んでみたら、fdiskでパーティションを拡張した後に、/etc/init.d/resize2fs_once
を作成してupdate-rc.dでサービスに登録し、再起動後に実行させていることがわかった。
resize2fs_once
はサービスが起動されたら、resize2fsをrootfsパーティションに対して行い、自分自身のサービス登録を解除して、ファイル削除するようになっている。
RaspbianのOSイメージを焼いたばかりのSDカードでrootfsをマウントして/etc/init.d
の中を調べてみると、確かにresize2fs_once
が入っていた。
OSイメージに入っていた/etc/init.d/resize2fs_once
はこんな感じになっていた。
#!/bin/sh ### BEGIN INIT INFO # Provides: resize2fs_once # Required-Start: # Required-Stop: # Default-Start: 3 # Default-Stop: # Short-Description: Resize the root filesystem to fill partition # Description: ### END INIT INFO . /lib/lsb/init-functions case "$1" in start) log_daemon_msg "Starting resize2fs_once" ROOT_DEV=$(findmnt / -o source -n) && resize2fs $ROOT_DEV && update-rc.d resize2fs_once remove && rm /etc/init.d/resize2fs_once && log_end_msg $? ;; *) echo "Usage: $0 start" >&2 exit 3 ;; esac
Raspbianの初回起動時には必ずこのサービスが動くので、rootfsのパーティションに対してresize2fsが行われる。
/etc/init.d/resize2fs_once
もスパイ映画の「なお、このテープは自動的に消滅する」みたいな作りになっていて面白い。
まとめ
Raspbianのrootfsの初回起動時自動拡張は/usr/lib/raspi-config/init_resize.sh
と/etc/init.d/resize2fs_once
の合わせ技で実現されていることがわかった。
Elmのupdate: Msg -> Model -> (Model, Cmd Msg)にあこがれてJavaScriptの状態管理用のライブラリ作った
作ったのはこれ。
動機
普段Reduxで非同期なことをやる時にredux-thunkミドルウェアを使っている。
で、次のようなことをするときにいつも悩む。
- 非同期にデータを取ってくるアクションで取得前にローディングマスクを表示し、取得完了後にローディングマスクを非表示にする。
- なんらかのメッセージ(完了したとかエラーになったとか)をToast的に表示して、数秒後に消す。
例えばローディングマスクのやつだと、だいたい次のように実装している。
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; const fetchMessage = () => { return new Promise(resolve => { // サンプルなのでsetTimeoutでごまかし setTimeout(() => { resolve('hello'); }, 1000); }); }; const load = () => { return async dispatch => { dispatch({ type: 'prepare-for-loading' }); const msg = await fetchMessage(); dispatch({ type: 'loaded', payload: msg }); }; }; const store = createStore((state = { msg: '', loading: false }, action) => { switch (action.type) { case 'prepare-for-loading': { return { ...state, loading: true }; } case 'loaded': { return { msg: action.payload, loading: false }; } default: { return state; } } }, applyMiddleware(thunk)); store.subscribe(() => { console.log(store.getState()); }); store.dispatch(load());
このとき気になるのが、load
の実装。
const load = () => { return async dispatch => { dispatch({ type: 'prepare-for-loading' }); const msg = await fetchMessage(); dispatch({ type: 'loaded', payload: msg }); }; };
thunkなアクションとしてdispatchを受け取る関数を返し、非同期API実行の前後で2回dispatchしている。 これが1つのアクションで2つの仕事をしているように見えて、なんだかなーと思っていた。 テストを書くときも、この関数は2つアクションを返して、1つ目は...みたいになっていて、ますますなんだかなーと。
次のように中身を分離して使う側でそれぞれ呼び出すという手もあるが、これを並べて同じタイミングで実行するのか、
それとも1つ目のdispatchで変更されたStateのloading:true
が来たら2つ目を別のタイミングで実行するべきかなどで悩ましい。
store.dispatch({ type: 'prepare-for-loading'}); store.dispatch(() => { return async dispatch => { const msg = await fetchMessage(); dispatch({ type: 'loaded', payload: msg }); } });
そんな感じで悶々としていた。
Elmとの出会い
ReduxがElmを参考にしたという話は知っていたけど、特別触ってみたりはしていなかった。 ある日rebuild.fmのエピソードで取り上げられているのを聴いて、ちょっと興味が湧いたので勉強してみた。
Elmアーキテクチャの簡単なやつを触って、update
のインターフェイスがupdate : Msg -> Model -> Model
な辺りまでは、
確かにReduxっぽい...みたいな印象だった。
その後、commandを使う辺りまで進んで、インターフェイスがupdate : Msg -> Model -> (Model, Cmd Msg)
になった時に、自分がやりたかったのこれだなーと思った。
ローディングのやつの例でElmだと次のようになると思う(実際は非同期取得の結果の成否とかもっとcase増えると思うけど)。
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of PrepareForLoading -> ( { model | loading = True }, load ) Loaded payload -> ( { model | msg = payload }, Cmd.none )
で、JavaScriptでこんな感じにやるには...と考えて冒頭に貼ったcycloneというライブラリを作った。
cycloneを使った場合
Reduxがupdate<S, A>(state: S, action: A) => S
なのに対して、cycloneはupdate<S, A>(state: S, action: A) => [S, A]
というインターフェイスになっていて、更新された状態と次に実行するアクションをタプルで返すようにしてある。次に実行するアクションがない場合はnone
を返す。これはElmでのCmd.none
を意識した。
上のローディングマスクの例をcycloneで書き直すと次のようになる。
import { createStore, none } from '@ushiboy/cyclone'; const fetchMessage = () => { return new Promise(resolve => { setTimeout(() => { resolve('hello'); }, 1000); }); }; const prepareForLoading = () => ({ type: 'prepare-for-loading' }); const load = async () => { const msg = await fetchMessage(); return { type: 'loaded', payload: msg }; }; const store = createStore({ msg: '', loading: false }, (state, action) => { switch (action.type) { case 'prepare-for-loading': { return [ { ...state, loading: true }, load() ]; } case 'loaded': { return [ { msg: action.payload, loading: false }, none() ]; } default: { return [state, none()]; } } }); store.subscribe(() => { console.log(store.getState()); }); store.dispatch(prepareForLoading());
prepareForLoading
を実行すると、update
はloadingをtrueにし、次に実行するload
を返す。
storeは更新された状態を通知しつつ、次に実行すべきアクションを実行する。
load
の前に実行する関数の名前をどうするか、名前お悩み問題が増えたような気がするけど、
アクション的には1つのアクションで1つの仕事をするようになったので、概ね満足。
その他の機能
Reducer
ついでにつけた機能として、Reducerでの分割統治はできるようにしておこうということでcombine
とreducer
というAPIを用意した。
使用例は次の通り。
import { createStore, none, combine, reducer } from '@ushiboy/cyclone'; const store = createStore({ a: 0, b: 0, c: '' }, combine( reducer('a', (state, action) => { switch (action.type) { default: { return [state, none()]; } } }), reducer('b', (state, action) => { switch (action.type) { default: { return [state, none()]; } } }), reducer('c', [ 'a', 'b' ], (state, action, a, b) => { switch (action.type) { default: { return [state, none()]; } } }) ));
Reducerの定義はreducer
関数に対象とするStateのキー名とupdate関数を渡して行うようにした。それをcombine
関数で統合してひとつのupdate関数にする。
Reducerで分割統治すると、後々この状態はこっちでも参照したい...がでるので、reducer
関数の第2引数に参照したい他の状態のキー名リストを渡すとupdate関数の第3引数以降で受け取れるようにした(上の例だとc
のReducerでa
とb
の状態を参照している)。
Extra Argument
もうひとつ、redux-thunkのwithExtraArgument的な機能も足した。
createStore
の第3引数にアクション実行時に利用したいオブジェクトなどを設定すると、アクション側で受け取って使用できる。
これはWebAPIみたいな、テストの時はインターフェイス同じなモックに差し替えたいものを使うことを目的としている。
サンプルコードは次の通り。
const greet = () => async ({ sleep }) => { await sleep(1000); return { type: 'greet', payload: { msg: 'hello' } }; }; const store = createStore( { word: '', waiting: false }, (state, action) => { switch (action.type) { default: { return [state, none()]; } } }, { sleep(time) { return new Promise(resolve => { setTimeout(() => { resolve(); }, time); }); } } );
greet
アクションはcreateStoreの第3引数で指定したsleep
APIを持つオブジェクトを受け取って使用している。
まとめ
Elmの学習はupdateのインターフェイス以外にも参考になることが結構あったのでやってよかった。
作ったライブラリは、ひとまず意図した動きになっているけど、中身の実装でまだ迷っている部分があるので、 その辺はドッグフーディングしながら様子見。
E2Eテストの実行時間を1/2とか1/4に縮めた
前回も触れたけど、普段Seleniumを使ってpythonでE2Eテストを書いている。
E2Eテストの接続先サーバはdocker-composeを使って必要なものをガッと立ち上げていて、 テスト内でredisやDBに直接接続してデータの初期化をしながらやっている。
使っているdocker-compose.yml
はだいたい次のような感じ。
version: '2' services: web: container_name: "test-web" image: nginx:1.11 ports: - "8080:80" volumes: - /etc/localtime:/etc/localtime - ./build:/usr/share/nginx/html - ./fixture/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - app app: container_name: "test-app" image: myapp volumes: - /etc/localtime:/etc/localtime - ./fixture/run.sh:/myapp/run.sh - ./fixture/production.ini:/myapp/production.ini depends_on: - redis - db redis: container_name: "test-redis" ports: - "6379:6379" volumes: - /etc/localtime:/etc/localtime image: redis db: container_name: "test-db" image: postgres:9.5 ports: - "5432:5432" volumes: - /etc/localtime:/etc/localtime - ./fixture/postgresql/initdb:/docker-entrypoint-initdb.d environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres
webサーバとしてnginxがあり、WEB APIのためのappサーバへのリクエストをリバースプロキシする。 appサーバはAPIのリクエストが来たら、dbやredisに対してアレコレする。
これをdocker-compose up -d
したあとにpy.test test
して実行する...という感じにやっていた。
あるアプリケーションで、機能が増えるに連れてだんだんとテストの量が増えていき、 現在は150件くらいのテストを実行するのに15分近くかかるようになっていた。
DBコンテナの/var/lib/postgresql/data
をtmpfsにし、selenium.webdriver.support.ui.WebDriverWait
のpoll_frequency
パラメータ(UIの変化を待機する条件をチェックする周期)をデフォルトの0.5秒から小さくして、
トータル時間12分位までには縮まったものの、劇的な効果は得られなかった。
そこで以前から考えていたpytest-xdistを使ったテストの並列化を試してみた。
pytest-xdistとdocker-composeの複数起動
pytest-xdistを使うとpy.test -n 2 test
のようにして、-n
オプションでワーカーの数を指定してテストを並列に実行できるようになる。
ただし、E2Eテストでこれをそのままやってしまうと、あるワーカーがテストを行っている最中に別のワーカーがDBの初期化をしてしまうので、 ワーカー毎に接続先のサーバ側を分ける必要があり、そのままでは使えない。
docker-composeは-p
オプションでプロジェクト名を指定して複数起動することができるので、
docker-compose.yml
に環境変数で各種ポートを指定できるように修正を加えて、プロジェクトごとにポートを変えて起動できるようにした。
修正したdocker-compose.yml
ファイルは次のような感じになった。
version: '2.1' services: web: container_name: "test-web-${ID:-1}" image: nginx:1.11 ports: - "${WEB_PORT:-8080}:80" volumes: - /etc/localtime:/etc/localtime - ./build:/usr/share/nginx/html - ./fixture/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - app app: container_name: "test-app-${ID:-1}" image: myapp volumes: - /etc/localtime:/etc/localtime - ./fixture/run.sh:/myapp/run.sh - ./fixture/production.ini:/myapp/production.ini depends_on: - redis - db redis: container_name: "test-redis-${ID:-1}" ports: - "${REDIS_PORT:-6379}:6379" volumes: - /etc/localtime:/etc/localtime image: redis db: container_name: "test-db-${ID:-1}" image: postgres:9.5 ports: - "${DB_PORT:-5432}:5432" tmpfs: - /var/lib/postgresql/data volumes: - /etc/localtime:/etc/localtime - ./fixture/postgresql/initdb:/docker-entrypoint-initdb.d environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres
コンテナ名が被らないように名称の末尾に付けるためのID
と各ポートWEB_PORT
、DB_PORT
、REDIS_PORT
を環境変数で渡せるようにした。
これで次のようにコマンドを叩けば同じ構成の環境をポートを変えて複数立ち上げられる。
$ ID=1 WEB_PORT=8080 DB_PORT=54320 REDIS_PORT=63790 docker-compose -p s1 up -d $ ID=2 WEB_PORT=8081 DB_PORT=54321 REDIS_PORT=63791 docker-compose -p s2 up -d
pytest-xdistによって、テスト実行中のワーカーにはPYTEST_XDIST_WORKER
環境変数が渡される。
ここから、gw0
やgw1
といった自分のワーカーIDを取得できる。
このワーカーIDと接続するサーバのポートを関連付けて使うようにすれば、ワーカー毎に異なるサーバへ接続して互いに干渉することなくテストできる。
ワーカーと接続サーバの関連付け
docker-composeの環境変数に渡す値と、ワーカー内で使用するポートの値を関連付けて管理するために、次のようなe2e.config.yml
設定ファイルを用意した。
workers: - web_port: 8080 redis_port: 63790 db_port: 54320 - web_port: 8081 redis_port: 63791 db_port: 54321 - web_port: 8082 redis_port: 63792 db_port: 54322 - web_port: 8083 redis_port: 63793 db_port: 54323
ワーカーごとにweb_port
、redis_port
、db_port
を記述できるようにした。
そして、この設定ファイルを読み込んで使うためのAPIをenvironment.py
として用意した。
import yaml import subprocess import os def load_e2e_config(filepath): with open(filepath, 'r') as f: data = yaml.load(f, Loader=yaml.SafeLoader) config = dict() workers = data.get('workers', []) for i, w in enumerate(workers): w['id'] = i + 1 config['gw%d' % i] = w return config def docker_compose_up(config): for name, c in config.items(): env = os.environ.copy() env['ID'] = str(c['id']) env['WEB_PORT'] = str(c['web_port']) env['REDIS_PORT'] = str(c['redis_port']) env['DB_PORT'] = str(c['db_port']) subprocess.run(['docker-compose', '-p', name, 'up', '-d'], env=env) def docker_compose_down(config): for name, c in config.items(): subprocess.run(['docker-compose', '-p', name, 'down'])
load_e2e_config
関数はe2e.config.yml
を読み込んでワーカーIDをキーとした設定オブジェクトに変換する。
docker_compose_up
関数はload_e2e_config
関数で作った設定オブジェクトを利用して、必要な数だけdocker環境を立ち上げる。
docker_compose_down
関数は立ち上げていたdocker環境を停止するために使う。
これを使ってサーバ起動用スクリプトをstartup-servers
として用意した。
#!/usr/bin/env python3 from myapp.environment import load_e2e_config, docker_compose_up if __name__ == '__main__': config = load_e2e_config('./e2e.config.yml') docker_compose_up(config)
同じくサーバ停止用スクリプトをhalt-servers
として用意した。
#!/usr/bin/env python3 from todo.environment import load_e2e_config, docker_compose_down if __name__ == '__main__': config = load_e2e_config('./e2e.config.yml') docker_compose_down(config)
テストケースのコードではsetUpメソッドでload_e2e_config
関数を使って設定ファイルを読み込み、PYTEST_XDIST_WORKER
環境変数から自身のワーカーIDを取得して、接続先のWeb、DB、Redisのポートを取り出して使うようにした。
def setUp(self): target_port = '8080' db_port = '5432' redis_port = '6379' worker = os.environ.get('PYTEST_XDIST_WORKER') if worker is not None: config = load_e2e_config(E2E_CONFIG_PATH) c = config[worker] target_port = str(c['web_port']) db_port = str(c['db_port']) redis_port = str(c['redis_port'])
テストの実行
用意したスクリプトと設定で、テストの実行を行った。
$ ./startup-servers $ HEADLESS=1 py.test -n 4 test $ ./halt-servers
並列化なしで12分位だった実行時間が、ワーカー数2つの並列化で6分位、ワーカー数4つで3分位で終わるようになった。
ちなみに使ったWebDriverはChromeだけで、FirefoxとIE、Edgeは試していない。 Firefoxは大丈夫そうな気がするけど、IEとEdgeは駄目なんじゃないかと予想している。
まとめ
並列化して実行時間をいい感じに縮められた。
前回の参考リポジトリにも反映しておいたけど、こちらはそもそもDBとか使わないやつなのであまり恩恵はない。 とりあえずテンプレート用に入れておいた。
こちらは並列化なしの結果が次のような感じ。
$ HEADLESS=1 py.test todo/ ============================================================== test session starts =============================================================== platform linux -- Python 3.7.1, pytest-4.2.1, py-1.8.0, pluggy-0.9.0 Using --randomly-seed=1551505883 rootdir: /work/my-todo-2019-early/e2e, inifile: plugins: xdist-1.26.1, randomly-1.2.3, forked-1.0.2 collected 5 items todo/specs/todos_test.py ..... [100%] =========================================================== 5 passed in 16.32 seconds ============================================================
並列化あり(ワーカー数4つ)だと結果が次のような感じになる。
$ HEADLESS=1 py.test -n 4 todo/ ============================================================== test session starts =============================================================== platform linux -- Python 3.7.1, pytest-4.2.1, py-1.8.0, pluggy-0.9.0 Using --randomly-seed=1551505959 rootdir: /work/my-todo-2019-early/e2e, inifile: plugins: xdist-1.26.1, randomly-1.2.3, forked-1.0.2 gw0 [5] / gw1 [5] / gw2 [5] / gw3 [5] ..... [100%] ============================================================ 5 passed in 6.59 seconds ============================================================