ラズベリーパイをラズベリーパイで焼く仕組みを作った
ラズベリーパイの初期設定をするラズベリーパイを作りました。
といっても見た目にインパクトがあるわけではありませんが...
(上のRaspberryPi 3Aが本記事の対象マシンです)
以下のようなデーモンが動いており、USB接続を自動で感知&SDカードに書き込みを行ってくれます。
※Cloud-initのバグについては参考を確認してください
概要
目的
ラズパイをサーバーとしてセットアップする際、色々面倒くさいです。
ubuntu serverを初期設定する手順をざっと羅列するだけでも...
- SDカードをSDカードリーダーに差し込む
- RaspberryPi Imagerを使ってOSを選択
- 管理者権限ウィンドウにて書き込みを許可
(SDカードにOSを書き込み) - SDカードを再挿入(ソフトウェアがアンマウントを実行するため)
- network-configを設定(ヘッドレスインストールですぐssh接続できるようにするため)
- user-dataを設定(ssh鍵認証の有効化を初期段階で実施するため)
- SDカードをラズパイに差す
- ラズパイを起動
1台ならともかく複数台設定する場合にいちいちこの操作をするのは面倒くさいため、
これを以下のような手順で、SDカードを差し込んだらすべてが完了するようにしてしまうことが目的です。
- SDカードをSDカードリーダーに差し込む
(OSの書きこみ・あらかじめ用意されたnetwork-config/user-dataを自動で書き込み) - SDカードをラズパイに差す
- ラズパイを起動
実施環境
- RaspberryPi 3A+
- OS: Ubuntu server 22.04.1 LTS (64 bit)
- 8GB SD card
root@ubuntu:~# python3 --version
Python 3.10.6
成果物
リポジトリは以下になります。
フィジビリティ確認のみでテストをしていないのでreleaseはしていません。
README.mdにインストール・設定・アンインストール方法を記載しています。
技術的な話
細かなところは既存の技術の寄せ集めです。本記事ではjinja2とcallback関数について触れておきます。
初期設定について
jinja2の利用
設定値の作成はjinja2 templateを利用しました。ansibleではおなじみです。
こちらはpythonから呼び出して利用することができます。
# -*- coding: utf-8 -*-
from PiSDWriter import global_vars as g
import jinja2, yaml, subprocess, os
# 設定値の読み込み部分
def load_vars():
main_config_path = g.conf_dir + '/main.yml'
wifi_config_path = g.conf_dir + '/wifi.yml'
config = yaml.safe_load(open(main_config_path)) # 辞書型として格納される
# ...(wifi読み込み部分。省略)...
return config
#...(中略)...
# ファイルの書き込み部分
def write_config(file_name, outconf_path):
templates_path = g.template_dir # templateまでのフルパス
os.makedirs(outconf_path, exist_ok=True)
fileSystemLoader = jinja2.FileSystemLoader(searchpath=templates_path) # FileSystemLoaderを用意
env = jinja2.Environment(loader=fileSystemLoader) # FileSystemLoaderを用いてrendererを用意
template = env.get_template(file_name+'.j2') # templateをロード
template_vars = load_vars() # dictを格納
with open(outconf_path + '/' + file_name, 'w') as file:
file.write(template.render(template_vars)) # dictを用いてレンダリングした結果を書き込み
print("config: " + file_name + " is ready")
本プログラムでの実装
設定をjinja2を用いて作成している部分がミソです。このためユーザーが作成する設定はキーと値のセットのみになります。
本プログラムでは以下のような設定ファイルを用います。
gateway_addr: 192.168.3.1 #デフォルトゲートウェイ
nameserver_addrs: #DNSサーバ 2台まで設定可能
- 8.8.8.8
- 8.8.4.4
authorized_keys: #初期設定ユーザの公開鍵を直接記載
- ssh-XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ip_address: 192.168.3.XX #IPアドレス
今のところIPアドレスはファーストアクセス後に変更する前提のため、SDカードを焼いたら固定のIPアドレスが付与されます。
ユーザー名も同じ理由で固定していますが、インストール時に作成されるtemplates
内のjinjaを設定することで変数の定義・カスタマイズができます。
#...(省略)...
hostname: ubuntu
timezone: "Asia/Tokyo"
locale: "ja_JP.UTF-8"
package_update: true
package_upgrade: true
password: ubuntu
chpasswd:
expire: false
ssh_pwauth: false
ssh_authorized_keys: {{ authorized_keys }}
write_files:
- path: /boot/firmware/config.txt
append: true
content: |
dtparam=audio=on
dtparam=act_led_trigger=default-on
dtparam=pwr_led_trigger=panic
runcmd:
- sed -i -e 's/$/ cgroup_enable=memory cgroup_memory=1/' /boot/firmware/cmdline.txt
- touch /etc/cloud/cloud-init.disabled
- reboot
実行部分
subprocessを用いてひたすら処理を行う仕組みになっています。
callback関数について
コールバック関数は、関数の引数に関数(厳密には関数のポインタ)を利用することで、関数内で関数を呼び出せる仕組みです。
pythonでは次のようにして利用することができます。
def example_add(b,c):
return b + c
def example_func(a, function, *args):
print(a)
print("{0} + {1} = ".format(args[0],args[1]))
# print("{0} + {1} = ".format(*args)) でもよい
return function(*args)
callback_func = example_add
example_func("add arguments", callback_func, 2, 3)
上記の結果では以下の出力を得られます。
>>> callback_func = example_add
>>> example_func("add arguments", callback_func, 2, 3)
add arguments # example_funcのprint出力
2 + 3 = # example_funcのprint出力
5 # example_func内のfunction = callback_func = example_addの結果出力
本プログラムでの実装
本プログラムではマウント/アンマウントの部分に用いています。
Cloud-initの更新と設定値(network-config
,user-data
)の書き込みについて、
マウント処理が共通しているためです。
# -*- coding: utf-8 -*-
import subprocess, os, tempfile, re, pathlib
from tqdm import tqdm
# ...(省略)...
def __mount_and_write_sdcard(device_node, function, *args):
# ...(マウント処理)...
function(dirpath, *args)
# ...(アンマウント処理)...
def __open_gz_copy_to_sdcard(mntdir, dest, src, pattern):
# ...(subprocessで tar展開やcpを実行)...
def __open_conf_copy_to_sdcard(mntdir, dest, src):
# ...(subprocessでcpを実行)
## cloud-initの書き込みを行う関数の呼び出し部分
def write_cloudinit_to_sdcard(device_node, dest, src, pattern):
if os.path.isfile(src):
callback = __open_gz_copy_to_sdcard ## callback変数に__open_gz_copy_to_sdcardを代入
__mount_and_write_sdcard(device_node,callback,dest,src,pattern)
## 各種設定の書き込みを行う関数の呼び出し部分
def write_configs_to_sdcard(device_node,dest,*src):
callback = __open_conf_copy_to_sdcard ## callback変数に __open_conf_copy_to_sdcardを代入
__mount_and_write_sdcard(device_node,callback,dest,*src)
複雑そうに見えますが、実際のところは
-
write_cloudinit_to_sdcardが呼び出された場合
- __mount_and_write_sdcardが実行される
- __mount_and_write_sdcardの処理中、__open_gz_copy_to_sdcardが呼び出される
- callback関数終了後、__mount_and_write_sdcardの残りの処理が実行される
-
write_configs_to_sdcardが呼び出された場合
- __mount_and_write_sdcardが実行される
- __mount_and_write_sdcardの処理中、__open_conf_copy_to_sdcardが呼び出される
- callback関数終了後、__mount_and_write_sdcardの残りの処理が実行される
という具合の処理を行っています。
インストールに関して
パッケージのインストーラとしてsetup.pyを利用しています。
モジュール外のファイルの追加については[options.package_data]
で行っています。
以下のファイル構成をしている時
/
src/
PiSDWriter/
configs/
***.yml
templates/
****
hoge.py
fuga.py
main.py
__init__.py
README
setup.py
setup.cfg
以下のような設定を行っています。
[metadata]
name = PiSDWriter
version = 0.0.1
description = write RaspberryPi SD easiry
license='MIT'
[options]
install_requires =
jinja2
pyyaml
requests
tqdm
pyudev
packages = find:
package_dir=
=src
zip_safe = False
[options.package_data]
PiSDWriter =
configs/*.template
templates/*
[options.packages.find]
where=src
[options.entry_points]
console_scripts =
pisdwriter = PiSDWriter.main:main
[options.entry_points]
を設定することでパスを自動で通してくれます。
今後の課題
- ユーザー設定の作成難易度を下げたい
- ウィザード形式のセットアップ画面があってもいいかもしれない
- IPアドレスのマネジメント
- SDカードを差し込む際にIPアドレスを何かしらの形で設定出来たらうれしい
- せっかくRaspberryPiなのでGPIOを用いた入力デバイスがあってもいいかもしれない
- 終了通知
- 現在はSDカードリーダーの光が点滅から点灯になることで終了を認識
- 時間がかかる処理ではないが、書き込んだこと自体を通知できたらもっと便利かも?
- 追記: 2023/11/04 LINE Notification APIを用いて通知を送信できるようにした。
終わりに
ラズパイで中規模のクラスタを作る人は、SDカードの初期セットアップに苦労されると思います(実際してました)
これでUIを触ることなくRaspberryPiをセットアップできます。
参考
オフィシャルイメージのCloud-initでは、Wifiを初回起動時に設定できないバグが存在する。
(この現象はUbuntu20.4、22.4のいずれのLTSで確認しています。いずれも最新版のCloud-initのインストールで解消する模様)
Discussion