まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:virsh

KVM(Kernel-based Virtual Machine) は CentOS/RHEL(RedHat Enterprise Linux) 環境に標準装備の仮想ハイパーバイザーです。わかりやすく言えば、VMWare Server のような機能がカーネルにはじめから用意されている(はじめから有効にはなっていない)ので、CentOS/RHEL でも、それ以外の Linux ディストリビューションでも単体で仮想環境を構築することができます。 僕も自宅の個人環境で使っています。KVM 環境の構築手順はこちらのエントリを参照ください:
CentOS に KVM 環境を構築する


KVM には GUI/CUI 両方の管理ツール/コマンドが用意されていて、仮想イメージのクローン(複製)なども行うことができます。ただこの複製ツールが手軽に使えるかというと、実用上ちょっとした問題がありました。
2015021801


その問題点は、この「複製」というのは「仮想イメージをまるごと複製」するのですが、ホスト名と MAC アドレスまでまるごと複製してしまう、という点でした。つまりあるサーバーインスタンスのイメージを複製すると、同じホスト名で、同じ MAC アドレスのネットワークインターフェースを持った異なるサーバーが用意される、ということです。 またネットワーク設定も複製されるため、例えば固定 IP アドレスが設定されている環境であれば同じ IP アドレスが割り振られることになりますし、DHCP 設定だとすると、同じ MAC アドレスのサーバーが別に存在していることになり、DHCP サーバーからは区別ができません(結果、正しい IP アドレスが取得できず、ネットワークが使えない状態で起動してしまいます)。


要は、複製コマンドそのものに問題があるわけではない(むしろ、複製という意味では正しい)のですが、イメージを複製した後にホスト名と MAC アドレスを変更したい(そこまで含めて自動化したい)のです。ただホスト名は AAA から BBB に変更すればよい、という正解が決まっているのでいいとして、MAC アドレスは変更したいといっても「では変更後の値を何にするのか?、一意に生成できるのか?」という問題もあったりして単純ではありません。またこれらの処理を自動化するとなると、起動していない仮想サーバーイメージ内のファイルシステムを外から操作することになるので、これも特殊なコマンドを使うことになります。その辺りを含めてシェルスクリプト化するまでを紹介します。


というわけで、目標として今回は clone-guest.sh というシェルスクリプトを作って、
# clone-guest.sh AAA BBB
というコマンドで、AAA という仮想マシンイメージを BBB という仮想マシンに複製できるようにします。複製そのものは /usr/bin/virt-clone コマンドを使えばできるのですが、その際に BBB は AAA とは異なる MAC アドレスを持っているように変更します。また複製後にこの BBB を自動起動するようにしてみます。また今回作成するシェルスクリプトでは複製の対象を CentOS/RHEL とします(これらのシステムを前提として MAC アドレス変更の処理を追加するためです)。


まず最初に「そもそもホスト名と MAC アドレスの変更ってどうやるの?」という疑問が生じます。ホスト名については CentOS/RHEL では /etc/sysconfig/network 内に HOSTNAME=**** として記載されているので、ここを書き換えるだけで済みます。具体的には例えば複製元の /etc/sysconfig/network に HOSTNAME=AAA.domain.com のような記載が含まれていたら、複製先では HOSTNAME=BBB.domain.com と書き換えればいい、ということになります。

一方、MAC アドレスはちと複雑です。まず MAC アドレスを書き換える必要のあるファイルが、
 - /etc/sysconfig/network-scripts/ifcfg-eth0
 - /etc/udev/rules.d/70-persistent-net.rules
の2つあります。更に書き換え後の MAC アドレスは現在とは異なる一意の値にする必要があるため、どのように生成するか、という問題も含まれているのでした。ここは詳しくは後述しますが /usr/bin/uuidgen コマンドを使って動的に取得するようにします。
 

次に、起動していない仮想イメージファイル内のファイルシステムへどのようにアクセスするか? ですが、こちらは KVM 標準の以下のコマンドを活用して実現します:
- /usr/bin/virt-cat     外部から仮想イメージ内の特定ファイルに対して cat を実行する
- /usr/bin/virt-copy-in 外部から仮想イメージ内の特定ファイルにコピーする

これらのツールと、KVM の一般的な仮想管理コマンドである virsh(/usr/bin/virsh) を組み合わせて、clone-guest.sh を以下のような内容で作りました。青字は僕のコメントです:
#!/bin/sh

# 作業ディレクトリ
work_dir=/tmp/clone-guest

# この作業で使う各種コマンド
cmd_awk=/bin/awk
cmd_cat=/bin/cat
cmd_cut=/bin/cut
cmd_grep=/bin/grep
cmd_mkdir=/bin/mkdir
cmd_sed=/bin/sed
cmd_uuidgen=/usr/bin/uuidgen
cmd_arp=/sbin/arp
cmd_virsh=/usr/bin/virsh
cmd_virt_cat=/usr/bin/virt-cat
cmd_virt_clone=/usr/bin/virt-clone
cmd_virt_copy_in=/usr/bin/virt-copy-in

source_bridge=br0 # KVM のブリッジインターフェース名(デフォルトだと virbr0)
original_domain_name=$1 # 複製元(AAA)
clone_domain_name=$2 # 複製先(BBB)

# 仮想イメージのあるディレクトリ
domain_image_dir=/var/lib/libvirt/images
clone_domain_image_path=$domain_image_dir/$clone_domain_name.img # 複製先仮想イメージファイル


# パラメータを2つ(複製元と複製先)指定して実行していることを確認
if [ "$clone_domain_name" = "" ] ; then
echo "Usage: clone-guest.sh <domain_name> <clone_name>"
exit 0
fi

# 作業ディレクトリが存在していなかったら作成
[ ! -e $work_dir ] && $cmd_mkdir -p $work_dir


# virt-clone コマンドで普通に複製
$cmd_virt_clone \
  --original $original_domain_name \
  --name $clone_domain_name \
  --file $clone_domain_image_path 

# 複製前の仮想イメージ内の /etc/sysconfig/network を取り出して作業ディレクトリに保存
$cmd_virt_cat -d $original_domain_name \
  /etc/sysconfig/network \
  > $work_dir/network.org

# 複製前の仮想イメージ内の /etc/sysconfig/network-scripts/ifcfg-eth0 を取り出して作業ディレクトリに保存
$cmd_virt_cat -d $original_domain_name \
  /etc/sysconfig/network-scripts/ifcfg-eth0 \
  > $work_dir/ifcfg-eth0.org

# 複製前後の仮想イメージの MAC アドレスを取り出す
original_mac_addr=$( \
$cmd_virsh domiflist $original_domain_name \
| $cmd_grep $source_bridge \
| $cmd_awk '{print $5}' \
)
clone_mac_addr=$( \
$cmd_virsh domiflist $clone_domain_name \
| $cmd_grep $source_bridge \
| $cmd_awk '{print $5}' \
)

# 複製前後の仮想イメージの UUID を取り出す
original_nic_uuid=$( \
$cmd_cat $work_dir/ifcfg-eth0.org \
| $cmd_grep -i uuid \
| $cmd_cut -d'=' -f2 \
| $cmd_sed 's/"//g' \
)
clone_nic_uuid=$($cmd_uuidgen)

# 複製後の仮想イメージの ifcfg-eth0 を書き換え
$cmd_cat $work_dir/ifcfg-eth0.org \
| $cmd_sed -e "s/$original_mac_addr/$clone_mac_addr/i" \
-e "s/$original_nic_uuid/$clone_nic_uuid/i" \
> $work_dir/ifcfg-eth0

# 複製後の仮想イメージに書き換え後の ifcfg-eth0 を書き戻す
$cmd_virt_copy_in -d $clone_domain_name \
  $work_dir/ifcfg-eth0 \
  /etc/sysconfig/network-scripts/

# 複製前の仮想イメージ内の /etc/udev/rules.d/70-persistent-net.rules を取り出して作業ディレクトリに保存
$cmd_virt_cat -d $original_domain_name \
  /etc/udev/rules.d/70-persistent-net.rules \
  > $work_dir/70-persistent-net.rules.org

# 複製後の仮想イメージの 70-persistent-net.rules を書き換え
$cmd_cat $work_dir/70-persistent-net.rules.org \
  | $cmd_sed "s/$original_mac_addr/$clone_mac_addr/i" \
  > $work_dir/70-persistent-net.rules

# 複製後の仮想イメージに書き換え後の 70-persistent-net.rules を書き戻す
$cmd_virt_copy_in -d $clone_domain_name \
  $work_dir/70-persistent-net.rules \
  /etc/udev/rules.d/

# 複製後の仮想イメージの network を書き換え
$cmd_cat $work_dir/network.org \
| $cmd_sed -e "s/$original_domain_name/$clone_domain_name/i" \
> $work_dir/network

# 複製後の仮想イメージに書き換え後の network を書き戻す
$cmd_virt_copy_in -d $clone_domain_name \
  $work_dir/network \
  /etc/sysconfig/

# 複製後の仮想イメージを起動
$cmd_virsh start $clone_domain_name

echo "Clone $clone_domain_name has been started."
exit 0

sed だの awk だのを駆使してなんとか作った、という感じです(苦笑)。あとはこのファイルを clone-guest.sh という名前で保存し、実行権限を与え、パスの通ったディレクトリに置いておけば、
# clone-guest.sh AAA BBB

という感じで、既存の AAA という仮想環境を BBB という名前で複製して、各種設定ファイルまで書き換えて、起動する、という使い方ができるはずです。 ここまでできるようになると、KVM もかなり便利に使えてます。










 

KVM をはじめとするハイパーバイザーの魅力は、OS環境が必要になった場合、テンプレート的な環境を元にすぐ用意できる、ということ。いわゆる LAMP 環境が必要になったら、LAMP 導入済みの仮想環境があれば、それを複製して新しいLAMPの仮想マシンを作れる、というイメージです。

が、KVM でこれをやろうとすると結構ひっかかるポイントがあります。具体的な環境にも依存するのですが、自分の場合は KVM のゲストOSにはDHCPを適用していました。これが後々大きな問題になりうるとは・・・ 試行錯誤を繰り返してなんとか形になったので、ここにまとめておきます。


そもそもどこでつまづくのか?

おさらいですが、仮想マシンを複製して新しい仮想マシンを作って利用する、ということは以下のような流れになります:
(1) 複製元の仮想ゲスト環境から、新しい仮想ゲスト環境を複製する
(2) 複製後の仮想環境の IP  アドレスを確認する
(3) 複製後の仮想環境にリモートアクセスして利用する

※ (1) のコマンドは、例えば複製元の KVM ドメイン名が guest1、複製先が guest2、複製先のイメージファイルを /var/lib/libvirt/images/guest2.img に指定するとしたら以下のようになります:
# virt-clone --original guest1 --name guest2 --file /var/lib/libvirt/images/guest2.img

この流れの中でつまづく可能性があるのは2点です:

まず自分の環境では DHCP でゲスト環境の IP アドレスを動的に割り振っています。なので (1) で複製したゲスト環境の IP アドレスを事前に想定することができないので、起動してもどうやって (3) のリモートアクセスしていいのか分からないのです。

そのためには (2) の手順が必要になります。具体的には KVM の virsh コマンドを使って親ホストから子ゲストにコンソールログインして IP  アドレスを確認することになります。この手順も含めて紹介しますが、裏を返せば (2) が可能になるように (1) の複製元仮想ゲスト環境を設定しておくことが必要になる、ということです。

※ KVM ホストが RUNLEVEL = 5 の GUI 環境で運用されている場合は、virt-manager の画面から直接目的の仮想環境のコンソールを開いてログインすることも可能ですが、RUNLEVEL = 3 の CUI 環境で運用しているケースを想定し、また最終的にはシェルスクリプト1つで複製からネットワーク設定の変更まで一発で変更できるような処理にしたいので、コンソールログインができるような環境を整えておくことをおすすめします。

もう一点の問題は (1) を普通にコマンドを実行して行っただけでは複製後のマシンのネットワークインターフェースが有効にならない(ループバックインターフェースのみが有効な状態で起動する)、ということです。複製元の環境がDHCPで、MACアドレスを持ってDHCPサーバーに問い合わせた結果のIPアドレスを割り振られています。この辺りは OS インストール前後の設定項目なのであまり意識して設定していない人もいると思いますが、具体的な内容は /etc/sysconfig/network-script/ifcfg-eth0 などに記載されています。

そして KVM の複製コマンドではこれらの情報ごと複製されることになります。つまり複製元の MAC アドレスが書かれた設定ファイルが、異なる MAC アドレスを持つ環境下にコピーされてしまうことになり、このファイルではDHCP サーバーから IP アドレスを正しく 取得することができないのでした。加えて複製元環境にホスト名が設定されていた場合は /etc/hosts や /etc/sysconfig/network なども複製元の情報を保持したままコピーされることになるので、これらのファイルもそのままでは正しくない、ということになります。これらを解決するには複製直後のネットワーク環境に対していくつかの変更が必要になるのでした。


KVM のゲスト Linux 環境にコンソールログインするための設定

こちらは以前のブログエントリで触れておきました。複製元のゲストOS環境にこちらで紹介した設定をして、コンソールログインが可能な状態にしておいてください。


KVM の Linux 環境のネットワーク関連設定ファイル

まずは複製後に変更の必要がある(可能性のある)ファイル4つと、その変更内容を羅列します:

・ /etc/hosts

複製前の環境にホスト名(例えば xxx.host.com)を設定していると、/etc/hosts にこのような記述がされているはずです。
 127.0.0.1  xxx.host.com localhost localhost.localdomain ...

で、この内容が複製先にもそのまま反映されることになります。それで問題ないケースと問題になるケースがあると思うのですが、問題になるケースでは複製先での変更が必要です。

・ /etc/sysconfig/network

上記の /etc/hosts と同じ理由です。/etc/sysconfig/network にはホスト名が記述されています。
 NETWORKING=yes
 HOSTNAME=xxx.host.com

この内容がそのまま複製先に反映されて影響があるケースでは複製先での変更が必要になります。

・ /etc/sysconfig/network-scripts/ifcfg-eth0

この作業の肝になる部分です。/etc/sysconfig/network-scripts/ifcfg-eth0 にはネットワークインターフェースの設定が記述されており、このファイルに関しては複製元の情報をそのまま使うことはできません

具体的には以下のような変更が必要です:
- 固定 IP アドレスで運用する場合は複製先には異なる IP アドレスを指定する
- DHCP で運用する場合は複製元の MAC アドレス情報を複製先のものに書き換える
- 更に UUID については複製元の情報を複製先で使うわけにはいかないため、新たに用意する

(DHCP の例)
DEVICE="eth0"
BOOTPROTO="dhcp"
HWADDR="XX:XX:XX:XX:XX:XX"
NM_CONTROLLED="no"
ONBOOT="yes"
TYPE="Ethernet"
UUID="nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn"
  : 

・ /etc/udev/rules.d/70-persistent-net.rules

このファイルにも MAC アドレスが記述されているので、複製先では複製先の MAC アドレスに必ず書き換える必要があります

SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="XX:XX:XX:XX:XX:XX", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"


KVM のゲスト Linux 環境のネットワーク関連設定ファイルの変更

なお、以下の説明では guest1 というドメイン名の KVM ゲストから guest2 というドメイン名の KVM ゲストを複製したものとして説明を続けます。guest2 には guest1 のネットワーク設定情報が残っているので変更する必要がある、という状況を想定しています。


まずは KVM のホストからゲスト環境のファイルシステムにアクセスするために、KVM ホストに libguestfs パッケージを導入します:

# yum install libguestfs libguestfs-tools libguestfs-tools-c

libguestfs パッケージが KVM ホストに導入されると KVM ホストから KVM ゲストのファイルシステムに直接アクセスすることが可能になります。なお、その際は目的の KVM ゲストはシャットダウン状態にしておきます。


例えば KVM ゲスト : guest2 の特定のファイル(例 /etc/hosts)を表示するには、KVM ホストから以下のコマンドを実行します。 -d でゲスト環境のドメインを指定します:

# virt-cat -d guest2 /etc/hosts

127.0.0.1  xxx.host.com localhost localhost.localdomain ...

ファイルを編集するには virt-edit コマンドを実行します。パラメータは同じです:

# virt-edit -d guest2 /etc/hosts

/etc/hosts に変更の必要がある場合は適宜変更して保存します。再度 virt-cat コマンドで変更後の内容を確認します。

# virt-cat -d guest2 /etc/hosts

127.0.0.1  yyy.host.com localhost localhost.localdomain ...
(ホスト名を xxx.host.com から yyy.host.com に変更した場合)


ホスト名の変更が必要な場合は 同様にして /etc/sysconfig/network ファイルも編集します:

# virt-edit -d guest2 /etc/sysconfig/network

# virt-cat -d guest2 /etc/sysconfig/network

NETWORKING=yes
HOSTNAME=yyy.host.com
(ホスト名を xxx.host.com から yyy.host.com に変更した場合)


次に複製先の /etc/sysconfig/network-scripts/ifcfg-eth0 ファイルを編集しますが、その前に複製先環境の MAC アドレスを確認します:

# virsh domiflist guest2

Interface Type Source Model MAC
-------------------------------------------------------
- bridge br0 virtio 'YY:YY:YY:YY:YY:YY'
(YY:YY:YY:YY:YY:YY 部分が guest2 の MAC アドレスです)

また複製先用の UUID も uuidgen コマンドで事前に用意しておきます:

# uuidgen

mmmmmmmm-mmmm-mmmm-mmmm-mmmmmmmmmmmm
(8桁-4桁-4桁-4桁-12桁の文字列が guest2 用の新しい UUID です)

guest2 の MAC アドレスと UUID が決まったので、この内容を guest2 の /etc/sysconfig/network-scripts/ifcfg-eth0 を編集して反映させます:

# virt-edit -d guest2 /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE="eth0"
BOOTPROTO="dhcp"
HWADDR="YY:YY:YY:YY:YY:YY"
NM_CONTROLLED="no"
ONBOOT="yes"
TYPE="Ethernet"
UUID="mmmmmmmm-mmmm-mmmm-mmmm-mmmmmmmmmmmm"
  : 


最後に複製先の /etc/udev/rules.d/70-persistent-net.rules ファイルを編集して、複製先環境の MAC アドレスを指定して保存します:
 
# virt-edit -d guest2 /etc/udev/rules.d/70-persistent-net.rules
 
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="YY:YY:YY:YY:YY:YY", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"


これで複製先で変更が必要なネットワーク設定は以上です。これらの変更後に guest2 を起動するとネットワークが有効な状態で立ち上がります:

# virsh start guest2

# virsh list
(guest2 が「実行中」として表示されるようになるまで待つ)


上記「KVM のゲスト Linux 環境にコンソールログインするための設定」が複製元(guest1)で済んでいれば、複製先である guest2 にもその内容が複製されているので guest2 にコンソールログインできるはずです。KVM のコンソールログインについてはこちらのエントリも参考にしてください:

# virsh console guest2


コンソールログインした先で ifconfig コマンドを実行して、guest2 に DHCP で割り当てられた IP アドレスを確認します。eth0 にアドレスが付与されていれば成功!

# ifconfig

eth0   Link encap:Ethernet  HWaddr YY:YY:YY:YY:YY:YY
           inet addr:ZZZ.ZZZ.ZZZ.ZZZ  Bcast:ZZZ.ZZZ.ZZZ.255  Mask:255.255.255.0
             :
(ZZZ.ZZZ.ZZZ.ZZZ 部分が guest2 のIPアドレスです)


なお、コンソールログインから抜けるには CTRL+] を押します:


これで KVM の複製コマンドで複製した仮想マシンでもネットワークが有効になりました。IP アドレスまで確認できているので、後は KVM ホスト経由でなくても普通に ssh などでリモートアクセスすることもできるようになりました。


とりあえず一連の作業としてはこんな感じですが、色々面倒なので自分の環境用に一部をシェルスクリプト化しました。具体的には以下の条件下で使えるつもりです:
・KVM ホストのシェルから実行する前提
・KVM ゲストの複製元も複製先も DHCP 設定
・KVM ゲストイメージはデフォルト設定のまま /var/lib/libvirt/images/ に存在している
・KVM ゲストイメージのドメイン名が XXX のイメージファイルは /var/lib/libvirt/images/XXX.img とする 
・複製先の /etc/hosts, /etc/sysconfig/network は書き換えない
・複製先のMACアドレスを自動取得&UUIDを自動生成して /etc/sysconfig/network-scripts/ifcfg-eth0 と /etc/udev/rules.d/70-persistent-net.rules を書き換える
・複製先の起動は行わない

そのシェルスクリプトはこちらです: clone-guest.sh

この内容についてはこちらの記事を参考にしています:
http://lab.adn-mobasia.net/?p=471


使い方は KVM ホストにコピーして、実行権限付与後に以下のように実行します:

# ./clone-guest.sh guest1 guest2
(guest1 が複製元ドメイン名、guest2 が複製先ドメイン名、guest1 にはコンソールログインの設定が済んでいて、かつ停止中であること)

すると /var/lib/libvirt/images/guest1.img を元に /var/lib/libvirt/images/guest2.img が複製され、そのネットワーク定義ファイルが上記の条件で自動的に書き換えられて、でいつでも起動できる、という状態で生成されます。もし起動まで自動で行いたいのであれば clone-guest.sh の最後に以下の一行を追加すればできると思います:

$cmd_virsh start $clone_domain_name


このサンプルでは /etc/hosts や /etc/sysconfig/network の変更は(しなくても起動まではするはずなので)行っていませんが、絶対に必要になるような環境では中身を参考にしながら変えられると思うので挑戦してみてください。

またこのサンプルはあくまで DHCP 環境を前提にしています。固定 IP にも対応するようなのは、ちょっと面倒な気もしますが、どなたか挑戦してみてください。
 
ともあれ、いろいろ手入力しないといけない状況と比べると格段に簡単になりました。仮想環境が理想に近づきました。 

KVM の virsh console コマンドを使って、(極端な話、ネットワークが有効になっていなくても)ホストOSからゲストOSにコンソールログインすることができる、ことになっています。

が、何も考えずに virsh console (ドメイン名) を実行すると 

ドメイン XXXX に接続しました

エスケープ文字は ^] です

と表示され、ここで Enter キーを押すとログインプロンプトが表示される・・・はずなのですが、なぜか固まってしまいます。 

どうもゲストOS側でコンソールログインをするための設定が必要な模様。結論としては次の2点:

(1) /etc/grub.conf にカーネルオプションを追加設定

kernel /vmlinuz-2.6.18-238.el6 ro root=/dev/***   console=tty0 console=ttyS0,115200n8
(kernel で始まる行の最後に赤字部分を追加、kernel で始まる行が複数ある場合は全てに設定)

(2) /etc/inittab にコンソールモードの設定を追加

S0:12345:respawn:/sbin/agetty ttyS0 115200
(赤字の一行を追加)


これで再起動後に、改めてホストOSから virsh console XXXX でコンソールログインが可能になります。

なお、コンソールから抜けるには CTRL+] です。


ゲストOSが
 ・別のゲストOSを元に複製で作成して、
 ・DHCPでIPアドレス設定するようになっていて、
 ・かつGUIが使えない(virt-manager から参照できない)
ようなケースでは、起動直後のIPアドレスを確認するための手段が「コンソールログインして ifconfig」しかないので、この方法が必要になるのでした。



このページのトップヘ