はじめに
以前の投稿で、ターミナルサーバ経由でNW機器を操作するためのモジュール自作例をご紹介しました。
Ansible自作モジュールを使ってターミナルサーバ経由でNW機器を操作してみた
自作したconsole_command
モジュールは、コマンド実行操作をnetmiko
というPythonライブラリで実装していました。
今回はモジュールではなく、NW機器へCLIログインするためのnetwork_cli
コネクションプラグインの方を、コンソール接続に対応できるようカスタマイズし、公式のios_command
モジュール等がそのまま使えるようにしてみました。
セットアップ
-
ターミナルサーバ、ルータ
Ansibleから各ルータへSSHログインする際は、ターミナルサーバのIPアドレス宛に、TCPポート番号3001と3002を使って接続します。
またログイン時、ターミナルサーバに設定したユーザ名/パスワード、ルータに設定したenableパスワードが聞かれる設定にしています。 -
PC(CentOS)
Ansibleのインストールに加え、以下を実施しています。 -
netmikoのインストール
$ sudo pip install netmiko
- hostsファイルの追記
今回2台のルータのIPアドレスは同じなのですが、Inventoryファイルに同じIPアドレスを記載すると、その内1台にしか操作が加えられないため、便宜的に以下の通りhostsファイルの追記を行い、1つのIPアドレスに2つのホスト名が割り当てられるようにしています。
$ sudo vi /etc/hosts
192.168.100.79 con_port1
192.168.100.79 con_port2
Inventoryファイル
以下の通り作成しました。ポート番号を指定している点、ターミナルサーバのユーザ名/パスワードを使用している点以外は、通常のSSH接続のInventoryファイルと同様です。
[cisco]
con_port1 ansible_port=3001
con_port2 ansible_port=3002
[cisco:vars]
ansible_network_os=ios
ansible_user=(ターミナルサーバのユーザ名)
ansible_password=(ターミナルサーバのパスワード)
ansible_become=yes
ansible_become_method=enable
ansible_become_pass=(ルータのenableパスワード)
** 追記 **
以下の書き方であれば、hostsファイルの追記は不要でした。
[cisco]
hqborder2 ansible_host=192.168.100.79 ansible_port=3001
brborder2 ansible_host=192.168.100.79 ansible_port=3002
[cisco:vars]
ansible_network_os=ios
ansible_user=(ターミナルサーバのユーザ名)
ansible_password=(ターミナルサーバのパスワード)
ansible_become=yes
ansible_become_method=enable
ansible_become_pass=(ルータのenableパスワード)
Playbook
Cisco IOS用のios_config
モジュールを使用し、(1)Configに書かれたホスト名情報、(2)ログインユーザ情報を取得し、debug
モジュールで結果を標準出力しています。
---
- hosts: cisco
gather_facts: no
connection: network_cli
tasks:
- name: run show command
ios_command:
commands:
- show run | include hostname #(1)
- show users #(2)
register: result
- name: debug
debug:
msg: "{{ result.stdout_lines }}"
network_cliコネクションプラグイン
network_cli.pyの格納場所
ansible --version
コマンドでansible python module locationを確認し、その配下にあるnetwork_cli.pyを編集しました。元の状態に戻せるよう、現ファイルは別名でバックアップを取っておいたほうが良いと思います。
$ sudo vi [ansible python module location]/plugins/connection/network_cli.py
ちなみに、Actionプラグイン側で、各モジュールがサポートするコネクションプラグイン(network_cli、httpapi、local等)を指定しているため、例えばnetwork_cli_con.pyのような別ファイルを作成し、Playbook内でプラグイン名をnetwork_cli_conに変えても、「無効なコネクションタイプです」というエラーが出てうまくいきませんでした。
カスタマイズ内容
以下3点です。
-
パッケージの追加
冒頭でnetmikoとansible.module_utils.basicを追加しています。 -
コンソールログインヘルパー1の追加
コンソールログイン開始時、Enterキーの押下(改行コードの送信)を実行しないとプロンプトが現れなかったため、Ansibleがparamiko
モジュールでログイン処理を開始する前に、netmiko
で改行コードを2回送信する処理を加えました。(以前の投稿で作成したコードを流用。)
なお、通常のログイン時に影響を及ぼさないよう、TCPポート番号が3001~3048の場合のみ本処理を実行させています。 -
コンソールログインヘルパー2の追加
paramiko
によるターミナルサーバのユーザ名/パスワード入力後、ユーザーモードから特権モードに移る間でも改行コードの送信が必要だったため、paramiko
のssh_shell.send(b'\r')
で2回改行コードを送信しています。
# (1) import two additional packages
from ansible.module_utils.basic import *
from netmiko import ConnectHandler
# (1) -END-
(中略)
def _connect(self):
'''
Connects to the remote device and starts the terminal
'''
# (2) Console login helper1 -START-
if self._play_context.port is not None and 3001 <= self._play_context.port <= 3048:
device_args = {
'device_type' : 'terminal_server',
'ip' : self._play_context.remote_addr,
'port' : self._play_context.port,
'username' : self._play_context.remote_user,
'password' : self._play_context.password,
}
net_connect = ConnectHandler(**device_args)
net_connect.write_channel('\r')
time.sleep(1)
net_connect.write_channel('\r')
time.sleep(1)
output = net_connect.read_channel()
# Disable initial configuration
if '[yes/no]' in output:
net_connect.write_channel('no\r')
# (2) -END-
if not self.connected:
self.paramiko_conn = connection_loader.get('paramiko', self._play_context, '/dev/null')
self.paramiko_conn._set_log_channel(self._get_log_channel())
self.paramiko_conn.set_options(direct={'look_for_keys': not bool(self._play_context.password and not self._play_context.private_key_file)})
self.paramiko_conn.force_persistence = self.force_persistence
ssh = self.paramiko_conn._connect()
host = self.get_option('host')
self.queue_message('vvvv', 'ssh connection done, setting terminal')
self._ssh_shell = ssh.ssh.invoke_shell()
self._ssh_shell.settimeout(self.get_option('persistent_command_timeout'))
self._terminal = terminal_loader.get(self._network_os, self)
if not self._terminal:
raise AnsibleConnectionFailure('network os %s is not supported' % self._network_os)
self.queue_message('vvvv', 'loaded terminal plugin for network_os %s' % self._network_os)
# (3) Console login helper2 -START-
if self._play_context.port is not None and 3001 <= self._play_context.port <= 3048:
self._ssh_shell.send(b'\r')
time.sleep(1)
self._ssh_shell.send(b'\r')
time.sleep(1)
output2 = self._ssh_shell.recv(1000).decode('utf-8')
if '[yes/no]' in output2:
self._ssh_shell.send(b'no')
self._ssh_shell.send(b'\r')
# (3) -END-
self.receive(prompts=self._terminal.terminal_initial_prompt, answer=self._terminal.terminal_initial_answer,
newline=self._terminal.terminal_inital_prompt_newline)
self.queue_message('vvvv', 'firing event: on_open_shell()')
self._terminal.on_open_shell()
if self._play_context.become and self._play_context.become_method == 'enable':
self.queue_message('vvvv', 'firing event: on_become')
auth_pass = self._play_context.become_pass
self._terminal.on_become(passwd=auth_pass)
self.queue_message('vvvv', 'ssh connection has completed successfully')
self._connected = True
return self
(中略)
実行結果
表示されたホスト名から、2台に機器に対してログインできていること、show usersの結果から、想定通りコンソール経由(con 0)でアクセスできていることが分かります。
$ ansible-playbook -i inventory_con playbook1_con.yml
PLAY [cisco] **********************************************************************************
TASK [run show command] ***********************************************************************
ok: [con_port1]
ok: [con_port2]
TASK [debug] **********************************************************************************
ok: [con_port1] => {
"msg": [
[
"hostname hqborder2"
],
[
"Line User Host(s) Idle Location",
"* 0 con 0 idle 00:00:00 ",
"",
" Interface User Mode Idle Peer Address"
]
]
}
ok: [con_port2] => {
"msg": [
[
"hostname brborder2"
],
[
"Line User Host(s) Idle Location",
"* 0 con 0 idle 00:00:00 ",
"",
" Interface User Mode Idle Peer Address"
]
]
}
PLAY RECAP ************************************************************************************
con_port1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
con_port2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
最後に
ちょっと無理矢理ですが、一応動くものが作れました。ただ、ターミナルサーバの仕様や、NW機器のログイン設定によっては、動作しなかったり想定外の動作をしたりする可能性がありますので、自己責任でお願いします!
これでキッティングが楽になるといいなー。 Happy Automation!