LinuxでHDDなデバイスファイルにudevで別名を振る

背景

/dev/sd[b-i] みたいに指定して8台のデバイスを使ってRAIDを組んだり、初期化したり、ZFSを組んだり、ループ回してdd流したりして遊んでいた平和な日々を送っていた。

ある日起動して同じように作業していたら、ファイルが全く参照できない状態になった。システムディスクが/dev/sdiになっていてそこのメタデータをぶち壊してしまったようだった。

私はデバイスファイルは一度認識したらハードウェア構成をいじらない限り固定だと思い込んでたのだ。というか実際6か月以上いじっててハードウェア変更なしに変わったのは初めてだった。その後OSを入れなおして作業するも、再起動するたびにシステムディスクのデバイスファイルがしばしば変わるようになってしまい、/dev/sd[b-i]/dev/sd[a-h] という指定ができなくなって不便した。RAID(HBA)カードを導入し始めてから起きている気がするので、その辺の兼ね合いもある?

これが起きないようにするためにはHDDに相当するデバイスファイルは名前を固定できると便利では?と考えた。

調査

固定ついでに /dev/sbbは物理的なHDDスロット1個目に対応している、みたいなデバイス名にしてわかりやすくできないか?と思って調べたが、たぶん無理そうだった(NAME=hdd1みたいなことはできない?)

その代わりエイリアス(SYMLINK)は作れるようだった。

wiki.archlinux.jp

そこで hdd1,hdd2...という感じで連番を物理的なHDDスロットに対応させたデバイスファイルのエイリアスを作ればいいのでは、と思い至った。(そういえば昔はhda,hdbとか気がする)

連番の振り方を考える必要がある。何を基準に連番を振るべきか調べていると、 /dev/disk/by-id/xxxxにあるxxxxはデバイス毎に不変な値(?)らしいのと、シリアル番号をもったIDになっているので、デバイスファイルを特定できそうだと思った。(ata-xxxxのxxxxはシリアルに相当。パーティションがある場合は-part1サフィックスがつくので区別できる)

シリアル番号とスロットの対応はついてるので、hdd1→シリアル1→スロット1という対応もできそう。メンテナンスが楽になりそうでは?ということでやってみた。

作業

※当たり前ですが自己責任で。エイリアスを作るだけなので使わなければ破壊することはないはずですが使うときは注意しましょう。

こんな感じのpythonスクリプト hdd-name.py を書く。

#!/bin/python

# python3 hdd-name.py

import glob
import os
import sys

BY_ID_GLOB="/dev/disk/by-id/ata-*"

# {} には シリアルに対応した id 値が入る
DEV_NAME_FORMAT="hdd{}"

# TODO: 要設定
# シリアル番号の末尾3桁を決め打ちして特定し順序付ける(全桁だと長いので対応付けが大変だった)
# WD製のWD60EZAXのみで確認中
serial_suffix_to_id = {
  "xx2": 1,
  "xx0": 2,
  "xxD": 3,
  "xxK": 4,
  "xxA": 5,
  "xxL": 6,
  "xx8": 7,
  "xxT": 8,
}
suffix_length = 3

dev_list = []

for file in glob.glob(BY_ID_GLOB):
  name = os.path.basename(file)
  name_suffix = name[-suffix_length:]
  id = serial_suffix_to_id.get(name_suffix)

  if not id:
      continue

  serial = name[4:]
  serial_suffix_to_id.pop(name_suffix)
  dev_list.append([id, serial])

# 参照できなかったシリアルがある
for (suffix, id) in serial_suffix_to_id.items():
  print(f"WARN: Does not found suffix: '{suffix}' (id={id})", file=sys.stderr)

for (id, serial) in sorted(dev_list, key=lambda i: i[0]):
  #print(f"{serial} => {id}")
  print(f'SUBSYSTEM=="block", ENV{{DEVTYPE}}=="disk", ENV{{ID_SERIAL}}=="{serial}", SYMLINK+="{DEV_NAME_FORMAT.format(id)}"')

シリアルとIDの対応表のところは各自で修正。

やっている内容は/dev/disk/by-id/ata-*を見てシリアル番号に対応するIDを使ってエイリアスを作るudev用のルール形式に標準出力するだけ。

実行するとこんな感じにudevのルール定義が出力される。

SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="WDC_WD60EZAX-00C8VB0_WD-WXGXXXXXXXxx2", SYMLINK+="hdd1"
SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="WDC_WD60EZAX-00C8VB0_WD-WXGXXXXXXxx0", SYMLINK+="hdd2"
...
  • SUBSYSTEMは多分なくてもよさそうだけど念のため。ブロックデバイスの意味だと思う()。
  • ENV{DEVTYPE}=="disk", がないとパーティション(ENV{DEVTYPE}=="partition")も一致してしまうし、シリアル値も一致してしまうので、SYMLINKの先がパーティションなブロックデバイスを指し示してしまう。(ZFSなどの作業していると勝手にパーティションを作られるから配慮する必要があった)
  • ENV{ID_SERIAL}/dev/disk/by-id/xxxxxxxx部分で比較するようにし、ディスクデバイスを特定
  • 条件に一致したものをSYMLINKを生成する。

これで特定のシリアルを持つ物理ディスクのデバイスファイルを/dev/hdd1などの別名をつけることができる。はず。

なお、udevに永続的に認識させるには /etc/udev/rules.dに配置する必要があるので、出力した内容をファイルとして配置する。

python3 hdd-name.py | sudo tee /etc/udev/rules.d/99-persistent-storage.rules

反映させるには次の2つのコマンドを実行。

sudo udevadm control --reload-rules &&
sudo udevadm trigger

確認。実行されていればエイリアスができているはずである。できていなければ、ルールの条件を満たせず適用できなかったので設定値を見直す。

ls -l /dev/hdd*
# 出力例
$ ls -l /dev/hdd*
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd1 -> sdd
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd2 -> sdc
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd3 -> sdb
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd4 -> sdi
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd5 -> sde
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd6 -> sdf
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd7 -> sdg
lrwxrwxrwx. 1 root root 3 Jul 14 17:51 /dev/hdd8 -> sda

バイスファイル名は順序通りにはなっていないが、シリアル番号を確認するとちゃんと対応づいているはずである。

その後、再起動しても反映されるかどうかを確認する。

$ for file in /dev/hdd*; do echo "$file => $(readlink $file): $(udevadm info --query=all --name=$file | grep ID_SERIAL=)"; done;
/dev/hdd1 => sdg: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx2
/dev/hdd2 => sdh: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx0
/dev/hdd3 => sdf: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxD
/dev/hdd4 => sde: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxK
/dev/hdd5 => sdc: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxA
/dev/hdd6 => sdd: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxL
/dev/hdd7 => sda: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx8
/dev/hdd8 => sdb: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxT

# 再起動後
$ for file in /dev/hdd*; do echo "$file => $(readlink $file): $(udevadm info --query=all --name=$file | grep ID_SERIAL=)"; done;
/dev/hdd1 => sdd: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx2
/dev/hdd2 => sdi: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx0
/dev/hdd3 => sde: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxD
/dev/hdd4 => sda: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxK
/dev/hdd5 => sdb: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxA
/dev/hdd6 => sdh: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxL
/dev/hdd7 => sdc: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xx8
/dev/hdd8 => sdf: E: ID_SERIAL=WDC_WD60EZAX-00C8VB0_WD-****xxT

バイスノード名(sdg等)が変わっているが、hdd1とシリアルの対応付けは変わってないことがわかる。

おまけ

Udevのルールに使える条件がよくわからなかった

udevのマニュアルからでは多分ほとんど読み取れないと思う。

頑張ってudevのマニュアルをあさるより、次の記事を見るとなんとなくわかると思う。

www.reactivated.net

udevadm infoを使えば値を調べることができるというのはわかった。

例えば、udevadm info /dev/sdb とすると以下のように出る。

udevadm info /dev/sdb

----
P: /devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:6/end_device-6:6/target6:0:6/6:0:6:0/block/sdb
M: sdb
U: block
T: disk
D: b 8:16
N: sdb
L: 0
S: disk/by-id/scsi-*********************
S: disk/by-path/pci-0000:01:00.0-sas-phy6-lun-0
S: disk/by-id/wwn-*********************
S: disk/by-id/ata-WDC_WD60EZAX-00C8VB0_WD-*********************
S: disk/by-diskseq/2
S: disk/by-id/scsi-SATA_WDC_WD60EZAX-00C_WD-*********************
S: hdd1
Q: 2
E: DEVPATH=/devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:6/end_device-6:6/target6:0:6/6:0:6:0/block/sdb
E: DEVNAME=/dev/sdb
E: DEVTYPE=disk
E: DISKSEQ=2
...
----

パーティションで区切られた/dev/sdb1を見ると以下のようになる


----
P: /devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:6/end_device-6:6/target6:0:6/6:0:6:0/block/sdb/sdb1
M: sdb1
R: 1
U: block
T: partition
D: b 8:17
N: sdb1
L: 0
S: disk/by-partlabel/zfs-*********************
S: disk/by-id/wwn-*********************-part1
S: disk/by-id/scsi-*********************-part1
S: disk/by-path/pci-0000:01:00.0-sas-phy6-lun-0-part1
S: disk/by-label/pool0
S: disk/by-id/ata-WDC_WD60EZAX-00C8VB0_WD-*********************-part1
S: disk/by-uuid/*********************
S: disk/by-id/scsi-SATA_WDC_WD60EZAX-00C_WD-*********************-part1
S: disk/by-partuuid/8077569f-*********************
Q: 2
E: DEVPATH=/devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:6/end_device-6:6/target6:0:6/6:0:6:0/block/sdb/sdb1
E: DEVNAME=/dev/sdb1
E: DEVTYPE=partition
E: DISKSEQ=2
----

DEVTYPEを見ればディスクかパーティションか区別できそう、というのがわかった。しかし 条件に DEVTYPE=="disk" としてもダメだったので悩んだ。GPT-4oもダメな方法を教えてくれた。こういうニッチなことを教えてもらうにはGPTは全く向いてない。

最初、各行のプレフィクスの意味がわからなかったが、udev(7) に一覧があった。抜粋。

Prefix Meaning
"P:" Device path in /sys/
"M:" Device name in /sys/ (i.e. the last component of "P:")
"R:" Device number in /sys/ (i.e. the numeric suffix of the last component of "P:")
"U:" Kernel subsystem
"T:" Kernel device type within subsystem
"D:" Kernel device node major/minor
"I:" Network interface index
"N:" Kernel device node name
"L:" Device node symlink priority
"S:" Device node symlink
"Q:" Block device sequence number (DISKSEQ)
"V:" Attached driver
"E:" Device property

が、結局その値をどうルールにかけるのか、というのが全く分からず苦労した。

udevadm(8)に一応Keysの項目で列挙されており、多分ルールに使えるのはこのあたりのキーだろう。

  • ACTION
  • DEVPATH
  • KERNEL
  • KERNELS
  • NAME
  • SYMLINK
  • SUBSYSTEM
  • SUBSYSTEMS
  • DRIVER
  • DRIVERS
  • ATTR{filename}
  • ATTRS{filename}
  • SYSCTL{kernel parameter}
  • ENV{key}
  • CONST{key}
  • TAG
  • TAGS
  • TEST{octal mode mask}
  • PROGRAM
  • RESULT
  • OWNER
  • GROUP
  • MODE
  • SECLABEL{module}
  • RUN{type}
  • LABEL
  • GOTO
  • IMPORT{type}
  • OPTIONS

でもこれらのキーにどういう値を入れて比較できるのかというのがよくわからなかった(LABEL,RUNとかをみてわかるように、そもそも比較だけじゃないのだけど)

さすがに資料がないことはないと思うんだけど。調べ方が悪いのでしょう。馬鹿だからトライアンドチェックで確認していた。

"E:"プレフィクスの値を参照するには ENV を使う

例えば、上記の例で E: DEVTYPE=disk とあるが、これをudevのルールで一致するか評価したい場合は以下のようにする。

ENV{DEVTYPE}=="disk"

わかれば簡単。でもわからないとわからなくないか…。これどこかに書いてある?

sysfsの値を参照するには ATTR を使う

ATTR{PATH}=="xxx"はsysfsで見れる値をudevのルールで参照できるようだ。

まず、sysfsの値をどうやって参照するか。udevadm info --attribute-walk が簡単のようだった。

udevadm info --attribute-walk /dev/sda

結果は以下になる。

  looking at device '/devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:3/end_device-6:3/target6:0:3/6:0:3:0/block/sda':
    KERNEL=="sda"
    SUBSYSTEM=="block"
    DRIVER==""
    ATTR{alignment_offset}=="0"
    ATTR{capability}=="0"
    ATTR{discard_alignment}=="0"
    ATTR{diskseq}=="1"
    ATTR{events}==""
...

sysfsが/sysにマウントされているならば、ファイルパスと属性を繋げてcatなどで参照できる。

# cat "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/host6/port-6:3/end_device-6:3/target6:0:3/6:0:3:0/block/sda/diskseq"
1

これをudevのルールで一致するか評価したい場合は以下のようにすればよい。

ATTR{diskseq}=="1"

sysfsの値を使う予定はなかったので使わないけど。

ディスクを差し替えた時のオペレーションを考えるとこのやり方は筋が悪そう

今更ー

その時が来たら考えます。