RaspberryPiイメージを縮小してバックアップするプログラムを作ってみました
1 はじめに
CX事業本部の平内(SIN)です。
RaspberryPiは、Liteのような小さなイメージでも、初回起動時にSDカードの容量いっぱいまで広げられるので、使用中のイメージの複製やバックアップをしようとすると、カードのサイズに応じて大きなサイズを相手する事になってします。
使用していない領域を切り詰めて小さくバックアップする事は可能ですが、ブロックの縮小や、パーティションの変更がやや厄介です。 今回は、これを軽易に行えるようにプログラムしてみました。
動画は、これを利用している様子です。16GのSDカードに入ったイメージが2Gでバックアップされます。
2 構成
使用しているのは、RaspberryPi 3Bで、OSは、今年5月の最新版(Raspberry Pi OS (32-bit) with desktop and recommended software)です。
$ cat /proc/cpuinfo | grep Revision Revision : a32082 $ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster $ uname -a Linux raspberrypi 5.4.51-v7+ #1327 SMP Thu Jul 23 10:58:46 BST 2020 armv7l GNU/Linux
コピー元となるSDカードとコピー先となるドライブをUSBドライブに接続して使用する構成になっています。
3 作業内容
作業している内容は、概ね以下のとおりです。
- 元イメージを必要最小ブロックまで縮小
- 元イメージのパーティションを縮小
- ddによるイメージのコピー
- 元イメージのパーティションを拡大
- 元イメージのブロック数を拡大
詳しくは、下記で紹介させて頂いております。
4 コード
プログラムの本体です。
途中、コピー元のイメージのブロックサイズやパーティションの変更が行われますが、使用する環境によっては、誤動作する可能性もあります。その場合、手動での復旧が必要になります。申し訳ありませんが、利用は、自己責任とさせて下さい。
MinimumBackup.py
import logging import datetime import os import subprocess from subprocess import PIPE logLevel = "DEBUG" # logLevel = "INFO" logger = logging.getLogger("MinimumBackup") logger.setLevel(logLevel) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("[%(asctime)s] [%(process)d] [%(name)s] [%(levelname)s] %(message)s")) logger.addHandler(handler) def exec(cmd): logger.debug("> {}".format(cmd)) proc = subprocess.run(cmd, shell=True, stdout=PIPE, stderr=PIPE, text=True) return proc.stdout def getDevice(): logger.info("df コマンドにより/dev/sd*を検出し、入力(RasPiイメージ)と出力(USBドライブ)を検出する") src = None dst = None result = exec("df -H") for line in result.split('\n'): if('Filesystem' in line): logger.debug(line) if('/dev/sd' in line): logger.debug(line) tmp = line.split() stat = { "dev": tmp[0][0:9], "size": tmp[1], "used": tmp[2], "avail": tmp[3], "path": tmp[5] } if('/media/pi/rootfs' in line): src = stat else: if(('/media/pi/boot' in line) == False): dst = stat return (src, dst) def getMinimumBlock(dev): logger.info("resize2fsにより、必要最小サイズを確認する") result = exec("sudo resize2fs -P {}".format(dev)) logger.debug(result.replace('\n','')) return int(result.split()[6]) def resize(dev, newBlocks): logger.info("resize2fsにより サイズを変更する") os.system("sudo umount {}".format(dev)) os.system("sudo e2fsck -f {}".format(dev)) if(newBlocks!=None): os.system("sudo resize2fs -p {} {}".format(dev, newBlocks)) else: os.system("sudo resize2fs -p {}".format(dev)) def getBlock(dev): logger.info("fdiskにより、パーティション情報を取得する") result = exec('sudo fdisk -l {}'.format(dev[0:8])) for line in result.split('\n'): if(line.startswith('Device')): logger.debug(line) if(line.startswith('/dev/sd')): logger.debug(line) tmp = line.split() if(tmp[0] == dev): stat = { "start": int(tmp[1]), "end": int(tmp[2]), "sectors": int(tmp[3]), "size": tmp[4] } logger.debug(stat) return stat def question(msg): while True: data = input("{} [y/n]:".format(msg)).lower() if data in ['y', 'ye', 'yes']: return True elif data in ['n', 'no']: return False def changePermition(dev, start, end): logger.info("パーミッションを変更します {} start:{} end:{}".format(dev, start, end)) line = 'p\nd\n2\nn\n\n\n' # 既存のパーティションを削除し、新しく作成する line += str(start) # 開始ブロック line += '\n' line += str(end) # 終了ブロック line += '\n' line += 'N\np\nw\nq\n' # 確認(表示)と書き込み d = dev[0:8] os.system("echo \"{}\" | sudo fdisk /dev/sda".format(line)) def backup(srcDev, dstDev, fileName, count): cmd = "sudo dd bs=1048576 if={} of={}/{} count={}".format(srcDev[0:8], dstDev, fileName, count) logger.debug("?:{}".format(cmd)) os.system(cmd) def main(): logger.critical("*****************************") logger.critical(" MinimumBackup") logger.critical("*****************************") # 入出力デバイスの取得 (src, dst) = getDevice() if(src == None): logger.error("入力のRasPiのイメージディスクが見つかりません") exit() if(dst == None): logger.error("出力用のUSBドライブが見つかりません") exit() logger.info("入力(RasPI) {} size: {} used: {} {}".format(src["dev"], src["size"], src["used"], src["path"])) logger.info("出力(USBDrive) {} size: {} avail: {} {}".format(dst["dev"], dst["size"], dst["avail"], dst["path"])) # 必要最小ブロック数の取得 minimumBlocks = getMinimumBlock(src["dev"]) logger.info("必要最小サイズ: {:.1f}GB ({:,}) {:,}Blocks".format(minimumBlocks*4/1024/1024, minimumBlocks*4*1024, minimumBlocks)) # 必要ブロック数 = 必要最小ブロック+余裕(2048) requiredBlocks = minimumBlocks + 2048 logger.info("必要ブロック数:{} - 必要最小ブロック+2048(余裕)".format(requiredBlocks)) # 必要セクタ数 = 必要なブロック数 * 8 requiredSectors = requiredBlocks * 8 logger.info("必要セクタ数:{}".format(requiredSectors)) # 現在のパーティション状態を取得 beforeBlock = getBlock(src["dev"]) logger.info("変更前の{}の開始ブロック:{} 終了ブロック:{}".format(src["dev"], beforeBlock["start"], beforeBlock["end"])) afterBlock = { "start": beforeBlock["start"], "end": beforeBlock["start"] + requiredSectors, } logger.info("変更後の{}の開始ブロック:{} 終了ブロック:{}".format(src["dev"], afterBlock["start"], afterBlock["end"])) # 書き込みが必要なブロック数(Mbyte) = (最終セクタ / 2048 ) + 2(余裕) count = int(afterBlock["end"] / 2048) + 2 logger.info("バックアップするブロック数(Mbyte):{} ({:.1f}G)".format(count, count/1024)) # 出力ファイル名 now = datetime.datetime.now() fileName = now.strftime('raspi-%Y-%m-%d-%H-%M') + '.img' logger.critical("入力:{} {}".format(src["dev"],src["size"])) logger.critical("出力:{}/{} {:.1f}G (空き容量 {})".format(dst["path"], fileName, count/1024, dst["avail"])) logger.critical("作業中は、一時的にパーティションの変更が行われるので、中断しないで下さい") logger.critical("バックアップを開始して宜しいですか?[Y/N]") if(question("バックアップを開始して宜しいですか?") == False): exit() logger.info("Backup Start.") try: # ブロックサイズ変更 resize(src["dev"], requiredBlocks) # パーティション変更 changePermition(src["dev"], afterBlock["start"], afterBlock["end"]) # バックアップ logger.critical("バックアップ中です、そのままお待ち下さい") backup(src["dev"], dst["path"], fileName, count) finally: # パーティション変更(戻す) changePermition(src["dev"], beforeBlock["start"], beforeBlock["end"]) # ブロックサイズ変更(戻す) resize(src["dev"], None) output = "{}/{}".format(dst["path"], fileName) if(os.path.isfile(output) == False): logger.error("バックアップに失敗しました") exit() logger.critical("バックアップが完了しました {}".format(output)) main()
5 出力
ログレベルをDEBUGにした場合の出力は、以下のようになります。(各コマンドの出力は省略されています)
$ python3 MinimumBackup.py [2020-08-23 08:11:37,735] [1124] [MinimumBackup] [CRITICAL] ***************************** [2020-08-23 08:11:37,736] [1124] [MinimumBackup] [CRITICAL] MinimumBackup [2020-08-23 08:11:37,736] [1124] [MinimumBackup] [CRITICAL] ***************************** [2020-08-23 08:11:37,737] [1124] [MinimumBackup] [INFO] df コマンドにより/dev/sd*を検出し、入力(RasPiイメージ)と出力(USBドライブ)を検出する [2020-08-23 08:11:37,737] [1124] [MinimumBackup] [DEBUG] > df -H [2020-08-23 08:11:37,755] [1124] [MinimumBackup] [DEBUG] Filesystem Size Used Avail Use% Mounted on [2020-08-23 08:11:37,755] [1124] [MinimumBackup] [DEBUG] /dev/sdb1 16G 4.8G 12G 30% /media/pi/MYDISK [2020-08-23 08:11:37,756] [1124] [MinimumBackup] [DEBUG] /dev/sda2 16G 1.3G 14G 9% /media/pi/rootfs [2020-08-23 08:11:37,756] [1124] [MinimumBackup] [DEBUG] /dev/sda1 265M 55M 210M 21% /media/pi/boot [2020-08-23 08:11:37,757] [1124] [MinimumBackup] [INFO] 入力(RasPI) /dev/sda2 size: 16G used: 1.3G /media/pi/rootfs [2020-08-23 08:11:37,757] [1124] [MinimumBackup] [INFO] 出力(USBDrive) /dev/sdb1 size: 16G avail: 12G /media/pi/MYDISK [2020-08-23 08:11:37,758] [1124] [MinimumBackup] [INFO] resize2fsにより、必要最小サイズを確認する [2020-08-23 08:11:37,758] [1124] [MinimumBackup] [DEBUG] > sudo resize2fs -P /dev/sda2 [2020-08-23 08:11:37,860] [1124] [MinimumBackup] [DEBUG] Estimated minimum size of the filesystem: 428543 [2020-08-23 08:11:37,860] [1124] [MinimumBackup] [INFO] 必要最小サイズ: 1.6GB (1,755,312,128) 428,543Blocks [2020-08-23 08:11:37,861] [1124] [MinimumBackup] [INFO] 必要ブロック数:430591 - 必要最小ブロック+2048(余裕) [2020-08-23 08:11:37,861] [1124] [MinimumBackup] [INFO] 必要セクタ数:3444728 [2020-08-23 08:11:37,861] [1124] [MinimumBackup] [INFO] fdiskにより、パーティション情報を取得する [2020-08-23 08:11:37,861] [1124] [MinimumBackup] [DEBUG] > sudo fdisk -l /dev/sda [2020-08-23 08:11:38,070] [1124] [MinimumBackup] [DEBUG] Device Boot Start End Sectors Size Id Type [2020-08-23 08:11:38,071] [1124] [MinimumBackup] [DEBUG] /dev/sda1 8192 532479 524288 256M c W95 FAT32 (LBA) [2020-08-23 08:11:38,071] [1124] [MinimumBackup] [DEBUG] /dev/sda2 532480 30744575 30212096 14.4G 83 Linux [2020-08-23 08:11:38,072] [1124] [MinimumBackup] [DEBUG] {'start': 532480, 'end': 30744575, 'sectors': 30212096, 'size': '14.4G'} [2020-08-23 08:11:38,072] [1124] [MinimumBackup] [INFO] 変更前の/dev/sda2の開始ブロック:532480 終了ブロック:30744575 [2020-08-23 08:11:38,073] [1124] [MinimumBackup] [INFO] 変更後の/dev/sda2の開始ブロック:532480 終了ブロック:3977208 [2020-08-23 08:11:38,073] [1124] [MinimumBackup] [INFO] バックアップするブロック数(Mbyte):1943 (1.9G) [2020-08-23 08:11:38,074] [1124] [MinimumBackup] [CRITICAL] 入力:/dev/sda2 16G [2020-08-23 08:11:38,074] [1124] [MinimumBackup] [CRITICAL] 出力:/media/pi/MYDISK/raspi-2020-08-23-08-11.img 1.9G (空き容量 12G) [2020-08-23 08:11:38,075] [1124] [MinimumBackup] [CRITICAL] 作業中は、一時的にパーティションの変更が行われるので、中断しないで下さい [2020-08-23 08:11:38,075] [1124] [MinimumBackup] [CRITICAL] バックアップを開始して宜しいですか?[Y/N] バックアップを開始して宜しいですか? [y/n]:y [2020-08-23 08:11:43,784] [1124] [MinimumBackup] [INFO] Backup Start. [2020-08-23 08:11:44,045] [1124] [MinimumBackup] [INFO] resize2fsにより サイズを変更する [2020-08-23 08:12:05,335] [1124] [MinimumBackup] [INFO] パーミッションを変更します /dev/sda2 start:532480 end:3977208 [2020-08-23 08:12:05,688] [1124] [MinimumBackup] [CRITICAL] バックアップ中です、そのままお待ち下さい [2020-08-23 08:12:05,689] [1124] [MinimumBackup] [DEBUG] ?:sudo dd bs=1048576 if=/dev/sda of=/media/pi/MYDISK/raspi-2020-08-23-08-11.img count=1943 1943+0 records in 1943+0 records out 2037383168 bytes (2.0 GB, 1.9 GiB) copied, 315.226 s, 6.5 MB/s [2020-08-23 08:17:21,112] [1124] [MinimumBackup] [INFO] パーミッションを変更します /dev/sda2 start:532480 end:30744575 [2020-08-23 08:17:21,395] [1124] [MinimumBackup] [INFO] resize2fsにより サイズを変更する [2020-08-23 08:17:43,662] [1124] [MinimumBackup] [CRITICAL] バックアップが完了しました /media/pi/MYDISK/raspi-2020-08-23-08-11.img
6 コピーされたイメージ
USBメモリに保存されたイメージは、2.04Gでした。また、圧縮すると666MBとなってました。
balenaEtcherで別のSDカードにコピーします。
コピーされたイメージを起動すると、ディスクのサイズが、1.8Gになっています。 これは、縮小されたイメージをコピーしているからです。
pi@raspberrypi:~ $ df -H Filesystem Size Used Avail Use% Mounted on /dev/root 1.8G 1.3G 368M 78% / devtmpfs 481M 0 481M 0% /dev tmpfs 486M 0 486M 0% /dev/shm tmpfs 486M 13M 474M 3% /run tmpfs 5.3M 4.1k 5.3M 1% /run/lock tmpfs 486M 0 486M 0% /sys/fs/cgroup /dev/mmcblk0p1 265M 55M 210M 21% /boot tmpfs 98M 0 98M 0% /run/user/1000
もし、SDカードのサイズまで拡張するとしたら、以下の手順が必要です。
- fdiskでパーティションを広げる
- resize2fsでブロックを拡張する
(1) fdisk
以下がfdiskでパーティションを拡張している様子です。
縮小されている2つ目のパーティションを一旦削除して、改めて最大サイズで作成しています。
pi@raspberrypi:~ $ sudo fdisk /dev/mmcblk0 Welcome to fdisk (util-linux 2.33.1). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Command (m for help): p Disk /dev/mmcblk0: 14.9 GiB, 15931539456 bytes, 31116288 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: 0x18ed9157 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 532479 524288 256M c W95 FAT32 (LBA) /dev/mmcblk0p2 532480 3977216 3444737 1.7G 83 Linux Command (m for help): d Partition number (1,2, default 2): 2 Partition 2 has been deleted. Command (m for help): p Disk /dev/mmcblk0: 14.9 GiB, 15931539456 bytes, 31116288 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: 0x18ed9157 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 532479 524288 256M c W95 FAT32 (LBA) Command (m for help): n Partition type p primary (1 primary, 0 extended, 3 free) e extended (container for logical partitions) Select (default p): p Partition number (2-4, default 2): 2 First sector (2048-31116287, default 2048): 532480 Last sector, +/-sectors or +/-size{K,M,G,T,P} (532480-31116287, default 31116287): Created a new partition 2 of type 'Linux' and of size 14.6 GiB. Partition #2 contains a ext4 signature. Do you want to remove the signature? [Y]es/[N]o: N Command (m for help): p Disk /dev/mmcblk0: 14.9 GiB, 15931539456 bytes, 31116288 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: 0x18ed9157 Device Boot Start End Sectors Size Id Type /dev/mmcblk0p1 8192 532479 524288 256M c W95 FAT32 (LBA) /dev/mmcblk0p2 532480 31116287 30583808 14.6G 83 Linux Command (m for help): w The partition table has been altered. Syncing disks.
(2) resize2fs
resize2fsは、パラメータ無しで実行すると、ブロックは、最大サイズまで拡張されます
$ sudo resize2fs /dev/mmcblk0p2 resize2fs 1.44.5 (15-Dec-2018) Filesystem at /dev/mmcblk0p2 is mounted on /; on-line resizing required old_desc_blocks = 1, new_desc_blocks = 1 The filesystem on /dev/mmcblk0p2 is now 3822976 (4k) blocks long.
以上の作業で、ディスクサイズは、SDカードの容量である16Gまで拡張されます。
pi@raspberrypi:~ $ df -H Filesystem Size Used Avail Use% Mounted on /dev/root 16G 1.3G 14G 9% / devtmpfs 481M 0 481M 0% /dev tmpfs 486M 0 486M 0% /dev/shm tmpfs 486M 13M 474M 3% /run tmpfs 5.3M 4.1k 5.3M 1% /run/lock tmpfs 486M 0 486M 0% /sys/fs/cgroup /dev/mmcblk0p1 265M 55M 210M 21% /boot tmpfs 98M 0 98M 0% /run/user/1000
7 最後に
今回は、縮小したイメージをバックアップする手順をシェル化してみました。 当初、コピー後のイメージを自動で拡張するシェルも組み込んでいたのですが、こちら安定した動作が出来なくて外しました。
もし、既に使用されているイメージで、残りのディスクサイズに、それほど余裕が必要無いのであれば、縮小されたまま利用することも可能です。コード内の下記の部分を編集すると、ディスクの余裕を調整できます。
# 必要ブロック数 = 必要最小ブロック+余裕(2048) requiredBlocks = minimumBlocks + 2048
これで、ブロックやセクタの計算から開放されるので、仕事が捗りそうです。
※ 最悪、パーティションが壊れて起動できなくなったりした場合、手動で元に戻せるようにするため、作業前にパーティションの状況を確認しておくと安全かも知れません。