Ansibleのlineinfileモジュールでパターンの否定と後方参照する

目的としてはCentOS7で変なのになるNICのデバイス名をeth0とかの従来のままにしたかっただけで、
そのタスクを書こうとした備忘録です。

[オンプレで物理ならOS入れる時にvmlinuzの後ろに`net.ifnames=0`オプションつければいける](https://moriya.xrea.jp/tdiary/20150911.html)みたい。
あとは[cobblerとか](https://qiita.com/yunano/items/d53d57158e0fed4aad43)でもいいみたいですね。

要はgrubの設定ファイルの任意の1行を特定の文字列がない場合に部分的に文字列置換をしたかったんですけど、
ダブルクォートの後ろに文字列が追加されたり2重に追加されたりしないようにする正規表現的なところに手間取りました。

で、、以下でまあなんとかなりそうな気がするのでtemplateにせずにlineinfileでいけそう。
（templateにするとバージョンとか環境で色々違う場合に一様になってしまうのに抵抗感がある設定ファイルだとちょっとめんどくさいことになるというか）
まあOSイメージに入ってたら要らないんですけどね。(packer側のタスクにいれといたほうが効率よさそうなやつ）
以下では適当なファイルを作成して試していますが本来設定したいファイルは`/etc/default/grub`です。

```
# ansible localhost -c local -m lineinfile -b \
 -a 'dest=./hoge regexp="(^GRUB_CMDLINE_LIN*(?!.*net.ifnames=0).*)(\")$" line="\1 net.ifnames=0 biosdevname=0\"" backrefs=yes' -C -D

--- before: ./hoge (content)
+++ after: ./hoge (content)
＠＠ -1,5 +1,5 ＠＠
 GRUB_CMDLINE_LINUX="console=tty1 console=ttyS0,115200n8 earlyprintk=ttyS0,115200 rootdelay=300 net.ifnames=0" 
-GRUB_CMDLINE_LINUX="rhgb quiet" 
+GRUB_CMDLINE_LINUX="rhgb quiet net.ifnames=0 biosdevname=0" 
 GRUB_CMDLINE_LINUX="rhgb quiet net.ifnames=0 biosdevname=0" 
 GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet net.ifnames=0 biosdevname=0" 

localhost | SUCCESS => {
    "backup": "", 
    "changed": true, 
    "msg": "line replaced" 
}
```
パターンの否定と後方参照のあたりがだいぶ勉強になりました。
[正規表現：文字列を「含まない」否定の表現まとめ | WWWクリエイターズ](http://www-creators.com/archives/1827)
[Ansibleのlineinfileのオプションを詳しく見てみる – Koltatt](http://koltatt.net/programing/lineinfile_options/)

ansibleのlineinfileモジュールはbackrefsをyesにすると部分的にパターンマッチしたやつを置換後の文字列として使えるようです。
正規表現のパターンマッチの時にカッコ()でくくって一致した文字列を置換後に\1と書いて呼ぶのが後方参照。
1番目のカッコは\1、2番目のカッコは\2と書いて呼ぶかんじです。

以下のAnsibleオプションを解説すると、`net.ifnames=0`に一致しないが`GRUB_CMDLINE_LIN`から始まるダブルクォートの手前までを\1で後で見られるようにカッコ()でくくっておいて、
置換後の文字列で置換前の文字列のうしろに必要な設定を追加してダブルクォートで閉じる、ということです。

`'dest=./hoge regexp="(^GRUB_CMDLINE_LIN*(?!.*net.ifnames=0).*)(\")$" line="\1 net.ifnames=0 biosdevname=0\"" backrefs=yes'`

パターンの否定の部分、一致しないにあたるのは、`(?!.*PATTERN)`ですが、詳しくは[参考リンク先](http://www-creators.com/archives/1827)をどうぞ。

これでとりあえず見たことあるパターンでおかしな動作はしないことはたぶん確認できた気がします。

taskとしてはこんな感じに。

```
- name: delete config of udev's network device naming rules.
  file: 
    dest: "/etc/udev/rules.d/{{ udevnwnamecfg }}"
    state: absent
  with_items:
    - 60-net.rules
    - 71-biosdevname.rules
    - 75-net-description.rules
    - 80-net-name-slot.rules
  loop_control:
    loop_var: udevnwnamecfg

- name: to hold nic-name at grub config of centos7 like a centos6
  lineinfile:
    backrefs: yes
    dest: "/etc/default/grub"
    regexp: "(^GRUB_CMDLINE_LIN*(?!.*net.ifnames=0).*)(\")$"
    line: '\1 net.ifnames=0 biosdevname=0\"'

```

で、これがしてあってOS再起動が行われていさえすればインタフェースファイル名はむかしのとおりと思うのですが、すでに違う名前で上がってきてるやつもどうにかeth0とかにしたいという場合のためだけに作ったタスクが以下。
（NICを手動で固定にしたいケースのみでdhcpやNetworkmanagerのmethodがautoだと向いてないと思われる。）

```
# デバイス名にeth0が含まれなかったらloopでlo以外の全てのデバイスを変更
- name: check network device names. #戻り値はens192\nens224\nのようになる
  shell: ls -1 /etc/sysconfig/network-scripts/ifcfg-*|grep -v lo|sed -e 's,/etc/sysconfig/network-scripts/ifcfg-,,g'
  register: chk_nwif_name
  check_mode: no
  ignore_errors: yes
  changed_when: false

- block:
  - name: copy network device file
    copy:
      remote_src: true
      src: "/etc/sysconfig/network-scripts/ifcfg-{{ nwdev[1] }}" #ens192など添え字1のほうが順に入る
      dest: "/etc/sysconfig/network-scripts/ifcfg-eth{{ nwdev[0] }}" #0など添え字0側が順に入る
    register: cp_nwif_res
    with_indexed_items: "{{ chk_nwif_name.stdout_lines }}"  #indexつきでloopで改行区切りで読まれる[0,ens192\n1,ens224]
    loop_control:
      loop_var: nwdev
  - name: set fact interfacefiles
    set_fact:
      ifcfg_dests: "{{ cp_nwif_res.results | map(attribute='dest') | list }}" #[/etc/sysconfig/network-scripts/ifcfg-ens192,/etc/...]という戻り値
      ifcfg_srcs: "{{ cp_nwif_res.results | map(attribute='src') | list }}" #[/etc/sysconfig/network-scripts/ifcfg-eth0,/etc/...]という戻り値
  - name: replace old network name line on new name network device config.
    lineinfile:
      dest: "{{ repif[0] }}"
      regexp: "^{{ repif[2] }}={{ repif[1] | basename | regex_replace('ifcfg-') }}" #^NAME=ens192などになる
      line: "{{ repif[2] }}={{ repif[0] | basename | regex_replace('ifcfg-') }}" #NAME=eth0などになる
    with_nested:
      - "{{ ifcfg_dests }}"
      - "{{ ifcfg_srcs }}"
      - [ 'NAME', 'DEVICE' ] #with_nestedによりすべての組み合わせで作動する
    loop_control:
      loop_var: repif
  - name: delete old network device name file
    file:
      path: "{{ delif }}" #[/etc/sysconfig/network-scripts/ifcfg-ens192,/etc/...]が入る
      state: absent
    with_items: "{{ ifcfg_srcs }}"
    loop_control:
      loop_var: delif
    notify:
      - OS-reboot
      - OS-start-wait
  when: not 'eth0' in chk_nwif_name.stdout_lines #eth0がインタフェースファイルの中に無かったらblockの中身が作動
```
これ書くのちょっと自分の脳味噌的にアレで時間かかりました。
（nestedだとdestとsrcも連結して呼んでからloop内で分離して使うみたいな風に変えないとアレかもしれない）
まあでもループ処理とその変数の扱いについての理解が進んで楽しかったりはしました。
ただこれAMIとかOSテンプレートとかOSイメージ側に事前にgrubなどの設定がしてある場合は不要だと思われ。
要るケースは事前のイメージの設定が漏れててすでにやらかしてる時だけかな。
loopでcpした元ファイルを間違いのないように消したい実はmvしたかった的な用途には上記が多少役立つかも。
ただcopyモジュールに元ファイル削除オプションついてたらよかったのにとちょっと思いました。

参考：
[正規表現：文字列を「含まない」否定の表現まとめ | WWWクリエイターズ](http://www-creators.com/archives/1827)
[Ansibleのlineinfileのオプションを詳しく見てみる – Koltatt](http://koltatt.net/programing/lineinfile_options/)
[【CentOS 7】ネットワークインタフェース名をeth0などの旧来の方式に戻す方法 | Step On Board](http://www.lesstep.jp/step_on_board/centos-7/773/)
[CentOS7におけるNIC命名ルール - 雑木林](http://zokibayashi.hatenablog.com/entry/2015/04/05/035642)
[lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression. — Ansible Documentation](http://docs.ansible.com/ansible/latest/lineinfile_module.html#options)