hosts ファイルから、Ansible の NIC 設定タスクを構成するスクリプトを作成しました。
管理対象ホストに配布する(であろう) hosts ファイルから設定タスクを構成する格好です。ホスト名、IPアドレスを hosts で一元管理できるので楽ちんです。
例えば、以下のように使用します。
$ cat hosts | hosts2entity.pl '-mng$' | entity2yaml.pl
このスクリプトで、hosts ファイルに登録されたノードそれぞれについて ノード名.yml
ファイルを生成します。それを Ansible の host_vars/ に置き、NIC 設定タスクで利用します。
注記: 2018/12/13 付
当初期待したほど楽ちんでもない、と判断するに至りました。以下の理由からです。
理由:
- Ansible が接続に行く NIC を Ansible のタスクで設定変更するのはリスクが高い。
- 既存 NIC の設定が更新されず、NetworkManager で同じ NIC の新しい設定が出来たりする現象が発生した。
- Ansible の nmcli module が依存する NetworkManager-glib は obsoleted 扱いになっている。 (cf. Issue #48055 · ansible/ansible · GitHub )
理由1. は、構造的な問題です。Ansible からは管理系 NIC に接続に行くように構成する(だろう?)から、とするとサービス系 NIC の設定変更だけに留めるのが吉でしょう。そもそも Ansible を使う時点で少なくとも NIC 1つ(管理系?)は設定済みなので、わざわざ設定変更する必要がありません。
理由2. は、Vagrant + VirtualBox 環境との相性なのかも知れません(深く検証していません)。タスクを実行後、nmcli conn で見たら、同じ NIC に UUID の異なる 2 つの設定が出来ていたり、ifcfg-eth1 の他に ifcfg-eth1-1 みたいな設定ファイルが出来ていたり…と、おかしな動作が見受けられました。
理由3. は、過渡的な問題、だと思います。obsolated なので標準では NetworkManager-glib はインストールされません。nmcli モジュールの利用前に追加インストールしておく必要があります。
NetworkManager-glib のパッケージ情報を見ると old API
なんて書かれています(cf. NetworkManager-glib-1.12.0-6.el7.x86_64.rpm )。新しい API である libnm に取って代わられるところのようです。
また、Pull Request #48870 を見ると、needs_revision
なんてタグが付けられていて、nmcli モジュールに根本的な見直しが図られる、みたいな流れになっている(?)ようです。
以上から、今しばらくは nmcli Module には触らない方が良さそう、という判断に大きく傾きました。
もっとも host_vars/ には人間が見る用に値だけ置いておいて、Ansible タスクでは参照しないでおく、というのもアリでしょう。
何事においてもそうですが、充分な事前検証の上での利用をお薦めいたします。
この注記に関する参考文献
- "This module requires NetworkManager glib API" (NetworkManager-glib package obsoleted) · Issue #48055 · ansible/ansible · GitHub
- nmcli: fix NetworkManager-glib package obsoleted by ssahani · Pull Request #48870 · ansible/ansible · GitHub
- NetworkManager-glib-1.12.0-6.el7.x86_64.rpm CentOS 7 Download
- Projects/NetworkManager/libnm - GNOME Wiki!
(注記終わり)
どういうこと?
想定する構成
例として、以下のような3つのノードからなるシステムを想定します。
- 1つのノードが複数のNICを持ち、複数のネットワーク(ここでは管理系、サービス系)に接続する。
- サービス系ネットワークに接続するアドレスは、'ノード名' のホスト名を割り当てる。
- 管理系ネットワークに接続するアドレスは、'ノード名-mng' のホスト名を割り当てる。
hosts ファイル
前述のような構成では、以下のような hosts ファイルにまとめられます。
172.16.0.1 node1
172.16.0.2 node2
172.16.0.2 node3
10.0.0.1 node1-mng
10.0.0.2 node2-mng
10.0.0.3 node3-mng
ansible の NIC 設定タスク
ansible で以下のようなタスクを定義しておくと、管理対象ホストそれぞれにホスト名
、NICごとにIPアドレス
を設定します。
---
- name: setup hostname
hostname:
name: "{{ hostname }}"
- name: setup ipv4 addr. for a management network
nmcli:
conn_name: "{{ nic0_name }}"
type: ethernet
ip4: "{{ nic0_ipv4 }}"
state: present
- name: setup ipv4 addr. for a service network
nmcli:
conn_name: "{{ nic1_name }}"
type: ethernet
ip4: "{{ nic1_ipv4 }}"
state: present
{{ }}
で括られた {{ hostname }}
、{{ nic0_name }}
、{{ nic0_ipv4 }}
といった部分には、変数が当てはめられます。
変数の値をノードごとに設定すれば、各ホストに個別の設定が反映されます。
※Ansible が接続に行く先の NIC (管理系 NIC を想定) は、タスクで設定をいじらない方が良いと思いますので、nic0 に関するタスクはコメントアウトしましょう(前述の注記を参照のこと)。
変数の値をノードごとに設定する
ansible で、Best Practicesに倣い playbook を構成すると、host_vars/ノード名.yml
に書かれた変数値が、当該ノードに対するタスクで適用されます。
以下のような host_vars/ノード名.yml
ファイルを置くと、前述の NIC 設定タスクに反映されます。
---
hostname: node1
nic0_name: nic0
nic0_ipv4: 10.0.0.1/24
nic1_name: nic1
nic1_ipv4: 172.16.0.1/24
スクリプトで何をするの?
ノード名.yml
というファイルを生成します。これらを host_vars/ の下に置き、ansible の NIC 設定タスクに読み込ませよう、という算段です。
スクリプト hosts2entity.pl
このとき、1つのノードが複数のNICを持つので、hosts ファイル上では複数のエントリに分かれて記述されます。それをノードごとに1つにまとめる必要があります。
hosts2entity.pl
は、複数行に分かれた hosts エントリを、ホスト名文字列の共通部分を基にまとめます。
ただし、1つのノードにまとめられるホスト名は、あらかじめ【共通部分】+【付加部分】となるように設計しておきます。
例1:
- node1
- node1-mng
という、2つのホスト名の hosts エントリを、1つのノード node1
のデータとしてまとめるには、スクリプト hosts2entity.pl
に -mng$
という引数を渡します。
要するに、【付加部分】にあたる文字列を指定します。正規表現を使用できます。
結果、hosts2entity.pl
は Data::Dumper
で文字列化された hash データを標準出力に出力します。※以下の出力例は読みやすいように行の順序を入れ替えています。
{
'node1' => {
'_entity' => 'node1',
'node1' => [
'172.16.0.1'
],
'node1-mng' => [
'10.0.0.1'
],
'_rules' => [
'',
'-mng$'
],
'_keys' => [
'node1',
'node1-mng'
]
},
'node2' => {
'_entity' => 'node2',
'node2' => [
'172.16.0.2'
],
'node2-mng' => [
'10.0.0.2'
],
'_rules' => [
'',
'-mng$'
],
'_keys' => [
'node2',
'node2-mng'
]
},
'node3' => {
'_entity' => 'node3',
'node3' => [
'172.16.0.3'
],
'node3-mng' => [
'10.0.0.3'
],
'_rules' => [
'',
'-mng$'
],
'_keys' => [
'node3',
'node3-mng'
]
},
}
スクリプト entity2yaml.pl
entity2yaml.pl
は、hosts2entity.pl
の出力を YAML 形式に変換して出力します。
とはいっても、hosts2entity.pl
の出力からどの値を拾うか、は ansible の NIC 設定タスクの書き方や、ansible の実行環境の構成によって変わります。hosts2entity.pl
の出力を眺めて、sub genYAML
や $file_output
辺りを調整して使用します。単純な整形です。
結果、以下のようなファイル出力を得ます。
node1.yml :
hostname: node1
nic0_name: nic0
nic0_ipv4: 10.0.0.1/24
nic1_name: nic1
nic1_ipv4: 172.16.0.1/24
node2.yml :
---
hostname: node2
nic0_name: nic0
nic0_ipv4: 10.0.0.2/24
nic1_name: nic1
nic1_ipv4: 172.16.0.2/24
node3.yml :
---
hostname: node3
nic0_name: nic0
nic0_ipv4: 10.0.0.3/24
nic1_name: nic1
nic1_ipv4: 172.16.0.3/24
ソースコード
以下の環境で、動作を確認しています。
- CentOS 7
- perl
- Ansible 2.7
Ansible は、 Best Practices のディレクトリ構成に従って構成しておきます。
hosts2entity.pl
# !/usr/bin/perl
use strict;
use warnings;
use utf8;
use Data::Dumper;
$Data::Dumper::Terse = 1;
# parse args
my @rules = ("", @ARGV);
# ==========================================================
# PROCESS
# ==========================================================
my $rh_hosts_ip = readHostsFile();
my $rh_entry = unifyEntity($rh_hosts_ip, @rules);
print Dumper($rh_entry);
exit(0);
# ==========================================================
# routines
# ==========================================================
# --------------------
# unifyEntity
# ... unify several hosts entries as ONE node
#
sub unifyEntity
{
my ($rh_input, @regexp) = @_;
my (%return) = ();
foreach my $k (keys(%{$rh_input}))
{
my ($entity, $index) = ($k, 0);
my $i = 0;
foreach my $r (@regexp)
{
if($k =~ /$r/)
{
$entity =~ s/$r//;
$index = $i;
}
$i++;
}
if(! exists($return{$entity}))
{
$return{"$entity"} = {
"_entity" => "$entity",
"_rules" => [@regexp],
"_keys" => [],
};
}
my $r_value = $return{"$entity"};
my $values_index = 'v_' . $k;
$r_value->{"_keys"}->[$index] = $k;
$r_value->{$k} = $rh_input->{"$k"};
}
return \%return;
}
# --------------------
# readHostsFile
#
# returns:
# {
# hostname1 => [ ipaddr1 ],
# hostname2 => [ ipaddr2, ipaddr2x, ...], # invalid, 2 addr for 1 host.
# :
# }
#
sub readHostsFile
{
my %entry = ();
while(<STDIN>)
{
s/#.*$//; # delete a comment part in a line.
next if (/^\s*$/); # skip a comment and an empty line
my ($ipaddr, @hostname) = split(/\s+/);
foreach my $h (@hostname)
{
if(exists($entry{"$h"}))
{
warn "WARN: a key '$h' duplicated: the entry already exists";
}
else
{
$entry{"$h"} = [];
}
my $r_values = $entry{"$h"};
push(@{$r_values}, $ipaddr);
}
}
return \%entry;
}
entity2yaml.pl
# !/usr/bin/perl
use strict;
use warnings;
use utf8;
use Data::Dumper;
$Data::Dumper::Terse = 1;
my ($dumped_hash, $rh_input) = ();
{
local $/ = '';
$dumped_hash = <>;
}
eval( '$rh_input = ' . "$dumped_hash" );
# ==========================================================
# PROCESS
# ==========================================================
foreach my $k (keys(%{$rh_input}))
{
my $output = genYAML($rh_input->{$k});
my $file_output = sprintf("%s.yml", $k);
my $fh;
open($fh, ">", $file_output) && print $fh $output;
close($fh);
}
exit(0);
# ==========================================================
# routines
# ==========================================================
sub genYAML
{
my ($r_value) = @_;
my $hostname = $r_value->{'_entity'};
my $nic0_host = $r_value->{'_keys'}->[1];
my $nic1_host = $r_value->{'_keys'}->[0];
my $nic0_ipv4 = $r_value->{$nic0_host}->[0];
my $nic1_ipv4 = $r_value->{$nic1_host}->[0];
my $yaml = <<EOM;
---
hostname: $hostname
nic0_name: nic0
nic0_ipv4: $nic0_ipv4/24
nic1_name: nic1
nic1_ipv4: $nic1_ipv4/24
EOM
return $yaml;
}
あとがき
今どきどこでも使えるだろう、と見越して perl で実装しましたが、よくよく考えれば Ansible 実行環境なのだから python を使えばよかったです。