おうちNASの監視・運用 ~mdadm~

## はじめに

おうちNASとして使っているOpenMediaVaultの監視・運用方法をまとめます。
`mdadm`を使ったHDDの異常監視や通知の設定は、意外と日本語の情報が少なかったので備忘録がわりに載せておきます。

#### 本記事の概要

- RAIDのベリファイ　`mdcheck_start.timer/mdcheck_continue.timer`を利用
- 異常監視　　　　　`mdmonitor.service`を利用し、LINEで通知
- 定期報告　　　　　`cron`を利用し、Discordで通知


#### 使用OS
・Debian 12 （未検証ですが、本記事の内容はUbuntuでも多分同じです）

#### 使用ソフトウェア
・mdadm 4.2-5
・OpenMediaVault 7.7.7-1

<details><summary>バージョン情報の詳細は折りたたみにしておきます</summary>

```
root@debian:~# cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
root@debian:~#
root@debian:~# apt show mdadm
Package: mdadm
Version: 4.2-5
Priority: optional
Section: admin
Maintainer: Daniel Baumann <daniel.baumann@progress-linux.org>
Installed-Size: 1266 kB
Pre-Depends: init-system-helpers (>= 1.54~)
Depends: debconf (>= 0.5) | debconf-2.0, libc6 (>= 2.34), libudev1 (>= 183), debconf, lsb-base, udev
Recommends: default-mta | mail-transport-agent, kmod | module-init-tools
Suggests: dracut-core
Homepage: https://raid.wiki.kernel.org/index.php/Linux_Raid
Tag: admin::boot, admin::configuring, hardware::storage, implemented-in::c,
 implemented-in::shell, interface::commandline, interface::daemon,
 role::program, scope::utility, use::configuring, use::monitor
Download-Size: 443 kB
APT-Manual-Installed: yes
APT-Sources: http://http.us.debian.org/debian bookworm/main amd64 Packages
Description: Tool to administer Linux MD arrays (software RAID)
 The mdadm utility can be used to create, manage, and monitor MD
 (multi-disk) arrays for software RAID or multipath I/O.
 .
 This package automatically configures mdadm to assemble arrays during the
 system startup process. If not needed, this functionality can be disabled.

root@debian:~#
root@debian:~# apt show openmediavault
Package: openmediavault
Version: 7.7.7-1
Priority: optional
Section: admin
Maintainer: Volker Theile <volker.theile@openmediavault.org>
Installed-Size: 13.8 MB
Depends: php-fpm, libpam-modules, php-json, dpkg, patch, dash, mawk | gawk, php-cgi, php-cli, php-mbstring, php-pam, sudo, ethtool, python3-dialog, acl, iproute2, xfsprogs, jfsutils, ntfs-3g, wsdd (>= 0.7.0), sdparm, mdadm, postfix, libsasl2-modules, bsd-mailx, python3-dbus, cpufrequtils, rsyslog, logrotate, smartmontools, openssl, openssh-server, uuid, tzdata, nfs-kernel-server, lsb-release, sshpass, grep, zstd, wget, util-linux, samba, samba-common-bin, rsync, apt-utils, netplan.io, avahi-daemon, libnss-mdns, iptables, monit, beep, php-bcmath, sed, gdisk, rrdtool, collectd, cron, anacron, quota, php-xml, login, quotatool, whiptail, lvm2, ca-certificates, libc-bin, findutils, coreutils, xmlstarlet, mount, parted, bash, diffutils, lsof, rrdcached, locales, nginx, bash-completion, python3, f2fs-tools, python3-apt, wpasupplicant, systemd, systemd-sysv, btrfs-progs, samba-vfs-modules, pciutils, python3-pyudev, python3-natsort, jq, chrony, python3-netifaces, udev, python3-lxml, salt-minion (>= 3006.0), libnss-myhostname, php-yaml, python3-click, python3-cached-property, python3-polib, libnss-systemd, libnss-resolve, debian-archive-keyring, openmediavault-keyring, systemd-resolved, python3-packaging, unattended-upgrades, apt-listchanges, apticron, debconf (>= 0.5) | debconf-2.0
Conflicts: gdm3, lightdm, lxdm, sddm, slim, wdm, xdm
Homepage: http://www.openmediavault.org
Download-Size: 3085 kB
APT-Manual-Installed: yes
APT-Sources: http://packages.openmediavault.org/public sandworm/main amd64 Packages
Description: openmediavault - The open network attached storage solution
 openmediavault is a small and simple to use network attached storage system
 with a smart WebGUI.

root@debian:~#
```

</details>


## RAIDのベリファイ

RAIDを使用するにあたっては**定期的なベリファイ（スクラビング、整合性チェック）** が必要です。
詳細は[こちらの記事](https://tagomoris.hatenablog.com/entry/20110419/1303181958)を読んでほしいのですが、要は**データを読み書きする時にしか故障を検知できない**ので、定期的にHDDの全域を読みにいくことで故障を検知するということになります。

ベリファイ自体は`echo "check" >/sys/block/md*/md/sync_action`をするだけで即座に実行されます。

実際に試してみます。
以下のように4つのRAIDアレイがある状態とします。

```
root@debian:~# cat /proc/mdstat 
Personalities : [raid1] [raid6] [raid5] [raid4] [linear] [multipath] [raid0] [raid10] 
md3 : active raid6 sdc4[2] sdd4[3] sdb4[1] sda4[0]
      1927868416 blocks super 1.2 level 6, 512k chunk, algorithm 2 [4/4] [UUUU]
      bitmap: 0/8 pages [0KB], 65536KB chunk

md2 : active (auto-read-only) raid1 sdc3[2] sdd3[3] sdb3[1] sda3[0]
      1950720 blocks super 1.2 [4/4] [UUUU]
      
md0 : active raid1 sdc1[2] sdd1[3] sdb1[1] sda1[0]
      974848 blocks super 1.2 [4/4] [UUUU]
      
md1 : active raid1 sdc2[2] sdd2[3] sdb2[1] sda2[0]
      9756672 blocks super 1.2 [4/4] [UUUU]
      
unused devices: <none>
root@debian:~#
```

`md0`のベリファイを行ってみましょう。

```
root@debian:~# echo "check" > /sys/block/md0/md/sync_action
root@debian:~# cat /proc/mdstat 
Personalities : [raid1] [raid6] [raid5] [raid4] [linear] [multipath] [raid0] [raid10] 
md3 : active raid6 sdc4[2] sdd4[3] sdb4[1] sda4[0]
      1927868416 blocks super 1.2 level 6, 512k chunk, algorithm 2 [4/4] [UUUU]
      bitmap: 0/8 pages [0KB], 65536KB chunk

md2 : active (auto-read-only) raid1 sdc3[2] sdd3[3] sdb3[1] sda3[0]
      1950720 blocks super 1.2 [4/4] [UUUU]
      
md0 : active raid1 sdc1[2] sdd1[3] sdb1[1] sda1[0]
      974848 blocks super 1.2 [4/4] [UUUU]
      [===>.................]  check = 18.4% (179712/974848) finish=0.1min speed=89856K/sec
      
md1 : active raid1 sdc2[2] sdd2[3] sdb2[1] sda2[0]
      9756672 blocks super 1.2 [4/4] [UUUU]
      
unused devices: <none>
root@debian:~# 
```

`cat /proc/mdstat`の実行結果が変わりました。
このようにベリファイ自体は簡単に実行できますし、ベリファイ後に`cat /proc/mdstat`をすることで、異常なデバイスがある（`md0`が`UUUU`ではなく`UUU_`とかになる）ことに気付けます。

これを自動で行うbashスクリプトを自分で書いても良いのですが、mdadmを入れた時点で**すでにそのようなサービスが入っているので使っていこう**というのが今回の趣旨になります。

具体的には`mdcheck_start.timer`と`mdcheck_start.service`です。

Timerユニットに馴染みのない方は[Arch Wiki](https://wiki.archlinux.jp/index.php/Systemd/%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC)を読んで欲しいのですが、要は実行内容を`mdcheck_start.service`を書き、その実行のトリガー（起動時間）を同名の`mdcheck_start.timer`に書く構成になっています。

中身を見ていきましょう。
まずは簡単な方の`mdcheck_start.timer`から。

```
root@debian:~# systemctl cat mdcheck_start.timer
# /lib/systemd/system/mdcheck_start.timer
#  This file is part of mdadm.
#
#  mdadm is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.

[Unit]
Description=MD array scrubbing

[Timer]
OnCalendar=Sun *-*-1..7 1:00:00

[Install]
WantedBy= mdmonitor.service
Also= mdcheck_continue.timer
root@debian:~# 
```

[Timer]セクションが一番大事で、**毎月の第1週の日曜日の1時に`mdcheck_start.service`を起動する**ということが書かれています。

次に`mdcheck_start.service`を見ていきましょう。

```
root@debian:~# systemctl cat mdcheck_start.service
# /lib/systemd/system/mdcheck_start.service
#  This file is part of mdadm.
#
#  mdadm is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.

[Unit]
Description=MD array scrubbing
Wants=mdcheck_continue.timer
Documentation=man:mdadm(8)

[Service]
Type=oneshot
Environment="MDADM_CHECK_DURATION=6 hours"
EnvironmentFile=-/run/sysconfig/mdadm
ExecStartPre=-/usr/lib/mdadm/mdadm_env.sh
ExecStart=/usr/share/mdadm/mdcheck --duration ${MDADM_CHECK_DURATION}
```

少しごちゃごちゃしていますが、要は最終行にあるExecStartの`/usr/share/mdadm/mdcheck --duration "6 hours"`を実行しているだけです。
ではこの`/usr/share/mdadm/mdcheck`って何だということになるのですが、これはbashスクリプトになっています。
内容は以下です。

<details><summary>mdcheckスクリプトの中身が長いので折りたたみ</summary>

```.bash
root@debian:~# cat /usr/share/mdadm/mdcheck
#!/bin/bash

# Copyright (C) 2014-2017 Neil Brown <neilb@suse.de>
#
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    Author: Neil Brown
#    Email: <neilb@suse.com>

# This script should be run periodically to automatically
# perform a 'check' on any md arrays.
#
# It supports a 'time budget' such that any incomplete 'check'
# will be checkpointed when that time has expired.
# A subsequent invocation can allow the 'check' to continue.
#
# Options are:
#   --continue    Don't start new checks, only continue old ones.
#   --duration    This is passed to "date --date=$duration" to find out
#		  when to finish
#
# To support '--continue', arrays are identified by UUID and the 'sync_completed'
# value is stored  in /var/lib/mdcheck/$UUID

# convert a /dev/md name into /sys/.../md equivalent
sysname() {
	set `ls -lLd $1`
	maj=${5%,}
	min=$6
	readlink -f /sys/dev/block/$maj:$min
}

args=$(getopt -o hcd: -l help,continue,duration: -n mdcheck -- "$@")
rv=$?
if [ $rv -ne 0 ]; then exit $rv; fi

eval set -- $args

cont=
endtime=
while [ " $1" != " --" ]
do
    case $1 in
	--help )
		echo >&2 'Usage: mdcheck [--continue] [--duration time-offset]'
		echo >&2 '  time-offset must be understood by "date --date"'
		exit 0
		;;
	--continue ) cont=yes ;;
	--duration ) shift; dur=$1
		endtime=$(date --date "$dur" "+%s")
		;;
    esac
    shift
done
shift

# We need a temp file occasionally...
tmp=/var/lib/mdcheck/.md-check-$$
trap 'rm -f "$tmp"' 0 2 3 15


# firstly, clean out really old state files
mkdir -p /var/lib/mdcheck
find /var/lib/mdcheck -name "MD_UUID*" -type f -mtime +180 -exec rm {} \;

# Now look at each md device.
cnt=0
for dev in /dev/md?*
do
	[ -e "$dev" ] || continue
	sys=`sysname $dev`
	if [ ! -f "$sys/md/sync_action" ]
	then # cannot check this array
		continue
	fi
	if [ "`cat $sys/md/sync_action`" != 'idle' ]
	then # This array is busy
		continue
	fi

	mdadm --detail --export "$dev" | grep '^MD_UUID=' > $tmp || continue
	source $tmp
	fl="/var/lib/mdcheck/MD_UUID_$MD_UUID"
	if [ -z "$cont" ]
	then
		start=0
		logger -p daemon.info mdcheck start checking $dev
	elif [ -z "$MD_UUID" -o ! -f "$fl" ]
	then
		# Nothing to continue here
		continue
	else
		start=`cat "$fl"`
		logger -p daemon.info mdcheck continue checking $dev from $start
	fi

	cnt=$[cnt+1]
	eval MD_${cnt}_fl=\$fl
	eval MD_${cnt}_sys=\$sys
	eval MD_${cnt}_dev=\$dev
	echo $start > $fl
	echo $start > $sys/md/sync_min
	echo check > $sys/md/sync_action
done

if [ -z "$endtime" ]
then
	exit 0
fi

while [ `date +%s` -lt $endtime ]
do
	any=
	for i in `eval echo {1..$cnt}`
	do
		eval fl=\$MD_${i}_fl
		eval sys=\$MD_${i}_sys
		eval dev=\$MD_${i}_dev

		if [ -z "$fl" ]; then continue; fi

		if [ "`cat $sys/md/sync_action`" != 'check' ]
		then
			logger -p daemon.info mdcheck finished checking $dev
			eval MD_${i}_fl=
			rm -f $fl
			continue;
		fi
		read a rest < $sys/md/sync_completed
		echo $a > $fl
		any=yes
	done
	if [ -z "$any" ]; then exit 0; fi
	sleep 120
done

# We've waited, and there are still checks running.
# Time to stop them.
for i in `eval echo {1..$cnt}`
do
	eval fl=\$MD_${i}_fl
	eval sys=\$MD_${i}_sys
	eval dev=\$MD_${i}_dev

	if [ -z "$fl" ]; then continue; fi

	if [ "`cat $sys/md/sync_action`" != 'check' ]
	then
		eval MD_${i}_fl=
		rm -f $fl
		continue;
	fi
	echo idle > $sys/md/sync_action
	cat $sys/md/sync_min > $fl
	logger -p daemon.info pause checking $dev at `cat $fl`
done
root@debian:~#
```

</details>

このスクリプトの詳細な解説はChatGPTにでも聞けばわかるので省略します。

簡単に説明すると
- 各mdデバイスに対して`/sys/block/md*/md/sync_action`に`check`を入れて、`duration`の時間だけベリファイをする
- `duration`の時間を超えたら`/sys/block/md*/sync_action`に`idle`を入れて、ベリファイを止める

というスクリプトです。

デフォルトでは`mdcheck_start.timer`はdisableされているので有効化しておきます。

```
root@debian:~# systemctl status mdcheck_start.service
○ mdcheck_start.service - MD array scrubbing
     Loaded: loaded (/lib/systemd/system/mdcheck_start.service; static)
     Active: inactive (dead)
       Docs: man:mdadm(8)
root@debian:~# systemctl status mdcheck_start.timer
○ mdcheck_start.timer - MD array scrubbing
     Loaded: loaded (/lib/systemd/system/mdcheck_start.timer; disabled; preset: enabled)
     Active: inactive (dead)
    Trigger: n/a
   Triggers: ● mdcheck_start.service

root@debian:~# systemctl enable mdcheck_start.timer
Created symlink /etc/systemd/system/mdmonitor.service.wants/mdcheck_start.timer → /lib/systemd/system/mdcheck_start.timer.
Created symlink /etc/systemd/system/mdmonitor.service.wants/mdcheck_continue.timer → /lib/systemd/system/mdcheck_continue.timer.
root@debian:~# 
```

ちゃんとタイマーの開始もしておきます。

```
root@debian:~# systemctl start mdcheck_start.timer
```

次に実行されるタイマーの詳細は以下のコマンドでわかります。

```
root@debian:~# systemctl list-timers 
NEXT                        LEFT               LAST                        PASSED       UNIT                         ACTIVATES                     
Sat 2025-06-07 00:39:00 JST 19min left         Sat 2025-06-07 00:09:05 JST 10min ago    phpsessionclean.timer        phpsessionclean.service
（中略）
Mon 2025-06-09 01:24:00 JST 2 days left        Tue 2025-06-03 16:23:58 JST 3 days ago   fstrim.timer                 fstrim.service
Sun 2025-07-06 01:00:00 JST 4 weeks 1 day left -                           -            mdcheck_start.timer          mdcheck_start.service

11 timers listed.
Pass --all to see loaded but inactive timers, too.

root@debian:~#
```

一番最後に`mdcheck_start.timer`が入ったことがわかります。今回実行したのが2025年6月7日（土）なので来月の第1週の日曜は2025年7月6日（日）になります。

もし実行時刻を変えたい方は`systemctl edit --full mdcheck_start.timer`で修正してください。[^1]
例えば、第一金曜と第三金曜の0時にベリファイをしたい場合は[Timer]セクションを以下のように変更します。

[^1]:`--full`オプションをつけないと既存のタイマー（今回だと第一週目の日曜）の設定が消えずに追記という形になるので注意してください。普通に`/etc/systemd/system/mdcheck_start.timer`を直接変更しても良いですが、`systemctl daemon-reload'`が必要になります。


```/etc/systemd/system/mdcheck_start.timerの抜粋
[Timer]
OnCalendar=Fri *-*-1..7 00:00:00
OnCalendar=Fri *-*-15..21 00:00:00
```

:::note info
`systemctl enable mdcheck_start.service`はできないので注意です。
以下を読めばわかります。
```
root@debian:~# systemctl enable mdcheck_start.service
The unit files have no installation config (WantedBy=, RequiredBy=, Also=,
Alias= settings in the [Install] section, and DefaultInstance= for template
units). This means they are not meant to be enabled using systemctl.
 
Possible reasons for having this kind of units are:
• A unit may be statically enabled by being symlinked from another unit's
  .wants/ or .requires/ directory.
• A unit's purpose may be to act as a helper for some other unit which has
  a requirement dependency on it.
• A unit may be started when needed via activation (socket, path, timer,
  D-Bus, udev, scripted systemctl call, ...).
• In case of template units, the unit is meant to be enabled with some
  instance name specified.
root@debian:~# 
```

:::

実は`mdcheck_start.service`と`mdcheck_start.timer`にだけに留まらず、`mdcheck_continue.service`と`mdcheck_cotinue.timer`もあります。
ただこちらはそんなに難しくはないので安心してください。

`mdcheck_start.service`で実行したベリファイはdurationの時間を超えると止まってしまうので、定期的（デフォルトだと毎日1時5分）に**ベリファイを再開させる**というだけの動作になります。

まずは簡単な`timer`の方を見ましょう。

```
root@debian:~# systemctl cat mdcheck_continue.timer 
# /lib/systemd/system/mdcheck_continue.timer
#  This file is part of mdadm.
#
#  mdadm is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.

[Unit]
Description=MD array scrubbing - continuation

[Timer]
OnCalendar= 1:05:00

[Install]
WantedBy= mdmonitor.service
root@debian:~# 
```

`OnCalendar= 1:05:00`とあるように、毎日1時5分に`mdcheck_continue.service`が実行されるというだけです。

`mdcheck_continue.service`の方はどうでしょうか。

```
root@debian:~# systemctl cat mdcheck_continue.service
# /lib/systemd/system/mdcheck_continue.service
#  This file is part of mdadm.
#
#  mdadm is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.

[Unit]
Description=MD array scrubbing - continuation
ConditionPathExistsGlob=/var/lib/mdcheck/MD_UUID_*
Documentation=man:mdadm(8)

[Service]
Type=oneshot
Environment="MDADM_CHECK_DURATION=6 hours"
EnvironmentFile=-/run/sysconfig/mdadm
ExecStartPre=-/usr/lib/mdadm/mdadm_env.sh
ExecStart=/usr/share/mdadm/mdcheck --continue --duration ${MDADM_CHECK_DURATION}
root@debian:~#
```

ほぼ`mdcheck_start.service`と同じですが、違いは`ExecStart`に`--continue`が付いただけです。
詳細は上に載せた`/usr/share/mdadm/mdcheck`スクリプトを見て欲しいのですが、**以前に開始されたベリファイがまだ完了していない場合に限り、その続きから再開して最大6時間まで処理を続ける**というコマンドになります。
新しいベリファイを開始しないというのがこの`--continue`オプションになります。

よくコマンド結果を見ていた人は気づくかと思いますが、`systemctl enable mdcheck_start.timer`を実行した時点で`mdcheck_continue.timer`も**有効化されてます**。
よって、**このサービスを使いたい場合は`systemctl start mdcheck_continue.timer`だけでOK**です。[^2]

[^2]: `systemctl list-times`を実行するとわかりますが、次の1:05に`mdcheck_continue.timer`が入っていると思います。

ベリファイの設定はこれで終わりです。


:::note info
実は特に何もしなくてもすでに[checkarrayというスクリプトがDebianにはあります](https://tyru.hatenablog.com/entry/20120505/debian_checkarray)。しかしこちらだとあまり細かく制御ができないので`mdcheck_start.service`の方を使いました。

もしcronを利用している場合は以下のベリファイも同時に動いてしまうため、コメントアウトすることを推奨します。

```/etc/cron.d/mdadm
#
# cron.d/mdadm -- schedules periodic redundancy checks of MD devices
#
# Copyright © martin f. krafft <madduck@madduck.net>
# distributed under the terms of the Artistic Licence 2.0
#

# By default, run at 00:57 on every Sunday, but do nothing unless the day of
# the month is less than or equal to 7. Thus, only run on the first Sunday of
# each month. crontab(5) sucks, unfortunately, in this regard; therefore this
# hack (see #380425).
57 0 * * 0 root if [ -x /usr/share/mdadm/checkarray ] && [ $(date +\%d) -le 7 ]; then /usr/share/mdadm/checkarray --cron --all --idle --quiet; fi
```

:::

## RAIDの監視（異常検知）

次は異常検知です。
もしHDDが故障してRAIDデバイスに異常が起きた際には、メールやメッセージ等ですぐに気づけるように通知が欲しいかと思います。

今回はLINE messaging APIを使って**異常を検知した時にLINEでメッセージが来る**ようにしていきます。
ただし記事が長くなってしまうので、APIが使えるようになるまでのところは[他の記事](https://zenn.dev/kou_pg_0131/articles/line-push-text-message)を参考にしてください。**ここでは「チャンネルアクセストークン」と「自分のユーザID」が分かっており、LINE messaging APIは使える状態にあるもの**とします。

異常検知自体は`mdmonitor.service`が担っており、**異常を検知した際に実行して欲しいシェルスクリプトを`/etc/mdadm/mdadm.conf`に書いておくことで、異常時にそのスクリプトが実行される**ようになります。[^3]

[^3]:`mdmonitor.service`は自動で有効化されているはずなので特に対応は不要ですが、もし無効化されている場合は`systemctl start mdmonitor.service`と`systemctl enable mdmonitor.service`をしておいてください。

`mdmonitor.service`で検知できるイベントは以下の12種類です。

- DeviceDisappeared
- RebuildStarted
- RebuildNN
- RebuildFinished
- Fail
- FailSpare
- SpareActive
- NewArray
- DegradedArray
- MoveSpare
- SparesMissing
- TestMessage

イベント名である程度わかると思いますが、詳細が知りたい方は`man mdadm`で読んでください。

`mdadm.conf`に書いたスクリプトは、発生したイベント名を第一引数(`$1`)に、RAIDデバイス名を第二引数(`$2`)に渡して実行されます。[^4]
その情報を使って条件分岐をしたシェルスクリプトを作りました。

[^4]:`Fail`や`FailSpare`、`SpareActive`といったイベントなどは第三引数(`$3`)も値が入る可能性があるので、スクリプトにはそちらも入れ込んでいます。


```/root/line_notify.sh
#!/bin/bash

# LINE Messaging APIのチャネルアクセストークン
CHANNEL_ACCESS_TOKEN="xxxxx"

# 通知先ユーザーのLINEユーザーID
USER_ID="xxxxx"

# イベント内容に応じたメッセージヘッダーを決定
case "$1" in
    RebuildStarted)
    # md3のみ通知
    if [[ "$2" == "/dev/md/3" ]]; then
	    MESSAGE_HEADER="RAIDアレイ ベリファイ開始 $2"
    else
	    exit 0
    fi
    ;;
    RebuildFinished)
    # md3のみ通知	
    if [[ "$2" == "/dev/md/3" ]]; then
	    MESSAGE_HEADER="RAIDアレイ ベリファイ終了 $2"
    else
	    exit 0
    fi
    ;;
    Rebuild[0-9][0-9])
    # RebuildNNは通知不要
    exit 0
    ;;
  *)
    MESSAGE_HEADER="RAIDイベント検知"
    ;;
esac

# 各情報の取得と部分メッセージの作成
MSG_EVENT="イベント: $1"
MSG_TARGET1="対象1: $2"
MSG_TARGET2="対象2: $3"

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
MSG_TIMESTAMP="日時: $TIMESTAMP"

HOSTNAME=$(hostname)
MSG_HOSTNAME="ホスト名: $HOSTNAME"

IP_ADDRESS=$(ip a | grep -oP '192\.\d+\.\d+\.\d+' | head -n 1)
MSG_IP="IPアドレス: $IP_ADDRESS"

RAID_STATUS=$(cat /proc/mdstat)
MSG_RAID="以下はRAIDアレイの状況
(cat /proc/mdstat)

$RAID_STATUS"

# 最終メッセージの作成
MESSAGE=$(printf "%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n\n%s" "$MESSAGE_HEADER" "$MSG_EVENT" "$MSG_TARGET1" "$MSG_TARGET2" "$MSG_TIMESTAMP" "$MSG_HOSTNAME" "$MSG_IP" "$MSG_RAID")

# JSONペイロードの作成
JSON_PAYLOAD=$(jq -n --arg to "$USER_ID" --arg text "$MESSAGE" '{
  to: $to,
  messages: [
    {
      type: "text",
      text: $text
    }
  ]
}')

# LINE Messaging API を使用してメッセージを送信
curl -X POST https://api.line.me/v2/bot/message/push \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${CHANNEL_ACCESS_TOKEN}" \
  -d "$JSON_PAYLOAD"

```

case文は色々分岐させているので少し解説します。
`RebuildNN`はベリファイがどのくらい終わったかを表すイベントで、 `NN`はデフォルトだと0,20,40,60,80が入ります。
前節で設定したベリファイが走るとベリファイの通知がめちゃめちゃ来るようになるのですが、**正直そんなに細かく通知が来てもうざい**ので、`RebuildNN`は通知しない設定にしています。
また、`RebuildStarted`と`RebuildFinished`は`md3`のみ通知するようにしています。今回使用しているホストでは`md0`から`md3`の4つのRAIDデバイスがあり、**全てのデバイスでベリファイの通知が来るとうざい**からです。
**正直ここら辺は好みなので自分でやる時は好きに変えてください**。

できたスクリプトは実行権限も追加しておきます。

```
root@debian:~# chmod +x /root/line_notify.sh
```

スクリプトができたら`mdadm.conf`に書き込みます。
PROGRAM行を追加します。

```/etc/mdadm/mdadm.conf
# mdadm.conf
#
# !NB! Run update-initramfs -u after updating this file.
# !NB! This will ensure that initramfs has an uptodate copy.
#
# Please refer to mdadm.conf(5) for information about this file.
#

# by default (built-in), scan all partitions (/proc/partitions) and all
# containers for MD superblocks. alternatively, specify devices to scan, using
# wildcards if desired.
#DEVICE partitions containers

# automatically tag new arrays as belonging to the local system
HOMEHOST <system>

# instruct the monitoring daemon where to send mail alerts
#MAILADDR root
PROGRAM /root/line_notify.sh

# definitions of existing MD arrays
ARRAY /dev/md/0  metadata=1.2 UUID=ad95ba6a:967f9065:2272de1f:8ab1a697 name=debian:0
ARRAY /dev/md/1  metadata=1.2 UUID=45940c26:3a8d9416:1b5b12f1:b39b9226 name=debian:1
ARRAY /dev/md/2  metadata=1.2 UUID=4273c02d:00d43a6f:0583f276:62ccd6c7 name=debian:2
ARRAY /dev/md/3  metadata=1.2 UUID=639bffbe:cea7ab5a:d16e03ad:9d280ce8 name=debian:3

# This configuration
```


上記ファイルのコメントにもあるように`mdadm.conf`を修正したら`update-initramfs -u`しておきます。

```
root@debian:~# update-initramfs -u
update-initramfs: Generating /boot/initrd.img-6.1.0-37-amd64
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-37-amd64
Found initrd image: /boot/initrd.img-6.1.0-37-amd64
Found linux image: /boot/vmlinuz-6.1.0-35-amd64
Found initrd image: /boot/initrd.img-6.1.0-35-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done
/dev/sda:
Installing for i386-pc platform.
Installation finished. No error reported.
/dev/sdb:
Installing for i386-pc platform.
Installation finished. No error reported.
/dev/sdc:
Installing for i386-pc platform.
Installation finished. No error reported.
/dev/sdd:
Installing for i386-pc platform.
Installation finished. No error reported.
root@debian:~# 
```

`mdmonitor.service`も再起動しておきます。

```
root@debian:~# systemctl restart mdmonitor.service 
```

それではイベントが起きた時のテストをしてみましょう。

```
root@debian:~# mdadm --monitor /dev/md3 --oneshot --test
```

これで`/dev/md3`にイベント名`TestMessage`が発生したことになります。[^5]
LINEを見てみましょう。

[^5]:NASを再起動すると`DeviceDisappeared`とかのイベントが発生することがあるので、それでテストしても良いかもしれません。また`mdadm --monitor --scan --oneshot`とすれば全RAIDデバイスでTestMessageのイベントの通知が送られます。今回はテストでたくさん来ても嬉しくないので`/dev/md3`に限定しました。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/54cfabc6-4e1f-4b7f-9ddb-049a38dcef29.png">

問題なくLINEに通知が来るようになりましたね。
おうちNASでも異常を検知したらすぐに気づけて便利かと思います。

## RAIDの監視（定期報告）

最後は定期監視です。

異常監視ができれば正直なくても良いものではありますが、ディスクの使用状況(`df -h`)やHDDのSMART情報(`smartctl`)など定期的に見ておいた方が良いものを送るようにします。
また、NAS自体の死活監視も兼ねています。

こちらは**Discordで送る**ようにしてみます。

今回はWebhookを使って通知を送りますが、その詳細は[別の記事](https://zenn.dev/lambta/articles/5edbda4ccb1ec6)を参照ください。
先ほどと同じように**WebhookのURLが分かり、APIが使える状態になっていることが前提**で、シェルスクリプトを書きました。

```/root/discord_notify.sh
#!/bin/bash

# Discord Webhook URL
WEBHOOK_URL="https://discord.com/api/webhooks/xxxxxx/vvvvvvvvvv"

# 各情報の取得と部分メッセージの作成
MESSAGE_HEADER="定期報告"

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
MSG_TIMESTAMP="日時: $TIMESTAMP"

HOSTNAME=$(hostname)
MSG_HOSTNAME="ホスト名: $HOSTNAME"

IP_ADDRESS=$(ip a | grep -oP '192\.168+\.\d+\.\d+' | head -n 1)
MSG_IP="IPアドレス: $IP_ADDRESS"

# mdデバイスのみのディスク使用状況
DISK_USAGE=$(df -h | grep '^/dev/md')
MSG_DISK="ディスク使用状況
(df -h)
\`\`\`$DISK_USAGE\`\`\`"

# SMART情報の取得
MSG_SMART="SMART情報
(smartctl -A /dev/sdx)
\`\`\`"
for DEV in sda sdb sdc sdd; do
  SMART_DATA=$(sudo smartctl -A /dev/$DEV 2>/dev/null | awk -v dev="/dev/$DEV" '
    BEGIN {
      print dev
    }
    /^[ ]*[0-9]+/ {
      id=$1
      name=$2
      raw=$NF
      if (id == 1 || id == 5 || id == 9 || id == 10 || id == 184 || id == 187 || id == 188 || id == 196 || id == 197 || id == 198 || id == 201 ) {
        printf "%-23s(%d) %s\n", name, id, raw
      }
    }
')
  MSG_SMART="${MSG_SMART}${SMART_DATA}"$'\n\n'
done
MSG_SMART="${MSG_SMART}\`\`\`"

# RAIDデバイスの状態取得
RAID_STATUS=$(cat /proc/mdstat | grep -v ^Personalities)
MSG_RAID="RAIDデバイスの状態
(cat /proc/mdstat)
\`\`\`$RAID_STATUS\`\`\`"

# 最終メッセージの作成
MESSAGE=$(printf "%s\n\n%s\n%s\n%s\n\n%s\n%s\n%s" "$MESSAGE_HEADER" "$MSG_TIMESTAMP" "$MSG_HOSTNAME" "$MSG_IP" "$MSG_DISK" "$MSG_RAID" "$MSG_SMART")

# JSONペイロードの作成
PAYLOAD=$(jq -n --arg text "$MESSAGE" '{ content: $text }')

# Discordに送信
curl -H "Content-Type: application/json" \
     -X POST \
     -d "$PAYLOAD" \
     "$WEBHOOK_URL"
```

「SMART情報の取得」の部分の処理が複雑なので少し解説します。
稼働時間(ID＝9)と[WikiのSMARTの英語版](https://en.wikipedia.org/wiki/Self-Monitoring,_Analysis_and_Reporting_Technology)のCriticalになっている情報(ID＝1,5,10,184,187,188,196,197,198,201)を抽出して表示するようにしています。
Discordは一度に送信できる文字数が2000文字で結構ギリギリなので、上限突破しそうな方は調整してください。

これはcronを使って2日に1回18時に送ろうと思います。

```
root@debian:~# crontab -e
```

crontabに書くのは以下です。

```
0 18 */2 * * /bin/bash /root/discord_notity.sh
```

スクリプトをテストしたい場合は`bash /root/discord_notify.sh`でOKです。
結果はこんな感じです。

![スクリーンショット 2025-06-09 23.18.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/fd0240c1-ee7e-45d1-b78b-9da6f8cda4fd.png)

![スクリーンショット 2025-06-09 13.04.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/bb6f7cf4-d4aa-4947-8a9e-a6a13662a41e.png)

これで定期報告も完了です。

異常検知も定期報告も両方LINEで通知したいとかメールで通知したいとか色々な要望があると思いますが、ほぼ一緒のスクリプトなのでそこは自分で調整してください。


:::note info

実は定期監視には`mdmonitor-oneshot.service/mdmonitor-oneshot.timer`を使うという方法もあります。
`mdmonitor.service`がRAIDの状態を常に監視してイベントが発生したらすぐに通知を送るのに対し、`mdmonitor-oneshot.service`はデフォルトだと毎日2時に`mdadm --monitor --oneshot --scan`を実行してイベントを検知したらPROGRAM行のスクリプトを実行して通知を送るというものです。

`mdadm --monitor --oneshot --scan`では特にイベントが起きていないと何も通知がされない仕様のため、今回はcronで定期的にシェルスクリプトを実行する形にしました。
:::


## 最後に

RAIDはバックアップではありません。適切な運用・監視でデータ損失を防ぎましょう。


## 参考情報

以下の情報は記事を書くにあたって参考にしました。ありがとうございました。

[LXDとUbuntuサーバーで⁠⁠、RAID障害時の再構築の予行演習と監視について確認する](https://gihyo.jp/admin/serial/01/ubuntu-recipe/0750)
[ArchWiki RAID](https://wiki.archlinux.jp/index.php/RAID)