zaki work log

作業ログやら生活ログやらなんやら

VyOSをインストールしてIPアドレスとsshの初期設定

OSSで開発されているネットワークOS(Wikipediaの説明より)であるVyOSを使って、仮想環境上にルーターを構築してみる。
VyOSはDebianベースで動作するOSで、VMにインストールすることができる。

vyos.io

ちなみにLinux(CentOS)をルーターにするには以下参照。

zaki-hmkc.hatenablog.com

社内のハンズオンイベントで扱ったりひよこ本にも載ってたりしててVyOS触っておきたいなーと思いつつタスク積んでいたけど、Linuxにスタティックルートの設定をする際にこれあった方が絶対によさそうだったのでようやく自宅ラボに導入しようとしてます。。
ちなみに本記事ではVyOSのイメージの入手とOSインストール、OSインストール後のIPアドレス設定と外部からssh接続できるようにする設定までです。

Rolling Release版のVyOS イメージの入手

f:id:zaki-hmkc:20210408094742p:plain

「Rolling Release」であればISOのダウンロードページにすぐたどり着く。

なんだけど、初めてなのでLTS(2021.04.07時点でv1.2.7)を使いたい。

vyos.io

なんだけど、ここを見ると全部有償版に見える。。。
ハテ…と思ってドキュメントのinstallationのページを見てみると、

docs.vyos.io

登録すればダウンロードできるっぽい?

Registered subscribers can log into https://support.vyos.io/ to have access to a variety of different downloads via the “Downloads” link. These downloads include LTS (Long-Term-Support) and associated hot-fix releases, early public access releases, pre-built VM images, as well as device specific installation ISOs.

というわけで、ここからユーザー登録。

support.vyos.io

・・・してみたけど、やっぱりLTSのダウンロードページを見つけられない。

ということであきらめて、現在の最新版を使ってみる。
最初にも書いたけど「Rolling Release」->「Downloading Rolling Release」で1.4のISOファイルダウンロードページにたどり着く。

downloads.vyos.io

「latest」をダウンロードするとあとからわからなくなるので、2021.04.07時点で最新の「vyos-1.4-rolling-202104061641-amd64.iso」をゲット。(中身はlatest)

ちなみにOVAファイルも配布されてたけど、1.1.8で妙に古かったのでスルー。

VMの作成

インストールのドキュメントは以下。

docs.vyos.io

The minimum system requirements are 512 MiB RAM and 2 GiB storage. Depending on your use you might need additional RAM and CPU resources e.g. when having multiple BGP full tables in your system.

マシンスペックは掲載されてるけど、ESXiで作る際の「ゲストOSのバージョン」はいまいち。ベースはDebianだけどバージョンがわからず。
あとで変更できるし、とりあえず「Debian GNU/Linux 10 (64bit)」で作成。 (ちなみに以下の通り10.9なので最新版のイメージであれば選択するDebianバージョンも最新でたぶん大丈夫)

vyos@vyos:~$ cat /etc/debian_version 
10.9

今回はルーターとして動かしたいのでNICを2個セット。
マシンスペックはそんなに必要ないみたいだけど、一応こんな感じ。

f:id:zaki-hmkc:20210408095328p:plain

install

CDブートするとLive Linux状態で使用可能になる。
VMにインストールするには「Permanent installation」の項を参照

docs.vyos.io

電源投入するとログイン待ち状態になるので、ユーザー名・パスワードはどちらもvyosを入力しコンソールログインする。

インストールするにはinstall imageを実行。
聞かれる質問は以下の通り。基本デフォルトで問題ない。

  • Would you like to continue? (Yes/No) [Yes]:
  • Partition (Auto/Parted/Skip) [Auto]:
  • Install the image on? [sda]:
  • This will destroy all data on /dev/sda. Continue? (Yes/No) [No]: Yes
  • How big of a root partition should I create? (2000MB - 4294MB) [4294]MB:

これでCreating filesystem on /dev/sda1 と表示されパーティションが作成される。

  • What would you like to name this image? [1.4-rolling-202104061641]:
  • Which one should I copy to sda? [/opt/vyatta/etc/config/config.boot]:
  • Enter password for user 'vyos': <管理ユーザー"vyos"のパスワードを入力する>
  • Retype password for user 'vyos':
  • Which drive should GRUB modify the boot partition on? [sda]:

Setting up grub: と出てOKと表示されれば処理完了。
poweroffしてシャットダウンし、isoファイルをVMから取り外す。

設定

ディスクから起動したらvyosユーザーでログイン。
主要な設定はQuick Startを参照。

docs.vyos.io

IPアドレス設定

NICを2個乗せているので、ip aで2つあることを確認(このコマンドでいいんだっけ 後述)し、eth0eth1がそれぞれVMのどのNICかをESXiのネットワークアダプタMACアドレスを見て確認する。

configureを実行して設定モードに変更。

vyos@vyos:~$ configure
[edit]
vyos@vyos# 

これはプロンプトが以下の2行になるって感じかな。。

[edit]
vyos@vyos#

で、eth0のアドレス設定。
172.16.1.3でサブネットは/23
DHCPはオフになってるので特にdeleteの必要はない。

set interfaces ethernet eth0 address 172.16.1.3/23

eth1は別途設定するので未設定のまま残しておく。

setを実行したらshowで内容を確認できる。
問題無ければ、commitで適用し、saveで保存する。

(commitを実行した時点で設定が反映されるので他のホストからpingなどで反応できる。ただしsaveしないとrebootで設定は消える)

設定確認

最初ip aを使って確認してた…

vyos@vyos:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 fe80::200:ff:fe00:0/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:3b:48:8c brd ff:ff:ff:ff:ff:ff
    inet 172.16.1.3/23 brd 172.16.1.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe3b:488c/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:3b:48:96 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::20c:29ff:fe3b:4896/64 scope link 
       valid_lft forever preferred_lft forever

けどここはネットワークOSらしく、show interfacesを使おう。

vyos@vyos:~$ show interfaces 
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             172.16.1.3/23                     u/u  
eth1             -                                 u/u  
lo               127.0.0.1/8                       u/u  
                 ::1/128     

sshサーバー

以降の作業用にsshサーバーを有効にする。
IPアドレスと同様configureで設定モードで実行する。

vyos@vyos:~$ configure
[edit]
vyos@vyos# 
set service ssh

これを実行し、IPアドレスの設定と同様にcommitsaveすれば、他のホストからsshで接続できるようになる。

zaki@ubuntu-node:~$ ssh vyos@172.16.1.3
The authenticity of host '172.16.1.3 (172.16.1.3)' can't be established.
ECDSA key fingerprint is SHA256:O03Yp97VsV5iOpw+BrW/tUvQH6l3ritn1+njB8rR9CE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '172.16.1.3' (ECDSA) to the list of known hosts.
Welcome to VyOS

vyos@172.16.1.3's password: 
Linux vyos 5.10.27-amd64-vyos #1 SMP Sun Apr 4 22:05:07 UTC 2021 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
vyos@vyos:~$ 

というわけで、VyOSをインストールしてeth0のみアドレス設定完了。
現在の構成はこんな感じ。

f:id:zaki-hmkc:20210412080027p:plain

ルーターの設定などは次回で。。

[Ansible] asyncとpollを使った非同期処理とループの並列実行

Ansibleのループ処理はデフォルトでは逐次処理で動作します。
例えばループの回数が5回で、1回の処理に10秒ずつかかる場合は合計50秒はかかる計算になります。

Ansibleではループによって複数回実行される処理がお互いに干渉せずに並列に実行可能なのであれば、asyncpollの指定で並列に処理することができます。
1回10秒(x5)の処理を並行で同時に実行することで10秒で完了する計算になります。

docs.ansible.com

デフォルト(逐次処理)の動作

まずは普通の逐次処理されるループ処理のplaybookです。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: clear log
      file:
        path: async.log
        state: absent

    - name: loop sample
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4

ごちゃごちゃ書いてるけど、shellでやってるのは以下の3つのコマンドを実行している。

  • 現在日時とsleepする時間とbeginをファイル書き込み
  • 指定秒数sleep
  • 現在日時とsleepした時間とendをファイル書き込み

loopを使って[2, 3, 4]のリストを渡すことで、「2秒スリープ」「3秒スリープ」「4秒スリープ」が順番に処理されることを出力されるファイルで確認できます。
出力されるファイルはこんな感じ。

2021.04.06_20.06.50; 2 begin
2021.04.06_20.06.52; 2 end
2021.04.06_20.06.53; 3 begin
2021.04.06_20.06.56; 3 end
2021.04.06_20.06.56; 4 begin
2021.04.06_20.07.00; 4 end

shellで実行される処理が並行して実行されていないことが確認できました。

今回は、このスリープ処理を同時に実行させてみます。

並列処理

docs.ansible.com

Ansibleの並列処理に必要な非同期の処理は基本全部ここにのってる。はず。
ちなみにループの場合の応用もこのページに載ってます。

asyncとpollによる非同期処理

今回使うのはasyncpoll。どちらも整数(秒)を引数に取ります。

async

asyncを単体で使用すると、「指定秒数以内にtaskが完了しない場合はエラー」となります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4

これを実行すると、sleep 10の最中にasync: 4によるタイムアウトが発動し、以下のようにエラーとなります。

fatal: [localhost]: FAILED! => changed=false 
  msg: async task did not complete within the requested time - 4s

async + poll

pollの指定が無い場合は、デフォルト値は15のため、15秒経ってエラーとして動作します。
なので、asyncに指定した値より大きい値はあまり意味がない(と思う)ので、おそらくasyncの秒数の約数にしておくとベターと思う。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4
      poll: 1

このようにpoll: 1にしておくと、実行しているモジュールが完了するかasyncで指定した4秒が経過するかを、pollに指定した1秒毎にチェックする動作になります。

poll:0

poll: 0は特殊で、asyncで指定しているtaskを非同期で処理しつつ、その完了を待たずに次のtaskへ処理が進むという動作になります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 11
      poll: 0
      # shellで実行している処理がタイムアウトしないように`async`の値は11秒に変更

上記のtask定義でansible-playbookを実行すると、shellの完了を待たずに非同期で処理が継続され、taskが完了します。
また、他にtaskがなければansible-playbookの実行は終了しシェルに処理が戻りますが、バックグラウンドで非同期処理用のプロセスが動作し続けます。

ansible-playbookは終了しても処理が実行してる最中にps auxfwするとこんな感じ。

zaki     110284  0.0  0.1 215616 11040 ?        S    21:08   0:00 /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zaki/.a
zaki     110285  0.0  0.1 215616 11412 ?        S    21:08   0:00  \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zak
zaki     110286  5.0  0.1 207000 14188 ?        S    21:08   0:00      \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home
zaki     110298  0.0  0.0 113288  1420 ?        S    21:08   0:00          \_ /bin/sh -c echo "start" >> async-poll.log; sleep 10; e
zaki     110299  0.0  0.0 108056   356 ?        S    21:08   0:00              \_ sleep 10

ループ処理に応用

ここまでの async + poll:0 設定を踏まえて、ループ処理とコンボすることで、ループで実行される各処理を逐次処理でなく並列に同時に実行することができます。

    - name: async loop
      shell: echo "{{ item }} begin" >> async.log; sleep "{{ item }}"; echo "{{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 5
      poll: 0

冒頭の逐次処理を行っていたtaskにasync+pollを追加。
これを実行した結果出力されるasync.logファイルは以下の通り。

2021.04.06_21.12.22; 2 begin
2021.04.06_21.12.22; 3 begin
2021.04.06_21.12.22; 4 begin
2021.04.06_21.12.24; 2 end
2021.04.06_21.12.25; 3 end
2021.04.06_21.12.26; 4 end

beginが同時に書き込まれ、endは各時間のスリープが終わったあとに書き込まれることを確認でき、ループ内の各処理が並列に処理されています。

非同期で実行した処理を待つ

asyncpoll:0の指定で並列処理できるようになったけど、実際のplaybookでは「並列処理したtaskの結果がどうだったか」というのを以降の処理を実行する前に結果を確認したうえで次の処理を行うことが多いと思います。
Ansibleのasyncを使った非同期処理はその結果を保持した上で、ansible.builtin.async_statusモジュールを使ってwaitできます。

docs.ansible.com

    # ループの並列処理
    - name: async loop
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 1
      poll: 0
      register: result_async

    # 並列処理の完了を待つ
    - name: wait
      ansible.builtin.async_status:
        jid: "{{ item.ansible_job_id }}"
      loop: "{{ result_async.results }}"
      register: async_poll_results
      until: async_poll_results.finished
      retries: 10
      delay: 1

並列ループ処理はregisterを使って結果を保持。
そしてその結果にはansible_job_idという値が含まれており、これをjid(Job identifier)パラメタに指定します。

このときに「ループとregisterを組み合わせた場合はresultsにリスト形式で各ループごとの結果が保持される」という動作に合わせて、async_statusモジュールを使ったJob identifierの指定もloopディレクティブと{{ item.ansible_job_id }}を使ってループごとに指定します。

zaki-hmkc.hatenablog.com

そしてこのtaskは、前の並列ループ処理が非同期で処理される結果、まだ未完了の状態で起動されるため、「並列で動作している非同期処理がすべて完了するまで」リトライし続けるように記述します。
これは、このtaskの結果をregisterで保持し、その保持した結果をuntilretriesを使って指定の状態(registerで保持した辞書型の変数のfinishedの値)が真になるまで繰り返し実行します。

非同期処理を待つplaybook自体は以下のエントリを参考。

zaki-hmkc.hatenablog.com

これで、並列実行のループ処理を実現しつつ、後続のtaskではその結果を待ったうえで処理を続けることができます。

環境

確認した環境は以下の通り。

ansible 2.10.5
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

[Ansible] loopとモジュール実行の結果を保持するregisterを併用する

registerを使うことでモジュールの実行結果(Return Values)を保持し、後続のタスクでその値を参照することができるので、処理の実行結果を次のタスクのパラメタにすることができます。
ただし、その結果のデータ構造がループでの実行時は通常とは異なるのでそれについての覚え書き。

通常時のサンプル

指定コマンドを実行するplaybookです。

---
- hosts: localhost
  gather_facts: false

  tasks:
    # コマンド実行
    - name: exec command
      ansible.builtin.command: hostname
      register: result_command

    - name: print result
      debug:
        msg: '{{ result_command }}'

1つ目のtaskでコマンド実行しつつ、その結果を2つ目のtaskで出力。内容は以下の通り。
commandのReturn Valueについてはドキュメントのこちら

ok: [localhost] => 
  msg:
    changed: true
    cmd:
    - hostname
    delta: '0:00:00.001591'
    end: '2021-04-06 18:53:48.144296'
    failed: false
    rc: 0
    start: '2021-04-06 18:53:48.142705'
    stderr: ''
    stderr_lines: []
    stdout: cloud-dev
    stdout_lines:
    - cloud-dev

この通り、モジュールの実行結果をregisterで指定した名称の変数にセットでます。
commandの場合は、コマンドの戻り値がrc・標準出力がstdoutに保持されます、ディクショナリ型データになっているのでキーを指定して参照できます。

ループ併用時

loopを使って以下の3つのコマンドを実行するように変更。

  • hostname
  • pwd
  • ls /opt
    - name: exec command
      ansible.builtin.command: "{{ item }}"
      loop:
        - hostname
        - pwd
        - ls /opt
      register: result_command

    - name: print result
      debug:
        msg: '{{ result_command }}'

この時の実行結果は以下の通り。
(長いので一部省略)

ok: [localhost] => 
  msg:
    changed: true
    msg: All items completed
    results:
    - ansible_loop_var: item
      changed: true
      cmd:
      - hostname
      delta: '0:00:00.001672'
      end: '2021-04-06 18:54:28.461799'
      failed: false
      invocation:
        module_args:
          ...
      item: hostname
      rc: 0
      start: '2021-04-06 18:54:28.460127'
      stderr: ''
      stderr_lines: []
      stdout: cloud-dev
      stdout_lines:
      - cloud-dev
    - ansible_loop_var: item
      changed: true
      cmd:
      - pwd
      delta: '0:00:00.001590'
      end: '2021-04-06 18:54:28.630725'
      failed: false
      invocation:
        module_args:
          ...
      item: pwd
      rc: 0
      start: '2021-04-06 18:54:28.629135'
      stderr: ''
      stderr_lines: []
      stdout: /home/zaki/src/ansible-sample/file
      stdout_lines:
      - /home/zaki/src/ansible-sample/file
    - ansible_loop_var: item
      changed: true
      cmd:
      - ls
      - /opt
      delta: '0:00:00.001935'
      end: '2021-04-06 18:54:28.798976'
      failed: false
      invocation:
        module_args:
          ...
      item: ls /opt
      rc: 0
      start: '2021-04-06 18:54:28.797041'
      stderr: ''
      stderr_lines: []
      stdout: |-
        cni
        containerd
      stdout_lines:
      - cni
      - containerd

loopwith_itemsなどのループとの併用時は、各ループ毎の実行結果はresultsというキー以下にリスト型でセットされるようにデータ構造が変化します。

該当ドキュメント

ループのドキュメントに載っています。

docs.ansible.com

When you use register with a loop, the data structure placed in the variable will contain a results attribute that is a list of all responses from the module. This differs from the data structure returned when using register without a loop:

環境

ansible 2.10.5
  config file = /home/zaki/src/ansible-sample/file/ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

ちょっと古いな…

Return Valueをカジュアルに確認したい場合

個人的には普段はansible-playbook実行時に-vを付けて実行してます。
verboseレベル1でモジュールの実行結果は標準出力へprintされるので、開発中は基本的にこのレベルで作業してます。

    - name: exec command
      ansible.builtin.command: hostname

このtaskを-v無しで実行してもchangedしか出力されないですが、-vを付けると以下のようにcommandの結果が出力されます。

changed: [localhost] => changed=true 
  cmd:
  - hostname
  delta: '0:00:00.001589'
  end: '2021-04-06 18:58:07.337019'
  rc: 0
  start: '2021-04-06 18:58:07.335430'
  stderr: ''
  stderr_lines: <omitted>
  stdout: cloud-dev
  stdout_lines: <omitted>

[NetBox] 最新のfeatureブランチでDockerビルドしてコンテナとしてデプロイする

NetBoxの機能でまだリリースはされていないけどfeatureブランチには実装済みの新しい機能を試してみたい場合、feature版のコンテナイメージは用意されてないためOS上に素で構築する必要があるが、それよりはfeature版コンテナイメージを手元でビルドした方が楽なのでは?と思って試してみた。

なお、試してみるとコンテナ版のビルドスクリプトはfeature版には対応していないため、実装の差異がある場合は手直しが必要*1

以下は時期バージョンの2.11のリリースノート。

github.com

環境

[zaki@cloud-dev netbox-docker (release)]$ docker-compose --version
docker-compose version 1.28.6, build 5db8d86f
[zaki@cloud-dev netbox-docker (release)]$ docker --version
Docker version 20.10.5, build 55c4c88

準備

Docker Compose版のリポジトリcloneする。

github.com

$ git clone https://github.com/netbox-community/netbox-docker.git
$ cd netbox-docker/

NetBoxコンテナイメージをビルドするには、付属のbuild.shを使うと簡単にビルドできる。

イメージのbuild

ビルドスクリプトの使い方は--helpを指定して確認できる。

$ ./build.sh --help
▶️ ./build.sh --help
Usage: ./build.sh <branch> [--push|--push-only]

ブランチ名を引数に実行すれば、そのブランチのソースを使ってコンテナイメージがビルドされる。
デフォルトはビルドのみで、(試してないけど)--pushも付与すれば、おそらくDocker Hubへpushされる(未確認。デフォルトではnetboxcommunityリポジトリになるので個人環境では変更が必要かも)。

というわけで、NetBoxのfeatureブランチをビルドするには以下の通り。

$ ./build.sh feature

実行すると .netboxディレクトリにNetBox本体のリポジトリcloneされビルド処理が始まる。
ビルド中はステージごとの処理時間がログの右側に表示されるのでtimeは付けなくていいかも。手元の環境(i7のESXiの上の4vCPUs RAM8GBのVM)では約10分必要だった。
あとは、実行ユーザー権限でDockerが使用可能であること。

ビルドログはこんな感じ。

▶️ ./build.sh feature
🌐 Checking out 'feature' of NetBox from the url 'https://github.com/netbox-community/netbox.git' into '.netbox'
✅ Checked out NetBox
🏭 Building the following targets: main ldap
🏗 Building the target 'main'
🐳 Building the Docker image 'docker.io/netboxcommunity/netbox:feature'.
    Build reason set to: interactive
[+] Building 641.3s (22/22) FINISHED                                                                                                                          
 => [internal] load build definition from Dockerfile                                                                                                     0.0s
 => => transferring dockerfile: 4.03kB                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                        0.0s
 => => transferring context: 159B                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/alpine:3.13                                                                                           2.6s
 => [internal] load build context                                                                                                                        0.3s
 => => transferring context: 18.63MB                                                                                                                     0.3s
 => [builder 1/4] FROM docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                             0.8s
 => => resolve docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                                     0.0s
 => => sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c 1.47kB / 1.47kB                                                           0.0s
 => => sha256:ca3cd42a7c9525f6ce3d64c1a70982613a8235f0cc057ec9244052921853ef15 2.81MB / 2.81MB                                                           0.7s
 => => sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a 1.64kB / 1.64kB                                                           0.0s 
 => => sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310 528B / 528B                                                               0.0s 
 => => extracting sha256:ca3cd42a7c9525f6ce3d64c1a70982613a8235f0cc057ec9244052921853ef15                                                                0.1s 
 => [builder 2/4] RUN apk add --no-cache       bash       build-base       cargo       ca-certificates       cyrus-sasl-dev       graphviz       jpeg-  46.7s
 => [main  2/14] RUN apk add --no-cache       bash       ca-certificates       curl       graphviz       libevent       libffi       libjpeg-turbo      11.0s
 => [main  3/14] WORKDIR /opt                                                                                                                            0.6s
 => [builder 3/4] COPY .netbox/requirements.txt requirements-container.txt /                                                                             0.0s
 => [builder 4/4] RUN /opt/netbox/venv/bin/pip install       -r /requirements.txt       -r /requirements-container.txt                                 569.6s
 => [main  4/14] COPY --from=builder /opt/netbox/venv /opt/netbox/venv                                                                                   4.4s
 => [main  5/14] COPY .netbox /opt/netbox                                                                                                                0.4s
 => [main  6/14] COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py                                                          0.0s
 => [main  7/14] COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh                                                                       0.0s
 => [main  8/14] COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh                                                                               0.0s
 => [main  9/14] COPY startup_scripts/ /opt/netbox/startup_scripts/                                                                                      0.0s
 => [main 10/14] COPY initializers/ /opt/netbox/initializers/                                                                                            0.0s
 => [main 11/14] COPY configuration/ /etc/netbox/config/                                                                                                 0.0s
 => [main 12/14] COPY docker/nginx-unit.json /etc/unit/                                                                                                  0.0s
 => [main 13/14] WORKDIR /opt/netbox/netbox                                                                                                              0.1s
 => [main 14/14] RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/       && chmod -R g+w media /opt/unit/       && SECRET_KEY="dummy" /opt/netbox/ven  2.8s
 => exporting to image                                                                                                                                  11.5s
 => => exporting layers                                                                                                                                 11.5s
 => => writing image sha256:71d6db96e4e172dfae2d1b0c68aad0283212936e17f0bc22aef00004f7f2e756                                                             0.0s
 => => naming to docker.io/netboxcommunity/netbox:feature                                                                                                0.0s
✅ Finished building the Docker images 'docker.io/netboxcommunity/netbox:feature'
🔎 Inspecting labels on 'docker.io/netboxcommunity/netbox:feature'
{"BUILD_REASON":"interactive","NETBOX_GIT_BRANCH":"HEAD","NETBOX_GIT_REF":"4883bc3dd445a8353ae1f204d0a1fe3dcc83cb73","NETBOX_GIT_URL":"https://github.com/netbox-community/netbox.git","ORIGINAL_TAG":"docker.io/netboxcommunity/netbox:feature","org.label-schema.build-date":"2021-04-03T02:05+00:00","org.label-schema.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.label-schema.name":"NetBox Docker","org.label-schema.schema-version":"1.0","org.label-schema.url":"https://github.com/netbox-community/netbox-docker","org.label-schema.usage":"https://github.com/netbox-community/netbox-docker/wiki","org.label-schema.vcs-ref":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.label-schema.vcs-url":"https://github.com/netbox-community/netbox-docker.git","org.label-schema.vendor":"The netbox-docker contributors.","org.label-schema.version":"1.1.0","org.opencontainers.image.authors":"The netbox-docker contributors.","org.opencontainers.image.created":"2021-04-03T02:05+00:00","org.opencontainers.image.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.opencontainers.image.documentation":"https://github.com/netbox-community/netbox-docker/wiki","org.opencontainers.image.licenses":"Apache-2.0","org.opencontainers.image.revision":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.opencontainers.image.source":"https://github.com/netbox-community/netbox-docker.git","org.opencontainers.image.title":"NetBox Docker","org.opencontainers.image.url":"https://github.com/netbox-community/netbox-docker","org.opencontainers.image.vendor":"The netbox-docker contributors.","org.opencontainers.image.version":"1.1.0"}
🏗 Building the target 'ldap'
🐳 Building the Docker image 'docker.io/netboxcommunity/netbox:feature-ldap'.
    Build reason set to: interactive
[+] Building 5.1s (24/24) FINISHED                                                                                                                            
 => [internal] load build definition from Dockerfile                                                                                                     0.1s
 => => transferring dockerfile: 38B                                                                                                                      0.0s
 => [internal] load .dockerignore                                                                                                                        0.0s
 => => transferring context: 34B                                                                                                                         0.0s
 => [internal] load metadata for docker.io/library/alpine:3.13                                                                                           1.7s
 => [internal] load build context                                                                                                                        0.1s
 => => transferring context: 176.93kB                                                                                                                    0.1s
 => [builder 1/4] FROM docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                             0.0s
 => CACHED [main  2/14] RUN apk add --no-cache       bash       ca-certificates       curl       graphviz       libevent       libffi       libjpeg-tur  0.0s
 => CACHED [main  3/14] WORKDIR /opt                                                                                                                     0.0s
 => CACHED [builder 2/4] RUN apk add --no-cache       bash       build-base       cargo       ca-certificates       cyrus-sasl-dev       graphviz        0.0s
 => CACHED [builder 3/4] COPY .netbox/requirements.txt requirements-container.txt /                                                                      0.0s
 => CACHED [builder 4/4] RUN /opt/netbox/venv/bin/pip install       -r /requirements.txt       -r /requirements-container.txt                            0.0s
 => CACHED [main  4/14] COPY --from=builder /opt/netbox/venv /opt/netbox/venv                                                                            0.0s
 => CACHED [main  5/14] COPY .netbox /opt/netbox                                                                                                         0.0s
 => CACHED [main  6/14] COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py                                                   0.0s
 => CACHED [main  7/14] COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh                                                                0.0s
 => CACHED [main  8/14] COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh                                                                        0.0s
 => CACHED [main  9/14] COPY startup_scripts/ /opt/netbox/startup_scripts/                                                                               0.0s
 => CACHED [main 10/14] COPY initializers/ /opt/netbox/initializers/                                                                                     0.0s
 => CACHED [main 11/14] COPY configuration/ /etc/netbox/config/                                                                                          0.0s
 => CACHED [main 12/14] COPY docker/nginx-unit.json /etc/unit/                                                                                           0.0s
 => CACHED [main 13/14] WORKDIR /opt/netbox/netbox                                                                                                       0.0s
 => CACHED [main 14/14] RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/       && chmod -R g+w media /opt/unit/       && SECRET_KEY="dummy" /opt/net  0.0s
 => [ldap 1/2] RUN apk add --no-cache       libsasl       libldap       util-linux                                                                       1.6s
 => [ldap 2/2] COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py                                                                0.0s
 => exporting to image                                                                                                                                   1.7s
 => => exporting layers                                                                                                                                  1.7s
 => => writing image sha256:b6c008b1e41d1f122c051ebba982ed7de5c3f47f14ab9ddef154f1b693cc3e7e                                                             0.0s
 => => naming to docker.io/netboxcommunity/netbox:feature-ldap                                                                                           0.0s
✅ Finished building the Docker images 'docker.io/netboxcommunity/netbox:feature-ldap'
🔎 Inspecting labels on 'docker.io/netboxcommunity/netbox:feature-ldap'
{"BUILD_REASON":"interactive","NETBOX_GIT_BRANCH":"HEAD","NETBOX_GIT_REF":"4883bc3dd445a8353ae1f204d0a1fe3dcc83cb73","NETBOX_GIT_URL":"https://github.com/netbox-community/netbox.git","ORIGINAL_TAG":"docker.io/netboxcommunity/netbox:feature-ldap","org.label-schema.build-date":"2021-04-03T02:05+00:00","org.label-schema.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.label-schema.name":"NetBox Docker","org.label-schema.schema-version":"1.0","org.label-schema.url":"https://github.com/netbox-community/netbox-docker","org.label-schema.usage":"https://github.com/netbox-community/netbox-docker/wiki","org.label-schema.vcs-ref":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.label-schema.vcs-url":"https://github.com/netbox-community/netbox-docker.git","org.label-schema.vendor":"The netbox-docker contributors.","org.label-schema.version":"1.1.0","org.opencontainers.image.authors":"The netbox-docker contributors.","org.opencontainers.image.created":"2021-04-03T02:05+00:00","org.opencontainers.image.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.opencontainers.image.documentation":"https://github.com/netbox-community/netbox-docker/wiki","org.opencontainers.image.licenses":"Apache-2.0","org.opencontainers.image.revision":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.opencontainers.image.source":"https://github.com/netbox-community/netbox-docker.git","org.opencontainers.image.title":"NetBox Docker","org.opencontainers.image.url":"https://github.com/netbox-community/netbox-docker","org.opencontainers.image.vendor":"The netbox-docker contributors.","org.opencontainers.image.version":"1.1.0"}

この2イメージがビルドされた。

[zaki@cloud-dev netbox-docker (release)]$ docker images
REPOSITORY               TAG            IMAGE ID       CREATED         SIZE
netboxcommunity/netbox   feature-ldap   1e775790d163   2 minutes ago   404MB
netboxcommunity/netbox   feature        ef2733470d41   2 minutes ago   399MB

デプロイ

デプロイは通常通り、必要に応じて docker-compose.override.yml を作成する。
このとき、指定コンテナイメージのtagにfeatureを指定し、今ビルドしたイメージを使うように指定する。
(※これはdocker-compose.override.ymlで指定しなくても、環境変数VERSIONfeatureを指定しても良いようにdocker-compose.ymlが作成されている。)

version: '3.4'
services:
  netbox:
    ports:
      - 9900:8080
    image: netboxcommunity/netbox:feature

デプロイ開始。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose up -d
Creating network "netbox-docker_default" with the default driver
Creating volume "netbox-docker_netbox-media-files" with local driver
Creating volume "netbox-docker_netbox-postgres-data" with local driver
Creating volume "netbox-docker_netbox-redis-data" with local driver
Pulling postgres (postgres:12-alpine)...
12-alpine: Pulling from library/postgres
ca3cd42a7c95: Already exists
a0d003399a5b: Pull complete
0a08de1ad3ba: Pull complete
36fedf58ee26: Pull complete
733693f76814: Pull complete
14068c3cc9f8: Pull complete
0e1916273d9a: Pull complete
163f3880181d: Pull complete
Digest: sha256:56ccea5941111d7cfd22894c6ab7eb7dd9c0fff1ffa489194e87d4f5372160ec
Status: Downloaded newer image for postgres:12-alpine
Pulling redis (redis:6-alpine)...
6-alpine: Pulling from library/redis
ca3cd42a7c95: Already exists
8ea57f684bcd: Pull complete
c99865ad8d20: Pull complete
e2b874e1c121: Pull complete
4419ed1bbecc: Pull complete
662c8cef3675: Pull complete
Digest: sha256:142f857eb675a6a817dff1ae4d2138fc0228db596db2849ad1c38023a9dea7a4
Status: Downloaded newer image for redis:6-alpine
Creating netbox-docker_redis-cache_1 ... done
Creating netbox-docker_redis_1       ... done
Creating netbox-docker_postgres_1    ... done
Creating netbox-docker_netbox-worker_1 ... done
Creating netbox-docker_netbox_1        ... done

実行状態。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose ps
            Name                           Command               State     Ports  
----------------------------------------------------------------------------------
netbox-docker_netbox-worker_1   /opt/netbox/venv/bin/pytho ...   Up               
netbox-docker_netbox_1          /opt/netbox/docker-entrypo ...   Exit 1           
netbox-docker_postgres_1        docker-entrypoint.sh postgres    Up       5432/tcp
netbox-docker_redis-cache_1     docker-entrypoint.sh sh -c ...   Up       6379/tcp
netbox-docker_redis_1           docker-entrypoint.sh sh -c ...   Up       6379/tcp

おや、、、

実行エラー

ログを確認してみると以下の通り。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose logs 

:
:

netbox_1         | 🧬 loaded config '/etc/netbox/config/configuration.py'
netbox_1         | 🧬 loaded config '/etc/netbox/config/extra.py'
netbox_1         | 💡 Superuser Username: admin, E-Mail: admin@example.com
netbox_1         | 🧬 loaded config '/etc/netbox/config/configuration.py'
netbox_1         | 🧬 loaded config '/etc/netbox/config/extra.py'
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/000_users.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/010_groups.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/020_custom_fields.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/020_tags.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/030_regions.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/040_sites.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/050_manufacturers.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/060_device_types.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/070_rack_roles.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/075_rack_groups.py
netbox_1         | Traceback (most recent call last):
netbox_1         |   File "./manage.py", line 10, in <module>
netbox_1         |     execute_from_command_line(sys.argv)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
netbox_1         |     utility.execute()
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 413, in execute
netbox_1         |     self.fetch_command(subcommand).run_from_argv(self.argv)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/base.py", line 354, in run_from_argv
netbox_1         |     self.execute(*args, **cmd_options)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/base.py", line 398, in execute
netbox_1         |     output = self.handle(*args, **options)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/commands/shell.py", line 93, in handle
netbox_1         |     exec(sys.stdin.read(), globals())
netbox_1         |   File "<string>", line 1, in <module>
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 282, in run_path
netbox_1         |     return _run_code(code, mod_globals, init_globals,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
netbox_1         |     exec(code, run_globals)
netbox_1         |   File "../startup_scripts/__main__.py", line 27, in <module>
netbox_1         |     runpy.run_path(f.path)
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 265, in run_path
netbox_1         |     return _run_module_code(code, init_globals, run_name,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 97, in _run_module_code
netbox_1         |     _run_code(code, mod_globals, init_globals,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
netbox_1         |     exec(code, run_globals)
netbox_1         |   File "/opt/netbox/startup_scripts/075_rack_groups.py", line 3, in <module>
netbox_1         |     from dcim.models import RackGroup, Site
netbox_1         | ImportError: cannot import name 'RackGroup' from 'dcim.models' (/opt/netbox/netbox/dcim/models/__init__.py)
netbox-docker_netbox_1 exited with code 1

エラーになっている。

先に答えを書くと、NetBox本体はfeatureブランチを今回対象としているけど、イメージビルドに使っているNetBox Dockerはfeatureブランチは存在しないのでデフォルトのreleaseブランチを使っているので、実装の差異があるとそこでエラーが発生する。

NetBox Dockerのコードを本体のfeatureブランチの内容に合わせる

以下は2021.04.03 11時(JST)時点での修正ポイント。

最初はfeatureブランチの内容が実装されてないから実装されるまで待とうかなーと思ったけど、あきらめきれずにソースを追ってみる。
(最近こんなのばっかだな)

最初に見るエラーの内容は以下。

Running the startup script /opt/netbox/startup_scripts/075_rack_groups.py
:
:
ImportError: cannot import name 'RackGroup' from 'dcim.models' (/opt/netbox/netbox/dcim/models/__init__.py)

エラー発生個所のコードを確認すると、featureブランチはLocationになってる

masterやdevelopだとRackGroupになっている

そして、実行中スクリプト075_rack_groups.pyのあるnetbox-dockerのリリースブランチでは、RackGroupを参照してる、と。

ということで、startup_scripts以下で RackGroup を参照しているところを Location に変更すれば一歩進める(少なくともimportはうまくいく)と予測が立つ。
ただ、パッケージ名変更だけで動くのかというとそれはあやしいのでもうblameをチェックすると、以下のissueで「名前の変更」が行われたことが確認できる。

github.com

変更点は以下。

github.com

grepと脳内インタプリタでざっと見た感じでは、netbox-docker側のスクリプトもリネームでとりあえず大丈夫そうなので、startup_scripts内で参照しているRackGroupLocationに変更してやる。
以下の通り。

diff --git a/startup_scripts/075_rack_groups.py b/startup_scripts/075_rack_groups.py
index 3974b56..d2bf249 100644
--- a/startup_scripts/075_rack_groups.py
+++ b/startup_scripts/075_rack_groups.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import RackGroup, Site
+from dcim.models import Location, Site
 from startup_script_utils import load_yaml
 
 rack_groups = load_yaml("/opt/netbox/initializers/rack_groups.yml")
@@ -17,7 +17,7 @@ for params in rack_groups:
         query = {field: params.pop(assoc)}
         params[assoc] = model.objects.get(**query)
 
-    rack_group, created = RackGroup.objects.get_or_create(**params)
+    rack_group, created = Location.objects.get_or_create(**params)
 
     if created:
         print("🎨 Created rack group", rack_group.name)
diff --git a/startup_scripts/080_racks.py b/startup_scripts/080_racks.py
index 087b3f9..e27b268 100644
--- a/startup_scripts/080_racks.py
+++ b/startup_scripts/080_racks.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import Rack, RackGroup, RackRole, Site
+from dcim.models import Rack, Location, RackRole, Site
 from startup_script_utils import load_yaml, pop_custom_fields, set_custom_fields_values
 from tenancy.models import Tenant
 
@@ -14,7 +14,7 @@ required_assocs = {"site": (Site, "name")}
 optional_assocs = {
     "role": (RackRole, "name"),
     "tenant": (Tenant, "name"),
-    "group": (RackGroup, "name"),
+    "group": (Location, "name"),
 }
 
 for params in racks:
diff --git a/startup_scripts/330_power_panels.py b/startup_scripts/330_power_panels.py
index bfde18f..db11a4d 100644
--- a/startup_scripts/330_power_panels.py
+++ b/startup_scripts/330_power_panels.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import PowerPanel, RackGroup, Site
+from dcim.models import PowerPanel, Location, Site
 from startup_script_utils import load_yaml, pop_custom_fields, set_custom_fields_values
 
 power_panels = load_yaml("/opt/netbox/initializers/power_panels.yml")
@@ -10,7 +10,7 @@ if power_panels is None:
 
 required_assocs = {"site": (Site, "name")}
 
-optional_assocs = {"rack_group": (RackGroup, "name")}
+optional_assocs = {"rack_group": (Location, "name")}
 
 for params in power_panels:
     custom_field_data = pop_custom_fields(params)

再デプロイ

変更が済んだら一度down --volumeでボリューム含めて全て削除(これはトラブル防止のために環境をクリアにするという趣旨)し、再度デプロイする。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose down --volume 
Stopping netbox-docker_netbox-worker_1 ... done
Stopping netbox-docker_postgres_1      ... done
Stopping netbox-docker_redis-cache_1   ... done
Stopping netbox-docker_redis_1         ... done
Removing netbox-docker_netbox_1        ... done
Removing netbox-docker_netbox-worker_1 ... done
Removing netbox-docker_postgres_1      ... done
Removing netbox-docker_redis-cache_1   ... done
Removing netbox-docker_redis_1         ... done
Removing network netbox-docker_default
Removing volume netbox-docker_netbox-media-files
Removing volume netbox-docker_netbox-postgres-data
Removing volume netbox-docker_netbox-redis-data
[zaki@cloud-dev netbox-docker (release)]$ docker-compose up -d
Creating network "netbox-docker_default" with the default driver
Creating volume "netbox-docker_netbox-media-files" with local driver
Creating volume "netbox-docker_netbox-postgres-data" with local driver
Creating volume "netbox-docker_netbox-redis-data" with local driver
Creating netbox-docker_redis-cache_1   ... done
Creating netbox-docker_postgres_1      ... done
Creating netbox-docker_redis_1       ... done
Creating netbox-docker_netbox-worker_1 ... done
Creating netbox-docker_netbox_1        ... done
[zaki@cloud-dev netbox-docker (release)]$ docker-compose ps
            Name                           Command               State                       Ports                     
-----------------------------------------------------------------------------------------------------------------------
netbox-docker_netbox-worker_1   /opt/netbox/venv/bin/pytho ...   Up                                                    
netbox-docker_netbox_1          /opt/netbox/docker-entrypo ...   Up      0.0.0.0:9900->8080/tcp,0.0.0.0:49164->8080/tcp
netbox-docker_postgres_1        docker-entrypoint.sh postgres    Up      5432/tcp                                      
netbox-docker_redis-cache_1     docker-entrypoint.sh sh -c ...   Up      6379/tcp                                      
netbox-docker_redis_1           docker-entrypoint.sh sh -c ...   Up      6379/tcp           

ログを見た感じでも正常に起動した模様。

webアクセス

この通り、featureブランチで開発されている2.11の表示になっている。(リリース版は現在2.10)

f:id:zaki-hmkc:20210403114740p:plain

2.11のリリースノートはこちら

github.com

#5451 - Add support for multiple-selection custom fields

たとえばカスタムフィールドの「multi-select」とかがサポートされるようになっている。 画面右上Adminメニューの「Custom fields」で、以下のように項目が追加されているのを確認できる。

v2.11 f:id:zaki-hmkc:20210403115250p:plain

以下はv2.10の場合 f:id:zaki-hmkc:20210403115401p:plain

まとめ

featureブランチの内容でも手元でイメージビルドすればDocker Composeを使ってNetBoxをデプロイできることを確認できた。
ただし実装の内容(次期バージョンと現バージョンの実装の差異)によっては修正が必要。
なので、「構築は楽だけどこれでまぁ大丈夫かな?」くらいだろうか。。

今回は露見してないけど「全くの新しいパッケージimportが必要」なパターンであればデプロイのタイミングではエラーにならずに画面から参照しようとするとランタイムエラーになる、というパターンもあるかもしれないので注意。

用途としては「壊して作り直す」が容易なコンテナのメリットを活かせるので、新機能を確認するための環境の用意の手段の一つとして、やりかたをチェックしておくと良いと思う。

*1:あくまで2021.04.03時点の2.11バージョンの場合

NetBoxのSecretを使って暗号化された情報を登録・参照する

NetBoxは、デバイスVMの情報に暗号化されたSecret情報を持たせることができるので、例えばログイン用のアカウント情報などもNetBoxに登録しておくことができます。
このSecretはbase64エンコードされるだけのもの、、ではなく、256ビットのAES共通鍵で暗号化されて保存されます。(ドキュメントより)

The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key

netbox.readthedocs.io

鍵作成

鍵形式は以下。

Supported Key Format

Public key formats supported

  • PKCS#1 RSAPublicKey* (PEM header: BEGIN RSA PUBLIC KEY)
  • X.509 SubjectPublicKeyInfo** (PEM header: BEGIN PUBLIC KEY)
  • OpenSSH line format is not supported.

Private key formats supported (unencrypted) - PKCS#1 RSAPrivateKey* (PEM header: BEGIN RSA PRIVATE KEY) - PKCS#8 PrivateKeyInfo (PEM header: BEGIN PRIVATE KEY)

https://netbox.readthedocs.io/en/stable/core-functionality/secrets/#supported-key-format

PKCS#1形式のRSAキーペアを用意するのが簡単でopenssl genrsaで作れる、、、と思ったけど、NetBoxのUIでもキーペアが作成できます。

鍵の作成は画面一番右上ユーザーメニューの[Profile]から。

f:id:zaki-hmkc:20210402070824p:plain

User Profile画面のサイドメニューの[User Key]で鍵管理の画面になります。

f:id:zaki-hmkc:20210402071030p:plain

初回アクセス時はまだ登録された鍵情報がないため、[Create User Key]押下し、鍵の作成を行います。
別途作成したキーペアの公開鍵の内容をテキストフィールドに入力するか、[Generate a New Key Pair]押下してここでキーペアを作成します。
今回はここでキーペアを作成してみます。

f:id:zaki-hmkc:20210402071224p:plain

[Generate a New Key Pair]押下するとキーペアが作成され、その情報が表示されます。

f:id:zaki-hmkc:20210402071409p:plain

ここで表示された秘密鍵はこの画面を閉じると再表示はできない(NetBox上には残らない)ので、テキストの内容を手元にコピーし、netbox_private.keyなどのファイル名で保存しておきます。
保存したら[I have saved my new private key]押下。 すると元の鍵管理画面に戻って作成された公開鍵情報が表示された状態になるので[Save]を押下。

f:id:zaki-hmkc:20210402071738p:plain

これで鍵情報が登録されました。

Secretの暗号化は共通鍵で暗号化されると書かれてたのに何でRSA公開鍵作るのかというと、暗号化・復号に使うAES共通鍵を、ここで作ったRSAキーペアの公開鍵で暗号化したものがNetBox上に保存され、Secret情報の暗号化・復号にはRSAキーペアの秘密鍵で復号したAES共通鍵を使用するみたい。(ドキュメントを読む限り、そうなる)
webの画面で作成されたキーペアは見た感じ2048ビット長になる模様(NetBox v2.10.4時点)。

web画面でSecretの作成

Secret Roleの作成

Secretを作成するにはその準備としてSecret Roleを作成しておく必要があります。 (Secretの必須パラメタにSecret Roleがある)

Secret Roleの作成は、メニューの[Secrets] -> [Secret Roles]から。

f:id:zaki-hmkc:20210402070727p:plain

Secret Roleには、暗号化するSecretが何の情報なのかメタ情報的な名称を付けてやるとよさげ。 例えば「login credentials」など。

f:id:zaki-hmkc:20210402070711p:plain

Secretの作成

VM情報を開くとダッシュボードに「Secrets」のパートの[+ Add secret]を押下。

f:id:zaki-hmkc:20210402071948p:plain

Secretの作成画面になるので、Secret Roleを選択しデータを入力、[Create]押下。

f:id:zaki-hmkc:20210402072713p:plain

そうすると、現在のNetBoxのweb操作のためのセッション情報に秘密鍵が保持されてない場合、秘密鍵を要求されるので、ここにキーペア作成時に保存した秘密鍵のテキストデータを入力し[Request session key]を押下します。

f:id:zaki-hmkc:20210402072844p:plain

秘密鍵を入力するとセッション情報として保持されたという通知がブラウザのダイアログで表示されるので[OK]押下。

f:id:zaki-hmkc:20210402073223p:plain

で、これで秘密鍵を使用可能になったので、[Create]押下してSecretの登録を再開します。

f:id:zaki-hmkc:20210402073452p:plain

このとき、秘密鍵がセッション情報に登録されているので、[Unlock]を押下すると内容を確認できます。(内容はダミーですw)

f:id:zaki-hmkc:20210402073637p:plain

RESTでSecretの参照

RESTの仕様は例によってNetBoxのweb画面下部の「{} API」で見れるswaggerから。
secretsのセクションにエンドポイント一覧があるのでそこで確認できます。

登録したSecretの情報を取得するには、/secrets/secrets/{id}/を使用します。(IDはwebで見たときのURLで確認)

[zaki@cloud-dev netbox-key]$ curl -X GET http://192.168.0.19:28080/api/secrets/secrets/1/ -H "Accept: application/json; indent=4" -H "Authorization: Token ${NETBOX_TOKEN}"
{
    "id": 1,
    "url": "http://192.168.0.19:28080/api/secrets/secrets/1/",
    "assigned_object_type": "virtualization.virtualmachine",
    "assigned_object_id": 6,
    "assigned_object": {
        "id": 6,
        "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
        "name": "client-dev"
    },
    "role": {
        "id": 1,
        "url": "http://192.168.0.19:28080/api/secrets/secret-roles/1/",
        "name": "login credentials",
        "slug": "login-credentials"
    },
    "name": "zaki",
    "plaintext": null,
    "hash": "pbkdf2_sha256$1000$jA10MuGv10l3$K586baQvjZQCl10idEA0WbeuWwHqotrOuG0Oz/7pAzQ=",
    "tags": [],
    "custom_fields": {},
    "created": "2021-04-01",
    "last_updated": "2021-04-01T22:32:14.236537Z"
}

ただし、普通にやってもデータが入っているplaintextのところは(暗号化データを復号できずに)nullとなっている。
この値を取得するには、webの画面でセッションとして秘密鍵情報を登録したのと同じように、RESTで秘密鍵を送信してセッションキーを発行しておく必要があります。

[zaki@cloud-dev netbox-key]$ ls -F
gen_by_openssl/  netbox_register_private.key

カレントディレクトリに秘密鍵を保存したnetbox_register_private.keyファイルがある場合、--data-urlencode@でファイル指定して以下のRESTでセッションキーを取得します。

[zaki@cloud-dev netbox-key]$ curl -X POST -H "Authorization: Token ${NETBOX_TOKEN}" -H "Accept: application/json; indent=4" --data-urlencode "private_key@netbox_register_private.key" http://192.168.0.19:28080/api/secrets/get-session-key/
{
    "session_key": "8zJfic3TpR/WcJhDtuIWChZQrdkuJeIsfVI3JmDmqhc="
}

これでセッションキーを取得できたので、X-Session-Keyヘッダにこの値を追加して再度SecretのAPIを叩く。

[zaki@cloud-dev netbox-key]$ curl -X GET http://192.168.0.19:28080/api/secrets/secrets/1/ -H "Accept: application/json; indent=4" -H "Authorization: Token ${NETBOX_TOKEN}" -H "X-Session-Key: 8zJfic3TpR/WcJhDtuIWChZQrdkuJeIsfVI3JmDmqhc="
{
    "id": 1,
    "url": "http://192.168.0.19:28080/api/secrets/secrets/1/",
    "assigned_object_type": "virtualization.virtualmachine",
    "assigned_object_id": 6,
    "assigned_object": {
        "id": 6,
        "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
        "name": "client-dev"
    },
    "role": {
        "id": 1,
        "url": "http://192.168.0.19:28080/api/secrets/secret-roles/1/",
        "name": "login credentials",
        "slug": "login-credentials"
    },
    "name": "zaki",
    "plaintext": "curry_tabetai",
    "hash": "pbkdf2_sha256$1000$jA10MuGv10l3$K586baQvjZQCl10idEA0WbeuWwHqotrOuG0Oz/7pAzQ=",
    "tags": [],
    "custom_fields": {},
    "created": "2021-04-01",
    "last_updated": "2021-04-01T22:32:14.236537Z"
}

無事に"plaintext": "curry_tabetai" という値を取得できました。
ちなみにAnsibleのlookup pluginを使う場合はkey_fileに秘密鍵ファイルのパスを指定すれば、セッション処理は自動でやってくれるので楽です。(あとでまとめる)

curlのオプションはこの辺も参照。 qiita.com

環境

Docker版NetBox (v2.10.4)で確認。

[Ansible] そのtag設定、想定通りに動いてますか? (継承機能とその実装を確認する)

playbook内のtask定義にtagを設定しておくことで、指定tagのtaskのみ実行したり、逆に指定tagのtaskを除外してansible-playbookを実行することができます。
開発中のtaskのみピンポイントで実行したい場合や、逆に、共有のDBのデータを更新したりするtaskはほかのユーザーやチームと調整してからでないと実行が難しかったり、Blue-Greenデプロイメントの実装で環境Aの機能をオフにしてもう片方の環境Bをオンにするような処理だけど開発中は環境Bだけ確認したかったり、大量データのダウンロードや冪等の確認を伴い処理に時間がかかるため開発中は実行したくないなど特定のtaskは実行したくない場合に利用できます。

また、特殊tagとして、常に実行するalwaysと実行しないneverというtagが予約語として用意されています。
neverは特に「通常は実行したくない場合」にtaskに設定しておくと、簡単に除外設定できます。

ただし、tag設定はtaskの呼び出し元のtag設定も継承されるという特徴があります。

docs.ansible.com

tag設定をちゃんと設計・把握していないと、例えばrole単体で作成してて自前のplaybookから呼んでいるときは問題なかったけど、チームの他の人が作成したplaybookと結合したら想定してなかったtagが追加されて、期待しない動作をすることがあるので注意が必要です。

なお、本文中の「tagの設定」はplaybookに対するtagsディレクティブを使ったtagの使用を表し、「tagの指定」はansible-playbook実行時のオプション--tags--skip-tagsによるtagの使用を表します。(この記事内でのローカルルール)

また、Ansibleバージョンは2.10.5で確認しています。

TL;DR

本記事の内容は、importでtaskを読み込んだ場合の話です。

  • playbookのtag設定は、「tagの継承」によってそのtaskの呼び出し元定義のtag設定も有効
  • tag設定によってtaskが実行されるか評価されるのはtask単位
    • 呼び出し元のplayやroleのtag設定と除外指定だけで配下の全taskが除外されたりはしない
  • 動かしたくないtaskがある場合はnever設定だけに頼らず--skip-tagsを指定
    • --tags neverのみ指定するとnevertag設定されたtaskは動作する
    • nevertagと他のtagを複数設定されたtaskは、他に設定されているtagを--tagsで指定されると動作する
    • --skip-tags nevernevertagが設定されたtaskは他の有効なtagがあっても動かない
  • ansible-playbook実行時は、まず--tagsで処理対象taskかチェックされてから--skip-tagsで除外対象かをチェックされる
    • --skip-tagsの指定が優先される
    • --skip-tags alwaysalwaystag設定されたtaskは動かない
  • 実行前にansible-playbook --list-tasksで処理対象taskとそのtag設定を確認しよう

tag設定の継承例

playbook定義

tag設定の継承のされかたは、ちょっとわざとらしい上に一部無意味だけど以下のplaybookを例で説明します。

# playbook
---
- hosts: localhost
  gather_facts: false
  tags: __play

  tasks:
    - import_role:
        name: other
      tags: __other
    - name: sample task
      debug:
        msg: "sample tasks"

  roles:
    - role: sample
      tags: __role1

    - role: other
      tags: __role2
  • playに__playを設定
  • playはtasksにtaskを2つ定義
    • task1は__othertagを設定してotherroleを設定
    • task2はtag設定無し
  • playはrolesも使ってroleを2つ定義
    • role1は__role1tagを設定してsampleroleを設定
    • role2は__role2tagを設定して(taskと同じ)otherroleを設定

tasksrolesを同時指定した場合の処理順はroles->tasks

docs.ansible.com

role定義

上記playbookが参照している一つ目のsampleroleは以下の通り。

# roles/sample/tasks/main.yml
---
- name: task1
  debug:
    msg: use __tag1
  tags: __tag1

- name: task2
  debug:
    msg: use __tag2 and never
  tags:
    - __tag2
    - never

- name: task3
  debug:
    msg: use never
  tags:
    - never

もう一つのotherroleは以下の通り。

# roles/other/tasks/main.yml
---
- name: other
  debug:
    msg: "other"
  tags: __other_task
  
- name: import
  import_tasks: other_sub.yml

そしてさらにotherroleでは、import_tasksを使って別のtaskファイルを読んでいる。内容は以下の通り。

# roles/other/tasks/other_sub.yml
---
- name: other_sub
  debug:
    msg: other_sub

構造を整理

上記のplaybookのplay/task/roleの呼び出し構成とtagの設定場所の概要は以下の通り。
playやtaskのように要素に対して設定されてるものはブロックの中へ、rolesimport_roleimport_tasksのように別定義の呼び出し時に設定されるものは矢印にtagを記載しています。

f:id:zaki-hmkc:20210324093715p:plain

ここで、playbookのファイル上でtaskに設定されてるだけのtagを整理すると以下の通り。

No task tags
(1) sample task N/A
(2) other __other_task
(3) other_sub N/A
(4) task1 __tag1
(5) task2 __tag2, never
(6) task3 never

しかし、実際は「tagの継承機能」が働くため、ansible-playbook実行時のtaskごとのtag設定としては以下の通りになります。(順不同)

No task tags
(1) sample task __play_tag
(2)-a other __other_task, __other, __play_tag
(2)-b other __other_task, __parent_role_tag2, __play_tag
(3)-a other_sub __other, __play_tag
(3)-b other_sub __parent_role_tag2, __play_tag
(4) task1 __tag1, __parent_role_tag1, __play_tag
(5) task2 __tag2, never, __parent_role_tag1, __play_tag
(6) task3 never, __parent_role_tag1, __play_tag

ここまで来るとマトリクス表の方がわかりやすいかもなのではみ出るけど併記。(はてなブログのスタイルなんか良いのないかなー)

No task __play __other __role1 __role2 __other_task __tag1 __tag2 never
(1) sample task
(2)-a other
(2)-b other
(3)-a other_sub
(3)-b other_sub
(4) task1
(5) task2
(6) task3

(2)のotherと(3)のother_subは、playで定義されたtasksから呼ばれるパターンと、rolesから呼ばれるパターンで、上位の定義が異なるため、同じファイルでも継承されるtagの情報が異なります。
(tasksからimport_roleで呼ばれるときは__otherが設定され、rolesから呼ばれるときは_role2が設定される)

呼び出し元の判定だけでは終了しない

大抵のプログラミング言語には制御構文ifを使った条件分岐を実装できます。
以下のようなコードを記述すると、条件Aに合致しなかった場合はcode-a以下入れ子になっている条件Bifは評価されずに処理は進みます。

if (条件A) {
  // code-a
  if (条件B) {
    // code-b
    if (条件C) {
      // code-c
    }
  }
}

それに対してAnsibleのplaybookにおけるroleやimport_tasksなどのimport系の外部taskの呼び出しは、呼び出し元のtag条件に合致しなくても、一旦すべてのtaskの内容をロードし、継承によるtag設定も含めて全tagを評価した上で、そのtaskが処理対象か否かを判定します。

前述の例で、例えばsamplerole内に定義されたtask1(No 4)を見てみると、呼び出し階層は以下のようになります。

f:id:zaki-hmkc:20210324094012p:plain

コマンドラインオプションの--tags指定の特徴として、「指定されたtagがplaybookで設定されているtaskを実行する。tagが設定されなければ除外」という動きになりますが、--tags __tag1を指定してansible-playbookを実行する場合、「プログラミングの制御構文ifのように呼び出し元のplayのtags: __playの時点でtagの条件に合致しないため処理を終了する」という動きにはなりません

オプションは後述しますが、--tags__tag1を指定して--list-tasksを使ってtaskとtagの一覧を出力すると、以下の通りtask1が処理対象となることが確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __tag1

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]

(追記) importとinclude

なおこの動作は、import系を使った静的なtaskの読み込みの場合になります。実行時に動的にtask読み込みを行うincludeは、事前の読み込み処理の時点ではtaskを読み込まないため、include_* を行うtask自体がtag条件に合致しなかった場合、対象の(もしimportだったら処理対象だったとしても)taskは読み込まれないため処理対象になりません。逆に、include_* 自体がtag条件に合致して外部のtask情報を読み込んでも、読み込んだtaskへtag情報は継承されません

本記事も基本的にすべてimport系のtask読み込みを前提とした説明になります。(task読み込みの前処理段階でincludeの場合はtaskを読み込まないため)

Comparing includes and imports: dynamic and static re-use

docs.ansible.com

taskとtagを確認するオプション

1つのplaybookで完結しているなど小規模で目視できるならともかく、この例のように呼び出し階層も複数あると、tagsで文字列検索しても構造を把握するのは正直難しいです。
ですが、ansible-playbookコマンドには対象playbookのtaskとtagの構造をリストアップする便利なオプションがあります。

--list-tags: 実行予定taskで使用される全tagを出力

--list-tagsを指定します。
これを使うと「実行対象のtaskに設定されている全てのtag」をまとめて出力できます。
neverなどによって対象外となっているtaskについては表示されません。

※ 正確には「実行されるかどうか評価の対象となるtask」であり、例えばwhenの評価結果で実行されない場合のtaskでも出力の対象となります。
事前処理としてplaybookの構文解析を行い処理対象となるtask一覧を静的にリストアップできた内容になります。

$ ansible-playbook -i localhost, playbook.yml --list-tags

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
      TASK TAGS: [__other, __other_task, __play, __role1, __role2, __tag1]

もしnever設定のtaskを処理するつもりがないのにneverが含まれている場合は注意してください。

--list-tasks: 実行予定の全taskとそれぞれのtagを出力

実行されるtaskとそのtag設定は、ansible-playbookのオプション--list-tasksで確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]
      other : other     TAGS: [__other_task, __play, __role2]
      other : other_sub TAGS: [__play, __role2]
      other : other     TAGS: [__other, __other_task, __play]
      other : other_sub TAGS: [__other, __play]
      sample task       TAGS: [__play]

これらのオプションは、--tag--skip-tagでtag指定した場合は、tag設定によって実行対象となるtaskのみが表示されます。
--checkのさらに1段階前の実行対象のtaskが何になるかを確認するのに利用すると良いです。

例えば--tagを指定すると以下の通り、指定されたtagが設定されている特定taskのみ実行対象になることを確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role2

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      other : other     TAGS: [__other_task, __play, __role2]
      other : other_sub TAGS: [__play, __role2]

neverが設定されたtaskが実行される条件

上記の実行例ではneverが設定されたtaskはリストアップの対象になりません。

--tagsが未指定(あるいはデフォルトのall指定)の場合は、never設定のtaskは処理対象となりません。 neverが設定されたtaskを実行対象にするには以下の通り。

  • --tags neverを指定する
  • taskにnever以外にもtagを設定しておき、そのtagを--tagsで指定する
  • neveralwaysをtaskに同時設定する (--tags指定は不要)

よってnevertagを設定して通常実行されないように設定されているtaskでも、--tagsで明示的に指定されたり、併記・あるいは継承されたtagを--tagsで指定しても実行されます。
言い換えると、taskに設定されたneverを含むtagのうち、どれか一つでも--tagsによって対象と判定されれば(never設定されていても)処理対象となります。

以下のように--tags __role1を指定すると、__role1tagの設定を上位で行ったsampleroleはneverが設定されていても全て実行対象となります。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]
      sample : task2    TAGS: [__play, __role1, __tag2, never]
      sample : task3    TAGS: [__play, __role1, never]

ただしこれは--tagsのみが指定された場合。
--skip-tagsの指定がある場合は、--tagsの判定後に除外判定が行われます。

never設定されたtaskを確実に動かしたくない場合は、--skip-tags neverを指定することで、「taskに設定されたneverを含むtagのうちどれか一つでも--tagsで処理対象と判定」された結果を、除外対象に上書きできます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1 --skip-tags never
playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]

実装の調査

というわけで長い前置きでしたが、ここからさらに長くなるおまけコンテンツ(本題)です。
playbookからどうやって設定されているtag情報を読み取って、継承処理を行っているか、その実装を確認してみます。

この辺りはとくにドキュメントで規定されているわけでは(たぶん)ないので、少なくとも現在のAnsible 2.10.5の場合のアーキテクチャの話になります。
今後このあたりの(仕様は変わらなくても)実装が変更されるメジャーバージョンアップなどがあれば話は変わってくるかもしれません。

リスト出力部分

まずは--list-tasks--list-tagsが指定された場合の「taskとtag情報を保持しつつ実際には処理せずにその情報のみprint」がどうなってるか確認してみます。

リスト表示のみの実行

--list-tasks--list-tagsが指定された場合の出力箇所はPlaybookCLIのrun()以下。
results変数にplayの情報がセットされており、ループで逐次内容をprintしています。

                    msg = "\n  play #%d (%s): %s" % (idx + 1, ','.join(play.hosts), play.name)
                    mytags = set(play.tags)
                    msg += '\tTAGS: [%s]' % (','.join(mytags))
                                    if context.CLIARGS['listtasks']:
                                        cur_tags = list(mytags.union(set(task.tags)))
                                        cur_tags.sort()
                                        if task.name:
                                            taskmsg += "      %s" % task.get_name()
                                        else:
                                            taskmsg += "      %s" % task.action
                                        taskmsg += "\tTAGS: [%s]\n" % ', '.join(cur_tags)

ここでtask情報のprintを行いreturnするとこのままreturn 0でプログラムは終了するので、リスト表示指定を行わず通常実行の場合では、results変数を取得する前段の以下の時点でターゲットノードに対する各処理は完了していることになります。

        results = pbex.run()

通常実行

前述のリスト表示の場合results変数にはplayの情報がセットされていました。
それに対して--list-tasksなどのリスト表示指定をせずに普通に実行すると、run()の戻り値はplayの実行結果(正常なら0、異常なら非0のint)となります。

そのため、リスト表示するかどうか判定するif isinstance(results, list)で処理対象外となり、そのelseでその戻り値をreturnして終了します。

つまり、--list-tasksなど指定してtask一覧を表示する場合、実際に処理を実行するrun()メソッドはコールされてから(処理が諸々スキップされたのちに)、Ansible実行の最後に出力する実装になっています。

処理分岐の実装箇所

pbex.run()のコールは以下で、前述の通り--list-tagsなどの指定による情報表示の前に行われます。

        # create the playbook executor, which manages running the plays via a task queue manager
        pbex = PlaybookExecutor(playbooks=context.CLIARGS['args'], inventory=inventory,
                                variable_manager=variable_manager, loader=loader,
                                passwords=passwords)

        results = pbex.run()

コールされているPlaybookExecutor()の実装は以下で、--list-tasksなどの指定が行われている場合、コンストラクタの時点TaskQueueManagerの初期化を行わずに、このオブジェクトをNoneにセットしています。

        if context.CLIARGS.get('listhosts') or context.CLIARGS.get('listtasks') or \
                context.CLIARGS.get('listtags') or context.CLIARGS.get('syntax'):
            self._tqm = None

この後の実処理(run())では、このself._tqmの値がNoneであればリストアップのみとして処理されていきます。

playbookの実行時の処理

playbookファイルのリストを1個ずつ処理していく処理の開始場所は以下の部分。

            for playbook_path in self._playbooks:
                pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)

ただし、リスト表示のみの動作の場合は、以下の部分になります。

                if self._tqm is None:  # we are doing a listing
                    entry = {'playbook': playbook_path}
                    entry['plays'] = []

entryという辞書に、playbookplaysというキーを持つ変数をセットしています。
この辞書に変数が追加されるのは上記と、以下のもう1か所
これはplayごとの処理のループ内で、対象playの情報を追加しています。
(このelse節で実処理が走るため、リスト表示時はtaskの実処理が行われない)

                    if self._tqm is None:
                        # we are just doing a listing
                        entry['plays'].append(play)

play毎の処理のループを抜けたら以下の部分で配列entrylistentryを追加しています。

                if entry:
                    entrylist.append(entry)  # per playbook

そしてplaybook毎のループの処理が完了したら、リスト表示時は処理完了。play情報の一覧になっているentrylistreturnします
(リスト表示ではなく実処理の場合はこのentrylistは空のままになっているため、ここではreturnされずに処理続行し、最終的にはtask実行が成功した0かエラー時の非0がreturnされるため型が異なる)

            if entrylist:
                return entrylist

よって呼び出し元であるPlaybookCLI#run()は、このreturn値がlist形式か否かで「処理済み」か「処理予定の情報を収集しただけ」かを、isinstance()で判断できます。

tag情報は、playのtagはここで取り出しているplay.tagsで参照しています。

                    mytags = set(play.tags)
                    msg += '\tTAGS: [%s]' % (','.join(mytags))

同じようにtaskのtagは以下
(list tasksと list tagsで表示形式が異なるので取得方法も若干異なる)

                                    all_tags.update(task.tags)
                                    if context.CLIARGS['listtasks']:
                                        cur_tags = list(mytags.union(set(task.tags)))

tag情報取得箇所

tag情報のprint箇所はわかったので、playbookで設定されているtagをどこから読み取ってるのか調べてみます。
そうすると、そもそもtagの前にどうやってplayやtaskの情報をロードしているのか、という話になるわけで、そこから追ってみます。

playbookをソースからロード

エントリポイントは、playbook_executor.pyの前述の以下の部分でplaybookの情報をロード。

                pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)

ここからコールされるのは、以下のplaybook/__init__.pyで実装してあるload()スタティックメソッド。

    @staticmethod
    def load(file_name, variable_manager=None, loader=None):
        pb = Playbook(loader=loader)
        pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager)
        return pb

ここでPlaybookインスタンスを作成し_load_playbook_data()メソッドをコール、そこからplaybookの内容をロードしているのは以下の箇所。

        try:
            ds = self._loader.load_from_file(os.path.basename(file_name))
        except UnicodeDecodeError as e:
            raise AnsibleParserError("Could not read playbook (%s) due to encoding issues: %s" % (file_name, to_native(e)))

ここでコールしているのは、呼び出し元で指定しているloaderインスタンスload_from_file()メソッド。
これが何かと言うと、playbook_executor.pyのPlaybook.load()コール時の引数のloader=self._loaderの部分
さらにこれはPlaybookExecutorインスタンス作成時の引数で指定されています。

    def __init__(self, playbooks, inventory, variable_manager, loader, passwords):
        self._playbooks = playbooks
        self._inventory = inventory
        self._variable_manager = variable_manager
        self._loader = loader

じゃあさらに呼び出し元のcli/playbook.pyを見ると、laoderインスタンスは_play_prereqs()で取得しています。

        loader, inventory, variable_manager = self._play_prereqs()

_play_prereqs()の実装はここ
この中でloaderDataLoader()をコールして取得しています。

    def _play_prereqs():
        options = context.CLIARGS

        # all needs loader
        loader = DataLoader()

そしてこのDataLoader()はこちら

斜め読みする限り、Ansibleとしての処理ではなくYAMLを読んでいます。
上位からコールされるメソッドのload_from_file()を追っていくと、self.load() -> from_yaml() -> json.loads() と呼ばれて、Pythonの標準パッケージのjsonで処理されています。
(※ YAML形式のplaybookもjsonで処理している)

なので、この時点ではYAMLとして文法が合っていれば、例えば存在しないモジュールや未定義のディレクティブを書いてたりしても読み込み処理までは動作します。
逆に、YAMLとして文法誤りがあれば、例えばインデント誤りなどあればこの時点でエラーになります。
ただし、Ansibleとしては処理されていないため、rolesimport_playbookのようにAnsibleとしての外部ファイル読み込みもまだ処理されないため、外部ファイルに文法誤りがあってもここではエラー判定は発生しません。

そのままplaybookの処理に戻って内容を追っていくと、呼び出し元で使えるようにplay情報としてリストへ追加しているのはPlaybookクラスの_load_playbook_data()メソッド内下記の2か所で行われます。

                pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
                if pb is not None:
                    self._entries.extend(pb._entries)
                entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader, vars=vars)
                self._entries.append(entry_obj)

前者はimport_playbookでplaybookをさらに読み込んでいる場合で、最終的にplaybook内に記述されているplayの読み込みは後者のPlay.load()で、ここで読み取った一つのplay分のYAML情報(entry)をentry_objとして取得し、self._entriesリストに追加されていきます。

playのロード

Play#load()は以下のスタティックメソッドで、Playインスタンスを生成し、load_data()をコールしています。

        p = Play()
        if vars:
            p.vars = vars.copy()
        return p.load_data(data, variable_manager=variable_manager, loader=loader)

load_data()の実装はplaybook/base.pyFieldAttributeBaseクラス

play以下全taskのロード

load_data()メソッドの下記コードで、全てのplayの情報が再帰的に処理され読み込まれます。

        for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)):
            # copy the value over unless a _load_field method is defined
            target_name = name
            if name in self._alias_attrs:
                target_name = self._alias_attrs[name]
            if name in ds:
                method = getattr(self, '_load_%s' % name, None)
                if method:
                    self._attributes[target_name] = method(name, ds[name])
                else:
                    self._attributes[target_name] = ds[name]

nameにはtasksroles・そしてtagsなどの属性値が順番にセットされ、_load_tasks()_load_roles()_load_tasks()というメソッドが定義されていればそれがコールされ、その戻り値をself._attributesにセットしていきます。
この処理で、taskやroleあるいやimport系の定義による子要素も読み込まれていきます。

よって、この時点で保持されるtag情報は、まだ該当のtaskに設定されているもののみがセットされます。
(呼び出し元のtag情報は継承されていない)

そして実は、全playのデータロードが完了しても、保持されているデータ構造はこのままで、taskに紐づくtag情報はそのtaskに設定されたtagのみとなっています。
(呼び出し元のtag情報をマージした情報が保持される構成にはならない)

継承の仕組み

前述の通り、読み込まれた全taskの情報はtask単位でオブジェクトとして保持されます。
そしてこの個々のtaskオブジェクトは、そのtaskが持つ使用モジュールやwhenuntilなどの条件、registerなど、各ディレクティブ設定の情報によって構成されています。 (前述の通りself._attributes[key]で参照できる)

設定が継承される動作は、これらの値を参照するタイミングで親情報と結合された内容となる処理によって実装されています。

taskのtag情報などは前述の通りself._attributes['tags']に保持されていますが、実際のコードは.tagsというプロパティ値でアクセスしています。
参照箇所は例えば、--list-tasksで実行した際には以下の通り。

# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L143
                    mytags = set(play.tags)
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L171
                                        cur_tags = list(mytags.union(set(task.tags)))

この.tagsというプロパティアクセスは、Python組み込み関数property()を使ったメンバアクセス関数設定とfunctools#partial()を使った引数固定の関数指定を使用し、getterのメソッドとして_generic_g_parent()をコールするように、tagsに限らず各属性に対する定義をメタクラスで行っています

                    method = "_get_attr_%s" % attr_name
                    if method in src_dict or method in dst_dict:
                        getter = partial(_generic_g_method, attr_name)
                    elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit:
                        getter = partial(_generic_g_parent, attr_name)
                    else:
                        getter = partial(_generic_g, attr_name)

                    setter = partial(_generic_s, attr_name)
                    deleter = partial(_generic_d, attr_name)

                    dst_dict[attr_name] = property(getter, setter, deleter)

このアクセサメソッド設定を、Taggableクラスで定義されている以下の_tagsという名前のプロパティ名を使用し、

class Taggable:

    untagged = frozenset(['untagged'])
    _tags = FieldAttribute(isa='list', default=list, listof=(string_types, int), extend=True)

メタクラスの以下のコード部分tagsという先頭の_を除いた名前でプロパティを動的に設定しています。

            keys = list(src_dict.keys())
            for attr_name in keys:
                value = src_dict[attr_name]
                if isinstance(value, Attribute):
                    if attr_name.startswith('_'):
                        attr_name = attr_name[1:]

これでattr_nameには_tagsの先頭_が取り除かれtagsがセットされ、前述の_generic_g_parent+partial+propertyの設定をdst_dict[attr_name]にセットすることで、.tagsというプロパティアクセスを実装しています。

value.inherit:についてはAtrributeクラスのデフォルト値Trueとなるように実装されます。
※ なので、ここを改造してFalseにセットして動作させると、tagの継承は発生しません。

    def __init__(
        self,
        isa=None,
        private=False,
        default=None,
        required=False,
        listof=None,
        priority=0,
        class_type=None,
        always_post_validate=False,
        inherit=True,
        alias=None,
        extend=False,
        prepend=False,
        static=False,
    ):

以上の実装によって、.tagsプロパティに参照すると、_generic_g_parent(prop_name=tags, self)がコールされるようになります。

_generic_g_parent()の内容についてはメタクラスと同じbase.pyにて実装されています。

def _generic_g_parent(prop_name, self):
    try:
        if self._squashed or self._finalized:
            value = self._attributes[prop_name]
        else:
            try:
                value = self._get_parent_attribute(prop_name)
            except AttributeError:
                value = self._attributes[prop_name]

        # _snip_

(条件によって異なりますが基本的には)_get_parent_attribute()をコールして戻り値を取り出しています。
ではこのメソッドがどこにあるかと言うと、これは呼び出し元(self)が実装しているもので、以下の2クラスで現状は実装されています。

Blockクラス側はRoleの場合の実装の違いなどはありますが、「親情報(_parent)がある場合は、_get_parent_attribute()再帰的に呼び出す」という処理によって、そのtaskが持つ呼び出し元の設定も含めた情報が得られるようになっています。

playbookにおけるtag設定が呼び出し元の設定も継承するのはこの仕組みによって実装されています。

実行対象のtagかどうかを評価する箇所

ansible-playbookの実行時、playbookにおけるtagsの設定に加え、コマンドラインオプションの--tagsおよび--skip-tagsの指定によって、対象taskが実行されるかどうかが決定します。

指定したオプションは以下のPlay#__init__の実装の通り、only_tags--tags指定のもの、skip_tags--skip_tags指定のものとして処理されます。
そして、--tags指定を行わなかった場合は、自動的にallが指定されます。

        self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',))
        self.skip_tags = set(context.CLIARGS.get('skip_tags', []))

判定のロジックは以下のTaggable#evaluate_tags()の通りで、まず--tagsによって処理対象かどうかが決定し、その後に--skip-tagsで除外設定かを判定します。(--skip-tagsの指定が優先される)

        should_run = True  # default, tasks to run

        if only_tags:
            if 'always' in tags:
                should_run = True
            elif ('all' in only_tags and 'never' not in tags):
                should_run = True
            elif not tags.isdisjoint(only_tags):
                should_run = True
            elif 'tagged' in only_tags and tags != self.untagged and 'never' not in tags:
                should_run = True
            else:
                should_run = False

        if should_run and skip_tags:

            # Check for tags that we need to skip
            if 'all' in skip_tags:
                if 'always' not in tags or 'always' in skip_tags:
                    should_run = False
            elif not tags.isdisjoint(skip_tags):
                should_run = False
            elif 'tagged' in skip_tags and tags != self.untagged:
                should_run = False

        return should_run

大原則としては以下の通り。

  • --tagsで指定されなくても動かしたければalwaysを設定 (ただし--skip-tags指定で上書きされる)
  • tagが設定されたtaskを動かしたければ--tagsで指定 (ただし--skip-tags指定で上書きされる)
  • tagが設定されたtaskを動かしたくなければ--skip-tagsで指定

気を付けるべきは、指定tag以外のtagが設定されたtaskや、--tags--skip-tagsで指定しなかったtagのtaskがどうなるか。 単純に「nevertagを設定しているから動かないだろう」「--tagsで指定してないから動かないだろう」は継承によって認識外のtagが付与される可能性もあるので危険。

tag以外 (クイズ)

例えばwhenによる条件も継承されます。

playbook.yml

---
- hosts: localhost
  gather_facts: false
  vars:
    cond1: true
    cond2: true

  roles:
    - role: sample
      when:
        - cond1 is defined and cond1|bool
        - cond2 is defined and cond2|bool
    - other

roles/sample/tasks/main.yml

---
- name: roled task 1
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

- name: set cond2 to false
  set_fact:
    cond2: 0

- name: roled task 2
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

- name: set cond2 to true
  set_fact:
    cond2: 1

- name: roled task 3
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

どうでしょう、5つのうちどのtaskが動作するかわかりますか?

正解は以下の通り。

$ ansible-playbook -i localhost, playbook.yml 

PLAY [localhost] *****************************************************************************************************************

TASK [sample : roled task 1] *****************************************************************************************************
ok: [localhost] => 
  msg: cond1 True , cond2 True

TASK [sample : set cond2 to false] ***********************************************************************************************
ok: [localhost]

TASK [sample : roled task 2] *****************************************************************************************************
skipping: [localhost]

TASK [sample : set cond2 to true] ************************************************************************************************
skipping: [localhost]

TASK [sample : roled task 3] *****************************************************************************************************
skipping: [localhost]

PLAY RECAP ***********************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0   

docs.ansible.com

まとまった情報が見当たらないのですが、多くのディレクティブ/playbookキーワードは継承されます。
例えばこのあたりの指定は呼び出し先のtaskに継承されます。

以下のtask.pyloop_controlのアクセサメソッドの実装のように、inherit=Falseというパラメタを指定しているものは機能は継承されませんが、前述のリストのようにそうでないものは機能が継承される実装になっているようです。

    _loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)

まとめ

  • playbookにおけるtagの設定は、そのtaskに設定されてるものが全てではないので気を付けましょう。
  • --list-tasks--list-tagsを活用しましょう。

「Kubernetes Novice Tokyo #9」でkindをPodman環境で使う内容でLT登壇した振り返り

Kubernetes Novice Tokyo #9」で「kind on Podman」というタイトルでLTしたので、内容について振り返ります。

k8s-novice-jp.connpass.com

発表資料

speakerdeck.com

内容

Kubernetesのノードとして動作するDockerコンテナを使うことでDocker上でKubernetesクラスタを作成できるkindというツールを、DockerではなくPodmanで動かす、という内容を紹介しました。

LTタイトルは、、、ちょっと端的過ぎたかもしれない。。(反省点)
「DockerではなくPodmanでkindを~」とかの方がわかりやすかったですね。

ベースの情報はLT内でも何度か触れたContainer Runtime Meetup #3のPodmanに関する内容になっていて、その後に割とすぐ手元の環境でもPodman環境でkindを動かす、というのは確認してたりします。

zaki-hmkc.hatenablog.com

以前書いたブログは、Podmanで動かす手順に終始してましたが、Noviceということもあって、「なんでPodman使ったの?」という背景の説明(CentOS開発終了→RHELが使いやすくなった→RHELだとDockerよりPodmanの方が使いやすい)も混ぜることにしました。

登壇タイミングと準備

Container Runtime Meetup #3が1月末だったので、本当は2月のKubernetes Novice Tokyo #8で話せれば(CentOS開発終了のニュースからもさほど時間経ってなくて)タイムリーだったのですが、準備の時間がちょっとなかったので別の機会で…と思ってました。ですが、3月のNoviceの募集が始まったタイミング(2月下旬)も微妙に忙しく、このネタはもう流そうと思ってましたw
そう思ってたところ、LTの抽選締め切りの3月5日直前あたりからちょっと時間取れるようになって、「あれ?週末準備すれば行けるんじゃね?」と思い、抽選前日に枠も1個空いていたので申し込みました。

なので、ネタとしては以前検証した内容だったけど、発表としては割と突貫で準備がんばりました。

とはいえ、LTの時間が5分だと絞り切れなかったと思うので、10分枠だったのは幸いでした。
(といいつつ、10分枠でも微妙にオーバーしそうな感じでしたが)

LTで入らなかったデモ

10分もあるのでデモできるだろーと最初は考えてましたが、無理でした。
kindクラスタの作成はLTの時間内で余裕で完了するので、LTの最初の方でクラスタ作成を行ってスライドの説明を進めつつ頃合いを見計らってターミナルに戻る、というヤツをやってみたかったのですが、コマンドの入出力だけじゃなくて環境や前提など最低限の説明を入れると思ったより時間使ってしまいますね。

5分LTでデモやってる方、スゴイです。。

副産物

(以下、kind version 0.10.0の場合)

rootlessモードの説明をする際に、なんだかんだでDockerのrootlessは試したことが無かったのでいい機会になりました。

zaki-hmkc.hatenablog.com

rootlessのDockerでもkind試しましたが、やっぱり「rootlessコンテナ環境でもsudoが必要」を確認できました。

ちなみに動作は微妙に異なり、rootlessのDocker環境でsudoを使わずにユーザー権限でクラスタ作成しようとすると以下の通りで、「ノードのコンテナをデプロイしたあとのクラスタ作成処理でエラー」となり、rootless実行時のガードは特に入っていない模様。
実際にコンテナをデプロイしてからエラーになるので、失敗まで時間がかかります。

zaki@ubuntu-node:~/local/kind$ ./kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.20.2) 🖼 
 ✓ Preparing nodes 📦  
 ✗ Writing configuration 📜 
ERROR: failed to create cluster: failed to generate kubeadm config content: failed to get kubernetes version from node: failed to get file: command "docker exec --privileged kind-control-plane cat /kind/version" failed with error: exit status 1
Command Output: Error response from daemon: Container 38056cbe6654f6e0c5e0f71c162aff811985eb207c2d3dd807cdb62f6ee9a88f is not running

それに対してPodmanの場合は以下の通りで、クラスター作成前にpodman provider does not work properly in rootless modeとメッセージが出力されている通り「Podmanの場合はrootlessなので一般ユーザー権限だと動かさない」という処理が入っているような雰囲気。(想像です)

[zaki@rhel8 kind]$ kind create cluster
enabling experimental podman provider
Creating cluster "kind" ...
podman provider does not work properly in rootless mode

また、環境変数KIND_EXPERIMENTAL_PROVIDERについては、「Dockerが未インストールでPodmanがインストール済みの場合」は、指定しなくてもPodmanでkindが動作します。
「DockerとPodmanが両方インストール済みの場合」はデフォルトではDockerでkindがデプロイされます。
両方インストール済みの場合にPodmanでkindを動作させたい場合は、明示的にKIND_EXPERIMENTAL_PROVIDER=podmanを指定すればPodmanでkindが使用されます。

関連リンク

スライドのページにも載せてますが再掲。

rheb.hatenablog.com

speakerdeck.com

rheb.hatenablog.com

medium.com

speakerdeck.com

speakerdeck.com