SSHアクセスできないNW装置だってAnsibleコアモジュールで操作したい!
はじめに
複数のサーバやクラウドの構成管理に使われるAnsibleは、バージョン2.1からNetwork Automationがコアモジュールとして正式サポートされた。Cisco IOS、Cisco IOS-XR、Cisco NXOSなどの各種NW装置についてもそれぞれAnsibleモジュール提供され、容易に操作できるようになっている。
ただしこれらAnsibleモジュールでは対象NW装置への接続にセキュアなsshプロトコルを利用しており、telnetプロトコルはサポートされていない。このため、telnetサービスは有効であるがsshサービスが有効になっていないような装置では、これらAnsibleモジュールが提供する高度な自動化が実現できない。
このような場合、まず考えるべきは対象NW装置でsshサービスを有効にすること。そもそもtelnetサービスしか有効になっていないことが現代の運用ではあり得ないのであって、セキュリティ対策上でもこれ以外の対応を考えるべきではない。ただしろくでもない事情によって、そうもいかないケースももしかしたら稀にあるかも知れない。あるのかな?[1]
さて以下では、何らかの事情によってtelnetサービスしか使えないNW装置で、いかにAnsibleを使うかを検討してみた。
まずは比較対象として、Cisco IOS-XR装置に対してcisco.iosxr.iosxr_command
モジュールをつかったssh接続を行う自動化手順を紹介する。インベントリファイルinventory.yml
を用意する。定義した装置はrouter01
とrouter02
の2台。[2]
iosxr_routers:
vars:
ansible_user: 'cisco'
ansible_password: 'cisco'
ansible_connection: 'network_cli'
ansible_network_os: 'iosxr'
hosts:
router01:
ansible_host: 10.1.0.1
router02:
ansible_host: 10.1.0.2
次にプレイブックplaybook.yml
を用意する。インベントリで定義した装置について、show ipv4 interface brief
コマンドの出力結果を表示するだけの単純なもの。
- hosts: iosxr_routers
gather_facts: false
tasks:
- name: send command
cisco.iosxr.iosxr_command:
commands:
- 'show ipv4 interface brief'
register: command_result
- name: display result
debug:
var: command_result
では実行してみる。
$ ansible-playbook -i inventory.yml playbook.yaml
PLAY [iosxr_routers] *************************************************************************************************************************
TASK [send command] *******************************************************************************************************************
ok: [router01]
ok: [router02]
TASK [display result] *****************************************************************************************************************
ok: [router02] => {
"command_result": {
"changed": false,
"failed": false,
"stdout": [
"Interface IP-Address Status Protocol Vrf-Name\nLoopback0 10.1.0.2 Up Up default \nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \nGigabitEthernet0/0/0/0 172.16.0.14 Up Up default \nGigabitEthernet0/0/0/1 172.16.0.18 Up Up default \n"
],
"stdout_lines": [
[
"Interface IP-Address Status Protocol Vrf-Name",
"Loopback0 10.1.0.2 Up Up default ",
"MgmtEth0/0/CPU0/0 unassigned Shutdown Down default ",
"GigabitEthernet0/0/0/0 172.16.0.14 Up Up default ",
"GigabitEthernet0/0/0/1 172.16.0.18 Up Up default "
]
]
}
}
ok: [router01] => {
"command_result": {
"changed": false,
"failed": false,
"stdout": [
"Interface IP-Address Status Protocol Vrf-Name\nLoopback0 10.1.0.1 Up Up default \nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \nGigabitEthernet0/0/0/0 172.16.0.2 Up Up default \nGigabitEthernet0/0/0/1 172.16.0.6 Up Up default \n"
],
"stdout_lines": [
[
"Interface IP-Address Status Protocol Vrf-Name",
"Loopback0 10.1.0.1 Up Up default ",
"MgmtEth0/0/CPU0/0 unassigned Shutdown Down default ",
"GigabitEthernet0/0/0/0 172.16.0.2 Up Up default ",
"GigabitEthernet0/0/0/1 172.16.0.6 Up Up default "
]
]
}
}
PLAY RECAP *****************************************************************************************************************************
router01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router02 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
はい、できた。
それぞれ2台の装置に対してshow ipv4 interface brief
コマンドの結果が無事に表示された。これと同じ出力を、telnetプロトコルによる接続のみで得たい。
ansible.netcommon.telnetモジュールを利用する
ではansible.netcommon.telnet
モジュールを使う方法を紹介する。
sshサービスが有効でない場合、通常telnetモジュールを使う以外に検討余地はないだろう。
インベントリファイルinventory.yml
は特に書き換える必要がない。
プレイブックplaybook.yml
は以下のように書き換える。
- hosts: iosxr_routers
gather_facts: false
tasks:
- name: send command
ansible.netcommon.telnet:
user: "{{ ansible_user }}"
password: "{{ ansible_password }}"
login_prompt: "Username: "
password_prompt: "Password: "
prompts:
- "[>#]"
command:
- 'show ipv4 interface brief'
register: command_result
changed_when: False
- name: display result
debug:
var: command_result
telnetモジュールは要はexpectコマンドのようなもので、コマンドを入力しその出力(次のプロンプトまでの文字列)を抜き出すというもの。そのためログイン時に画面に表示されるログインプロンプト文字列login_prompt
とパスワードプロンプト文字列password_prompt
をプレイブック中に指定し、ログイン処理に備える必要がある。コマンドプロンプト文字列prompts
を指定しているのも同様の理由によるもの。
実行してみる。
$ ansible-playbook -i inventory.yml playbook.yml
PLAY [iosxr_routers] *************************************************************************************************************************
TASK [send command] *******************************************************************************************************************
ok: [router02]
ok: [router01]
TASK [display result] *****************************************************************************************************************
ok: [router02] => {
"iosxr_results": {
"changed": false,
"failed": false,
"output": [
"show ipv4 interface brief\r\n\rMon Nov 15 21:01:11.749 JST\r\n\r\nInterface IP-Address Status Protocol Vrf-Name\r\nLoopback0
10.1.0.2 Up Up default \r\nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \r\nGigabitEthernet0/0/0/0 172.16.0.14 Up Up default \r\nGigabitEthernet0/0/0/1 172.16.0.18 Up Up default \r\n\r\n\rRP/0/0/CPU0:router02#"
]
}
}
ok: [router01] => {
"iosxr_results": {
"changed": false,
"failed": false,
"output": [
"show ipv4 interface brief\r\n\rMon Nov 15 21:01:11.288 JST\r\n\r\nInterface IP-Address Status Protocol Vrf-Name\r\nLoopback0
10.1.0.1 Up Up default \r\nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \r\nGigabitEthernet0/0/0/0 172.16.0.2 Up Up default \r\nGigabitEthernet0/0/0/1 172.16.0.6 Up Up default \r\n\r\n\rRP/0/0/CPU0:router01"
]
}
}
PLAY RECAP *****************************************************************************************************************************
router01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router02 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
できた。
一見特に大きな問題なく操作できたように見える。実際この程度のコマンド結果を表示する程度のプログラムであれば、これで事足りる。
ではここで、対象NW装置に対してコンフィグモードからインタフェースにIPアドレスを設定したい場合にも、telnetモジュールは同様に使えるだろうか。出力をきれいに整形して別プログラムで再利用する必要がある場合はどうだろうか。プロンプト文字列が通常と異なるようなコマンドの場合にも対応可能だろうか。
telnetモジュールではあくまで基本的な操作しか実現できず、iosxr_command
やiosxr_config
などのコアモジュールにより提供される機能と同様の高度な操作を実現しようとすると、複雑な逐次手順を記述しなくてはならずAnsibleによる自動化の便利さ・容易さを享受できないことが多い。
sshをtelnetに変換するプロキシを利用する
やはりここは(telnetモジュールのような代替手段でなく)iosxr_command
やiosxr_config
などのAnsibleコアモジュールを使いたい。
さて、ここにsshプロトコルをtelnetプロトコルに変換できる自作ツールがある。これを使って元のプレイブックをそのまま実行してみることを考える。
まずはツールのバイナリをリリースからダウンロードして、パスの通ったディレクトリに保存し、以下の通り起動する。
$ ssh2telnet --addr :2222 --login --login-prompt 'Username: ' --password-prompt 'Password: '
Starting ssh server on :2222
--login-prompt
および--password-prompt
ではIOS-XR装置のログインプロンプトとパスワードプロンプトを指定する。これらはssh2telnet
プログラムが対象装置にtelnet接続し自動ログインする際に利用される。
ツールを起動すると、2222/tcpポートでsshサーバーが待ち受けを開始する。
それではこのツール宛てのssh接続がきちんと対象装置宛てのtelnet接続に変換され、自動ログインできることを確認してみる。
別ターミナルから以下のコマンドを入力する。-l cisco@10.1.0.1
オプションではtelnetで接続する対象について、<ログインユーザ名>@<装置アドレス>
の形式で指定している。sshコマンド実行直後にパスワード入力を求められるので、ログインパスワード(ここではcisco)を入力すること。
$ ssh localhost -p 2222 -l cisco@10.1.0.1
cisco@10.1.0.1@localhost's password:
RP/0/RSP0/CPU0:router01#show clock
Thu Nov 15 13:00:00.000 JST
13:00:00:000 JST Thu Nov 15 2021
RP/0/RSP0/CPU0:router01#exit
Connection to localhost closed.
できた!sshサービスが有効になっていない装置に、sshプロトコルを使ってtelnet接続できた。ほかにも自由にコマンドを入力し、問題なく動作することを試してみよう。
ではこのツールを使う前提で、インベントリファイルinventory.yml
を書き換えてみる。
iosxr_routers:
vars:
ansible_host: 'localhost'
ansible_port: '2222'
ansible_password: 'cisco'
ansible_connection: 'network_cli'
ansible_network_os: 'iosxr'
hosts:
router01:
ansible_user: 'cisco@10.1.0.1'
router02:
ansible_user: 'cisco@10.1.0.2'
ansible_host
, ansible_port
とansible_user
のパラメータが変わっているので注意。
プレイブックplaybook.yml
のほうは変更が必要ない。以下再掲。
- hosts: iosxr_routers
gather_facts: false
tasks:
- name: send command
cisco.iosxr.iosxr_command:
commands:
- 'show ipv4 interface brief'
register: command_result
- name: display result
debug:
var: command_result
では実行してみる。
なおssh2telnet
はsshサーバーのHOST_KEYを起動のたび自動生成するため、停止・再起動後のssh接続ではfingerprintがマッチしない旨の警告が表示されることがある。これを防ぐため、プレイブック実行前にANSIBLE_HOST_KEY_CHECKING
環境変数をfalse
に設定している。[3]
$ export ANSIBLE_HOST_KEY_CHECKING=false
$ ansible-playbook -i inventory.yml playbook.yaml
PLAY [iosxr_routers] *************************************************************************************************************************
TASK [send command] *******************************************************************************************************************
ok: [router01]
ok: [router02]
TASK [display result] *****************************************************************************************************************
ok: [router02] => {
"command_result": {
"changed": false,
"failed": false,
"stdout": [
"Interface IP-Address Status Protocol Vrf-Name\nLoopback0 10.1.0.2 Up Up default \nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \nGigabitEthernet0/0/0/0 172.16.0.14 Up Up default \nGigabitEthernet0/0/0/1 172.16.0.18 Up Up default \n"
],
"stdout_lines": [
[
"Interface IP-Address Status Protocol Vrf-Name",
"Loopback0 10.1.0.2 Up Up default ",
"MgmtEth0/0/CPU0/0 unassigned Shutdown Down default ",
"GigabitEthernet0/0/0/0 172.16.0.14 Up Up default ",
"GigabitEthernet0/0/0/1 172.16.0.18 Up Up default "
]
]
}
}
ok: [router01] => {
"command_result": {
"changed": false,
"failed": false,
"stdout": [
"Interface IP-Address Status Protocol Vrf-Name\nLoopback0 10.1.0.1 Up Up default \nMgmtEth0/0/CPU0/0 unassigned Shutdown Down default \nGigabitEthernet0/0/0/0 172.16.0.2 Up Up default \nGigabitEthernet0/0/0/1 172.16.0.6 Up Up default \n"
],
"stdout_lines": [
[
"Interface IP-Address Status Protocol Vrf-Name",
"Loopback0 10.1.0.1 Up Up default ",
"MgmtEth0/0/CPU0/0 unassigned Shutdown Down default ",
"GigabitEthernet0/0/0/0 172.16.0.2 Up Up default ",
"GigabitEthernet0/0/0/1 172.16.0.6 Up Up default "
]
]
}
}
PLAY RECAP *****************************************************************************************************************************
router01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
router02 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
プレイブックは一切修正することなく、まったく同一の出力を得ることができた。
やったね🎉[4]
-
あまり探りを入れないほうがいい。Don't think, feel. ↩︎
-
こういった検証目的の装置はIOSXRvをvagrantとVirtualBoxで一発起動する方法やIOSXRvをvagrant-libvirtで一発起動する方法で用意するのがたいへん便利 ↩︎
-
ssh2telnet -k /path/to/hostkey
で静的にHOST_KEYを指定することも可能 ↩︎ -
ssh2telnet
を使って大規模なプレイブックを実行した実績が、実は私にはまだない。何か不具合や問題があれば、GithubにIssue報告もらえるとうれしい ↩︎
Discussion