2014年7月28日月曜日

systemdあるある

ついったーでつぶやいたsystemdあるあるネタまとめ
  • journaldのログが一部壊れた
  • gdm更新したら手元のセッションがブチ切られた
  • thrashingしてる時にvirtual consoleのgetty起動を待つ気持ち
  • コネクションがはられるまで起動が遅延するので監視スクリプトが異常停止をレポート
  • unitを書き換えてsystemctl daemon-reloadする方法があるとは知らずunitを書き換えるたびに再起動
  • default.targetのことをランレベルって呼んじゃって今使ってるシステムがsysvinitかsystemdか再確認依頼がくる
  • RHEL7で/etc/init.d以下すっきりなくなってるかと思ったらpcpがあって謎。
  • systemctl経由でserviceコマンドみたいな自由過ぎるサブコマンドが使えなくて「hogectl」コマンド作るか悩む
  • 複数セッション貼ってるときにコンソールから一般ユーザでshutdownしようとして確認ダイアログがでてびっくりする
  • systemdで起動すると(途中のシェルが激減してるので)今までよりPIDが小さいので違和感
  • dbus-daemonをkillする実験がしたくなる
  • VMのtemplateをつくるときに/etc/machine-idを削り忘れる

2014年5月25日日曜日

systemd-udevdによるNIC命名とbiosdevnameがまざって混乱する件

最近のsystemd-udevdはlinuxのネットワークインタフェース名を設定します。それにともない特にRHEL界隈でNIC命名規則が複雑になってきたので紹介します。。

問題: linuxカーネルはNICの名前をドライバ初期化順につけるので名前とNICの対応が安定しない。再起動毎に順序がかわる可能性もある。

これに対して過去から最近までいろいろな手法で対策がとられています。

ユーザが明示的に指定する

MAC アドレスなどの識別子とインタフェース名を設定ファイルで明示して指定する。こんなかんじ。
DEVICE=eth1
HWADDR=D4:85:64:01:46:9E
ONBOOT=yes

Predictable Network Interface Name

くわしくはsystemd本家のwikiにみっちり書いてますが、ざっくりまとめるとハードウェアの物理的な位置やバス上のアドレスを根拠にした、安定した名前をつけます。

命名規則: デバイスの種類(Ethernetならen, wirelesslanならwl) + ファームウェアでの場所情報(スロット番号またはPCIバスの番号) [+ VLANタグなど]
例: ens0

Consistent Device Naming by biosdevname

安定した名前づけの仕組みとしては、biosdevnameという別の実装があります。これは最近のFedoraや、RHEL6, 7でDell製ハードウェアの場合に利用されます。
http://linux.dell.com/biosdevname/
SMBIOSというファームウェアの規格で「オンボードの1番」や「拡張カードスロットの2番」のような場所情報を提供し、これを元にNICの名前を作ります。
例: em1

Persistent Net

これらの物理的な位置を利用する命名方法が登場する前から利用されているpersistent-netとよばれる仕組みがあります。
これは一旦linuxカーネルに適当な名前づけをされたあとに(これが物理的な配置とは無関係になっていることには目をつぶり)、次回の起動からは同じNICに同じ名前をつけるようにするため、MACアドレスとインタフェース名の対応づけをudevのルールとして永続化する仕組みです。
この対応づけは/etc/udev/rules.d/70-persistent-net.rules に保存されます。

RHEL7や最近のFedoraでpersistent-netのルール作成はされませんがRHEL6までで作成されたものをアップグレードした場合はひきつづき有効です。

こんがらがるので以下にまとめます。優先順位自体は最近のFedoraのものと同じですが、デフォルトでの有効無効が環境によってバラバラです。

最近のFedora:  ユーザの指定 > persistent-net > biosdevname > systemd > 古典的な名前づけ
RHEL6 on Dell: ユーザの指定 > persistent-net > biosdevname > 古典的な名前づけ
RHEL6 on not Dell: ユーザの指定 > persistent-net > 古典的な名前づけ
RHEL7 on Dell: ユーザの指定 > persistent-net > biosdevname > systemd > 古典的な名前づけ
RHEL7 on not Dell: ユーザの指定 > persistent-net > systemd > 古典的な名前づけ

NIC名前例:

systemd: eno1(ethernetでオンボード1番)
biosdevname: em1 (ethernetでオンボード1番)
古典的な名前づけ: eth0 (最初にeth%d って名前で命名されたNIC)

各自動設定の止め方
systemdのNIC命名: カーネルのコマンドラインに net.ifnames=0
biosdevname: カーネルのコマンドラインに biosdevname=0
persistent-net: /etc/udev/rules.d/70-persistent-net.rulesから該当する行を消す

udevのルールの比較



# 以下RHEL7betaをためした人むけ
RHEL7 public betaを確認するとベンダにかかわらず on Dellのようになっているのですが、RHEL7RCでは上記のように動作が修正されています

2014年5月3日土曜日

なぜ cdromグループに入っていないユーザーがCD, DVDを焼けるか、あるいははじめてのsystemd-logind

このあいだ「今度買うハードウェアはDVDマルチライタがついているんだけど、読み込みは許可しつつ焼けないように制限かけたい」って質問されたら、意外に奥が深かったので再現風にまとめ。

事前の予想


自分のデスクトップにLinuxを使いはじめてから15年くらいです。
昔CD-R焼けなくて対応したときの記憶をたどると、たしかこんなかんじだったんじゃないかなと:
  • /dev/cdrom あたりはrootユーザ、cdromグループになっている
  • 使えるユーザはcdromグループに入っているから使える 
これを踏まえて「デバイスファイルを作ったりcdromグループ設定しているのは今はudevだろうから、そのルールを確認してモードを660じゃなくて640にした上でユーザをcdromグループにすれば読み込みのみにできるかな」と予想しました。

確認と謎


自分のマシン(debian testingでinitはsystemdにしてます)でチェックするとこんなかんじ
dragon:~$ ls -l /dev/cdrom
lrwxrwxrwx 1 root root 3 May  2 03:07 /dev/cdrom -> sr0
dragon:~$ ls -l /dev/sr0
brw-rw----+ 1 root cdrom 11, 0 May  2 03:07 /dev/sr0
dragon:~$ id
uid=1000(kankun) gid=1000(kankun) groups=1000(kankun),24(cdrom),25(floppy),29(audio),30(dip),44(video),
46(plugdev),105(scanner),109(bluetooth),111(netdev)
ああcdromグループに入ってるわ…… ということでちょっと安心しましたが、長期間使っている環境なので油断できません。Fedoraマシンでみると
[kankun@snake ~]$ id uid=1000(kankun) gid=1000(kankun) groups=1000(kankun),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
cdromグループは別にあるもののユーザはcdromグループに入っていません。RHEL5,6,7betaもご同様。
しかしいずれの環境でも、普段からCD読み書きできています。気になります。

「ファイルシステムとしてmountするのはudisksあたりが頑張ってくれてるはずだから、直接デバイス読んでみよう」 less -f /dev/sr0 すると CD/DVDの内容が読めました。
brw-rw----+ 1 root cdrom 11, 0 May  2 03:07 /dev/sr0

という状態のデバイスをrootでもcdromグループでもない一般ユーザがアクセスできるのです。
一瞬頭に「???」が浮かびましたがbrw-rw----+」の最後に+があります。確認するとユーザにアクセスを許可するACLが設定されていました。
dragon:~$ getfacl /dev/sr0
getfacl: Removing leading '/' from absolute path names
# file: dev/sr0
# owner: root
# group: cdrom
user::rw-
user:kankun:rw-
group::rw-
mask::rw-
other::---

誰がこのACLを設定しているの?(タイトルでネタバレ)

# ここからの調査は最初RHEL6でやったんですが、より実装がためになるのでFedora20で調べなおして再現してます。なのでちょいフィクションです。

このACLを設定したプログラムに心あたりがないわけです。自分で明示的に指定したこともありません。自分のマシンで自分の知らない権限設定がされているとかいやですね。しらべましょう。誰がこんな設定してるんでしょうか。ちょっと予想します。
  • 今のログインユーザだから(pam?)
  • インストーラで作成したアカウントだから何か特別に設定が書かれている(どこぞの設定ファイルがudevのルールから参照されてる?)
  • GNOMEデスクトップユーザむけのなにかが動いている(udisksとか??)
とりあえずgnomeうんぬんがでてくると登場人物が多すぎて大変なので、状態をクリアするために再起動してXを起動せずにコンソールからログインします。

やはりACLが設定されています。GNOMEは排除できました。

pamをうたがって別のvirtual terminal(VT)でrootでログインします。ACLの状態がかわりました。これでインストーラも排除できました。
getfacl: Removing leading '/' from absolute path names
# file: dev/sr0
# owner: root
# group: cdrom
user::rw-
group::rw-
mask::rw-
other::---
今ログインしてるユーザ全員に許可をだしているわけではなさそうです。一般ユーザのアカウントへの許可は消えてしまいました。最後にログインしたユーザーかな??

VTきりかえてkankunアカウントでもういっぺん確認します。
getfacl: Removing leading '/' from absolute path names
# file: dev/sr0
# owner: root
# group: cdrom
user::rw-
user:kankun:rw-
group::rw-
mask::rw-
other::---
やばい…… VT切り替えしかしてないのにACLが書き変わってる……。pamだけじゃこの動きはできません。予想が全滅してポルナレフごっことかやりたくなります。やっても話が進まないのでぐっと堪えます。

ls -l /dev して同じように+がついているものをみると kvm, rfkill, video0 でも同様のACLが設定されていました。今デスクトップを目の前でつかっているアカウントがデスクトップ用途で使いやすいようにACLを変更する、という意図でやっていそうです。

こういうのたしかsystemdに含まれているlogindってひとが、ConsoleKitから引きついでました。
コンソール切り替えの情報とか監視してないかなー と、lsof -n |grep ttyとかしてみます。
systemd-l 683 root 5r REG 0,15 4096 4386 /sys/devices/virtual/tty/tty0/active
あった!

systemd-logindによるACL設定


logindが何やってるか知りませんが、こいつかこいつ経由で情報もらった誰かがACLいじっているはず。logindはsystemdのリポジトリにはいってます。
systemdのリポジトリでaclをgrepします。。。./src/login/logind-acl.c ってのがいました。

logind-acl.hで定義されている関数は、 devnode_acl、devnode_acl_all で、デバイスファイルからold_uidとして指定されたuidへの許可ACLを削除して、new_uidとして指定されたuidへの許可ACLを設定します。

devnode_acl_all関数の中で以下のディレクトリを探索してACL設定する対象となるデバイスを決定していました。udevによるタグで絞り込まれています。

       dir = opendir("/run/udev/static_node-tags/uaccess");

このタグづけ対象デバイスは、logindのソースと同じディレクトリにある70-uaccess.rulesの中で定義されていて、たとえばCD/DVDについては以下のようなルールがありました。
# optical drives
SUBSYSTEM=="block", ENV{ID_CDROM}=="1", TAG+="uaccess"
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="4|5", TAG+="uaccess"
これでさっきのディレクトリに登場しそうです。
$ ls /run/udev/tags/uaccess
b11:0  c10:232    c116:1    c116:10  c116:11  c116:12  c116:2  c116:3  c116:33  c116:4  c116:5  c116:6  c116:7  c116:8  c116:9  c189:257  c189:258    c189:259  c189:260  c189:385  c21:2  c226:0  c81:0
してました。b11:0 です。ブロックデバイスのmajor 11, minor 0ですね。

systemd-logindがACL設定をuaccessタグがついているデバイスにおこなっていそうなことはわかりましたが、どういう契機で行われるかという疑問が残っています。もうひといきです。

1. ログイン時のACL設定 

systemd-logindのソースと同じところにpam-module.cといういかにもあやしいのがいます。これをビルドするとpam_systemd.so というモジュールができてセッション作成時に systemd-logindへセッション作成依頼をdbus経由で投げつけます。
systemd-logindがこの依頼を受けてセッション作成し、必要に応じてACL設定の変更をおこなっています。

2. VTの状態によるACL設定

systemd-logind内での関数呼び出し元をおいかけると、以下のようになっていて、VTの状態を反映したACL設定をおこなっていました。VT切り替え監視そのものもsystemd-logindが見ていてmanager_dispatch_console() がハンドラになっています。

manager_dispatch_console()
-> seat_read_active_vt()
  -> seat_active_vt_changed() 
    -> seat_set_active() 
      -> seat_apply_acls() 
        -> devnode_acl_all() 
          -> devnode_acl()

3. udevdによるACL設定

さらにlogindのソースとおなじディレクトリに追いてある73-seat-late.rules.in をみると
TAG=="uaccess", ENV{MAJOR}!="", RUN{builtin}+="uaccess"
という記載があって、uaccessタグがついているデバイスがつながるとudev組み込みのuaccessという処理が走るように設定されています。これは systemdのsrc/udev/udev-builtin-uaccess.cで定義されていて、既にseatとユーザがひもづいている状態でデバイスを接続した時に、そのデバイスについてdevnode_acl()関数を呼びだしてACL設定をおこなうものでした。

まとめると以下のようになっていて、ログインしたとき、画面切り替えした時、既にログイン済みでデバイスをUSBなどで追加したときにちゃんとACLが設定できるようになっていました。

はじめの疑問にもどる

そして結局一般ユーザにCD, DVDを焼けないように設定したいという当初の質問がどうなったかというと……

/lib/udev/rules.d/70-uaccess.rules を/etc/udev/rules.d にコピーしてCD用のエントリを消すと、uaccessタグが付与されなくなるのでまずは一般ユーザからCD, DVD焼けなくなる。 でも読む分にはudisks2がユーザ権限でmountしてくれるのでこれでOK。

この方法の問題点としては、70-uaccess.rules に将来の変更で対象となるデバイスが追加された場合にうまく反映されない点があるので手放しでおすすめはしづらいところ。アップデート時には注意が必要です。

systemd以前の世代だと類似のしくみがhal+ConsoleKitだったりudev+ConsoleKitで別途実装されているのでこの方法とちょっとかわります。「一般的にこのコマンド叩けばOK」という美しい方法はないのでした。なやましい……。

2014年4月7日月曜日

パーティションテーブルのオンライン編集

Linux動作中に、mountなどで利用しているディスクのパーティションテーブルを変更するときにエラーになったりならなかったりするはなし。
 
パーティションテーブルの書き換え自体はいきなりブロックデバイスに書くのでいつでもできます。しかしカーネルが保持しているパーティション情報の更新は失敗するケースがあります。パーティション情報の更新はデバイスにたいするioctlでおこない、対応する手法によって失敗する条件が変わります。

ioctl BLKRRPART でパーティションを読み直しさせると、既に利用しているデバイスの場合はEBUSYで失敗します。

つかうがわ
util-linux-ng-2.17.2/fdisk/fdisk.c

                printf(_("Calling ioctl() to re-read partition table.\n"));
                i = ioctl(fd, BLKRRPART);

つかわれるがわ
linux-2.6.32-71.el6.x86_64/fs/partitions/check.c

int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
{
        struct disk_part_iter piter;
        struct hd_struct *part;
        struct parsed_partitions *state;
        int p, highest, res;

        if (bdev->bd_part_count)
                return -EBUSY;
ioctl BLKPG でパーティションを明示して追加させると、既に利用しているパーティションと競合する場合はEBUSYで失敗します

つかうがわ
util-linux-ng-2.17.2/partx/partx.c
                                pt.pno = lower+j;
                                pt.start = 512 * (long long) slices[j].start;
                                pt.length = 512 * (long long) slices[j].size;
                                pt.devname[0] = 0;
                                pt.volname[0] = 0;
                                a.op = BLKPG_ADD_PARTITION;
                                a.flags = 0;
                                a.datalen = sizeof(pt);
                                a.data = &pt;
                                if (ioctl(fd, BLKPG, &a) == -1) {
                                    perror("BLKPG");

つかわれるがわ
linux-2.6.32-431.el6.x86_64/block/ioctl.c
                        /* overlap? */
                        disk_part_iter_init(&piter, disk,
                                            DISK_PITER_INCL_EMPTY);
                        while ((part = disk_part_iter_next(&piter))) {
                                if (!(start + length <= part->start_sect ||
                                      start >= part->start_sect + part->nr_sects)) {
                                        disk_part_iter_exit(&piter);
                                        mutex_unlock(&bdev->bd_mutex);
                                        return -EBUSY;
                                }
                        }

再起動せずにパーティションを追加してそのパーティションをブロックデバイスとして見せたいシチュエーションでは、BLKPGを使うpartxなどでカーネルにパーティション追加の通知をすると既存のパーティションと重なっていなければ利用可能です。

さらに異常なシチュエーションで、既存のパーティションと重なっているような場合であっても、devicemapperで適当なマッピングをパーティション情報から作ってくれる kpartx を利用すればなんとでもなったりします……。

2014年3月17日月曜日

linux virtual consoleのリサイズ

linuxのvirtual consoleをリサイズする方法を調べたのでメモ

いくつか手法があるのでおすすめ順に。

1. fbset コマンドを利用してリサイズ
    - 実行しながらダイナミックに変更できるフレームバッファの表示サイズを変更する
    - RHEL6から入っていない。Fedoraには存在している
    - 要perl

2. カーネルのコマンドラインでvideo=オプションを指定する
    - video=800x600 のようにモードを指定する
    - カーネルの中に含まれているモードのDBを利用するので好き勝手なサイズにはできない
    - video=オプションのサポートはデバイスドライバ毎におこなわれているので互換性の問題がある。ただし現在主流のものはOK

3. drm_kms_helperモジュールのedid_firmwareオプションで(通常はディスプレイが提供する)EDID情報を提供する
    - drm_kms_helper.edid_firmware=edid/1024x768.bin
    - モジュールのオプションなのでカーネルのコマンドラインだけでなくmodprobe.confでも指定できる

4. カーネルのコマンドラインでnomodesetオプションを指定する
    - 80x24のコンソールだけでよければこれでok.
    - vga= オプションでサイズ指定もある程度可能
    - 基本的にデバッグ用のオプションなので推奨されません(RHELだとサポート対象外)

5. virtual consoleをつかわない
    - 仮想化環境であれば、シリアルコンソールを設定してvirtual consoleを使わないオプションはそれなりにリーズナブル

変更:
1. nomodesetはデバッグ用でサポートされない話を追記
2. drm_kms_helperモジュールを教えてもらったので追記