tips: hosts ファイルから Ansible の NIC 設定タスクを構成する ※注記あり

hosts ファイルから、Ansible の NIC 設定タスクを構成するスクリプトを作成しました。
管理対象ホストに配布する(であろう) hosts ファイルから設定タスクを構成する格好です。ホスト名、IPアドレスを hosts で一元管理できるので**楽ちん**です。

例えば、以下のように使用します。

```shell
$ cat hosts | hosts2entity.pl '-mng$' | entity2yaml.pl
```

このスクリプトで、hosts ファイルに登録されたノードそれぞれについて `ノード名.yml` ファイルを生成します。それを Ansible の host_vars/　に置き、NIC 設定タスクで利用します。

----

# 注記: 2018/12/13 付

当初期待したほど**楽ちんでもない**、と判断するに至りました。以下の理由からです。

理由:
1. Ansible が接続に行く NIC を Ansible のタスクで設定変更するのはリスクが高い。 
2. 既存 NIC の設定が更新されず、NetworkManager で同じ NIC の新しい設定が出来たりする現象が発生した。
3. Ansible の nmcli module が依存する NetworkManager-glib は obsoleted 扱いになっている。 (cf. [Issue #48055 · ansible/ansible · GitHub](https://github.com/ansible/ansible/issues/48055) )

理由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](https://centos.pkgs.org/7/centos-x86_64/NetworkManager-glib-1.12.0-6.el7.x86_64.rpm.html) )。新しい API である [**libnm**](https://wiki.gnome.org/Projects/NetworkManager/libnm) に取って代わられるところのようです。
また、[Pull Request #48870](https://github.com/ansible/ansible/pull/48870) を見ると、`needs_revision` なんてタグが付けられていて、nmcli モジュールに根本的な見直しが図られる、みたいな流れになっている(?)ようです。

以上から、**今しばらくは nmcli Module には触らない方が良さそう**、という判断に大きく傾きました。

もっとも host_vars/ には人間が見る用に値だけ置いておいて、Ansible タスクでは参照しないでおく、というのもアリでしょう。
何事においてもそうですが、充分な事前検証の上での利用をお薦めいたします。


## この注記に関する参考文献

+ ["This module requires NetworkManager glib API" (NetworkManager-glib package obsoleted) · Issue #48055 · ansible/ansible · GitHub](https://github.com/ansible/ansible/issues/48055)
+ [nmcli: fix NetworkManager-glib package obsoleted by ssahani · Pull Request #48870 · ansible/ansible · GitHub](https://github.com/ansible/ansible/pull/48870)
+ [NetworkManager-glib-1.12.0-6.el7.x86_64.rpm CentOS 7 Download](https://centos.pkgs.org/7/centos-x86_64/NetworkManager-glib-1.12.0-6.el7.x86_64.rpm.html)
+ [Projects/NetworkManager/libnm - GNOME Wiki!](https://wiki.gnome.org/Projects/NetworkManager/libnm)

(注記終わり)

----

# どういうこと?

## 想定する構成

例として、以下のような3つのノードからなるシステムを想定します。

![nodes.png](https://qiita-image-store.s3.amazonaws.com/0/174932/d2fe2f11-5d56-fd7a-91f9-c9a7538edcf2.png)

+ 1つのノードが複数のNICを持ち、複数のネットワーク(ここでは管理系、サービス系)に接続する。
+ サービス系ネットワークに接続するアドレスは、'ノード名' のホスト名を割り当てる。
+ 管理系ネットワークに接続するアドレスは、'ノード名-mng' のホスト名を割り当てる。

## hosts ファイル

前述のような構成では、以下のような hosts ファイルにまとめられます。

```text
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アドレス`を設定します。

```YAML
---
- 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout)に倣い playbook を構成すると、`host_vars/ノード名.yml` に書かれた変数値が、当該ノードに対するタスクで適用されます。

以下のような `host_vars/ノード名.yml` ファイルを置くと、前述の NIC 設定タスクに反映されます。

```YAML
---
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 データを標準出力に出力します。※以下の出力例は読みやすいように行の順序を入れ替えています。

```perl
{
  '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 :

```YAML
hostname: node1
nic0_name: nic0
nic0_ipv4: 10.0.0.1/24
nic1_name: nic1
nic1_ipv4: 172.16.0.1/24
```

node2.yml :

```YAML
---
hostname: node2
nic0_name: nic0
nic0_ipv4: 10.0.0.2/24
nic1_name: nic1
nic1_ipv4: 172.16.0.2/24
```

node3.yml :

```YAML
---
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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout) のディレクトリ構成に従って構成しておきます。


## hosts2entity.pl

```perl
#!/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

```perl
#!/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 を使えばよかったです。

# 参考文献

+ [nmcli - Manage Networking — Ansible Documentation](https://docs.ansible.com/ansible/latest/modules/nmcli_module.html#nmcli-module)
+ [Best Practices — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)