自己开发的 Ansible 命令系网络模块的故事
首先
在本篇文章中,我们将重点介绍Ansible网络模块中用于执行show命令等的”xxx_command”模块。虽然在文章中介绍了自制的模块,想要说的是”这个模块是推荐的!”,但由于尚未完善,所以请将其阅读为自制模块的一个示例!
关于command模块
Ansible 2.6及之前的版本中,存在着各个厂商操作系统特定的模块,如ios_command、nxos_command和junos_command等。从2.7开始,还发布了与操作系统无关的cli_command。
每个模块的特点如下所示。
即使对于cli_command,也不是完全不需要关注操作系统的差异,您仍然需要在Inventory中指定ansible_network_os,如下所示。(据我所知,第三方提供的多厂商支持模块也是如此。)
[ios]
192.168.100.200
[ios:vars]
ansible_connection=network_cli
ansible_network_os=ios #OSを指定
ansible_user=test
ansible_password=test
[nxos]
192.168.100.201
[nxos:vars]
ansible_connection=network_cli
ansible_network_os=nxos #OSを指定
ansible_user=test
ansible_password=test
背景是我制作自己的模块。
在处理Cisco设备时,尽管通过执行相同的show命令,但对于每个操作系统类型(IOS、NX-OS、ASA、IOS-XR),将其分组有点麻烦。对于不熟悉的成员来说,这可能是一个小障碍。因此,我考虑是否可以创建一个可以自动检测操作系统并执行命令的模块。
在另一个项目中,我偶然有机会研究了Python库netmiko的设备类型(操作系统)自动检测功能,并且我觉得如果将其集成为一个模块,可能会解决问题!
我建议您参考横地先生的博客了解netmiko和自动检测功能。
使用netmiko自动检测并执行命令以探测网络设备的类型。
指令获取目标设备
我准备了以下两种设备。
※1: 严格来说,它是指IOS-XE,但在Ansible和netmiko中,被归类为”IOS”。
※2: 这次,我们使用了Cisco DevNet的一个沙盒,即Open NX-OS Programmability之一。
自己创建的模块内容
模块名是基于netmiko库创建的命令模块,本来打算叫netmiko_command,但考虑到它不是正式模块,我改变了心意,现在叫做neko_command(这也不是什么重要的话题!)
该模块由Python编写,主要执行以下7个步骤。
-
- 获取在Inventory文件和Variable文件中指定的值,并将其存储在变量中。在这里,如果值是必需的,则将其设为required=True。如果是可选的,则设为required=False,并通过default=指定默认使用的值。另外,如果不希望在Playbook执行结果中显示的参数,则将其设为no_log=True。
对于指定的值中的command(指定要执行的show命令),我们会检查其开头是否以”show”开头。如果检查结果为NG,则输出消息并标记为Fail。(这个步骤不是必需的)
指定用于通过netmiko进行SSH登录的设备信息。在这里,将device_type设为autodetect(自动检测)。
使用netmiko的SSHDetect来自动检测设备类型。
将检测到的设备类型(如cisco_ios、cisco_nxos等)重新存储到device_type中。
使用netmiko的ConnectHandler,根据权限级别输入enable密码,并执行通过command指定的show命令。
将检测到的设备类型best_match和show命令的结果stdout存储在名为result的字典格式变量中,并将其转换为JSON格式进行输出。
我认为1(输入)和7(输出)在许多模块中是共通的(当然参数会有所不同)。此外,虽然本次使用了netmiko的3~6,但是可以选择使用已存在的库如ios_command来进行登录处理和命令获取,然后自行实现后续处理的选项也是有的。
from ansible.module_utils.basic import *
from netmiko import ConnectHandler
from netmiko.ssh_autodetect import SSHDetect
def main():
'''
Ansible module to get device type automatically and issue show command.
'''
# (1) store value from inventory file
module = AnsibleModule(
argument_spec=dict(
host=dict(required=True),
port=dict(default=22, required=False),
username=dict(required=True),
password=dict(required=True, no_log=True),
secret=dict(required=False, default='', no_log=True),
enable_mode=dict(required=False, default=False),
command=dict(type='str', required=True),
),
supports_check_mode=True
)
# (2) parse command
if module.supports_check_mode and not module.params['command'].startswith('show'):
module.fail_json(
msg='Only show commands are supported when using check_mode, not '
'executing %s' % module.params['command']
)
warnings = list()
result = {'changed': False, 'warnings': warnings}
# (3) define login information
remote_device = {
'device_type': 'autodetect',
'ip': module.params['host'],
'username': module.params['username'],
'password': module.params['password'],
'port' : module.params['port'], # optional, defaults to 22
'secret': module.params['secret'], # optional, defaults to ''
'verbose': False, # optional, defaults to False
}
# (4) autodetect device type
guesser = SSHDetect(**remote_device)
best_match = guesser.autodetect()
# (5) store the detected type in device_type
remote_device['device_type'] = best_match
# (6) issue show command
connection = ConnectHandler(**remote_device)
if module.params['enable_mode']:
connection.enable()
response = connection.send_command(module.params['command'])
# (7) return the output of show command
result['best_match'] = best_match
result['stdout'] = response
module.exit_json(**result)
connection.disconnect()
if __name__ == "__main__":
main()
然后,将创建的Python文件存储在模块执行的位置。可以使用以下命令确认存储位置。
[centos@localhost ansible]$ ansible --version
ansible 2.7.0
config file = /etc/ansible/ansible.cfg
ansible python module location = /usr/lib/python2.7/site-packages/ansible # ←ココ
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Apr 11 2018, 07:36:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)]
库存文件
文件的内容如下。
-
- すべて同じグループに収容可能ですので、allグループにまとめて対象機器情報を記載しています。
-
- 認証方式が同じ場合、機器固有の環境変数として[all]内で指定するのは、ホスト情報くらいかと思います。今回は、Cisco DevNetを使っており、機器毎にユーザ名、パスワード、ポート番号が異なったため、個別に指定しています。
-
- グループ変数[all:vars]の内、ansible_network_osは何かしら入れないと、実行の最初のタイミングでAnsibleConnectionFailureエラーとなるため、ダミーの値としてjunosを指定しています。(イケてないですw)
-
- 今回の2台の機器は、ログイン時のenableパスワード入力は不要ですので、ansible_becomeやansible_become_passは省略しています。
ansible_commandで、取得コマンドとして「show version」を指定しています。
[all]
test ansible_host=192.168.100.200 ansible_user=test ansible_password=cisco ansible_port=22
sbx-n9kv-ao ansible_host=sbx-nxos-mgmt.cisco.com ansible_user=admin ansible_password=Admin_1234! ansible_port=8181
[all:vars]
ansible_network_os=junos
# To avoid AnsibleConnectionFailure before executing neko_command module,
# set 'junos' as a dummy value (any type of network os is OK.).
ansible_connection=network_cli
ansible_command=show version
玩的本领 de
以下是Playbook的内容。
neko_commandモジュールで、Inventoryファイルで指定した値を元にshowコマンドを実行。その結果をresultに格納。
debugモジュールで、resultに格納されているキー値 best_matchおよびstdout_linesを出力。
---
- hosts: all
gather_facts: no
tasks:
- name: execute show command
neko_command:
host={{ ansible_host }}
username={{ ansible_user }}
password={{ ansible_password }}
port={{ ansible_port }}
command={{ ansible_command }}
register: result
- name: debug
debug:
msg:
- "{{ result.best_match }}"
- "{{ result.stdout_lines }}"
运行Playbook
使用Netmiko需要预先安装,可以使用pip install netmiko等命令进行安装。
然后,我们会执行ansible-playbook -i inventory4 playbook1-5.yml。
执行结果
测试主机的best_match的值部分地变成星号,但是show命令能够正常获取。
然而,由于netmiko自动检测处理需要花费时间,每台设备的所需时间大约为30秒。(有点长!)
[centos@localhost ansible]$ ansible-playbook -i inventory4 playbook1-5.yml
PLAY [all] *************************************************************************************************************************************
TASK [execute show command] ********************************************************************************************************************
ok: [test]
ok: [sbx-n9kv-ao]
TASK [debug] ***********************************************************************************************************************************
ok: [test] => {
"msg": [
"********_ios",
[
"Cisco IOS XE Software, Version 03.17.02.S - Standard Support Release",
"Cisco IOS Software, CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 15.6(1)S2, RELEASE SOFTWARE (fc1)",
"Technical Support: http://www.********.com/techsupport",
"Copyright (c) 1986-2016 by Cisco Systems, Inc.",
"Compiled Mon 06-Jun-16 22:04 by mcpre",
~ 中略 ~
]
]
}
ok: [sbx-n9kv-ao] => {
"msg": [
"cisco_nxos",
[
"Cisco Nexus Operating System (NX-OS) Software",
"TAC support: http://www.cisco.com/tac",
"Documents: http://www.cisco.com/en/US/products/ps9372/tsd_products_support_series_home.html",
"Copyright (c) 2002-2018, Cisco Systems, Inc. All rights reserved.",
"The copyrights to certain works contained herein are owned by",
"other third parties and are used and distributed under license.",
"Some parts of this software are covered under the GNU Public",
"License. A copy of the license is available at",
"http://www.gnu.org/licenses/gpl.html.",
"",
"Nexus 9000v is a demo version of the Nexus Operating System",
"",
"Software",
" BIOS: version ",
" NXOS: version 9.2(1)",
" BIOS compile time: ",
" NXOS image file is: bootflash:///nxos.9.2.1.bin",
" NXOS compile time: 7/17/2018 16:00:00 [07/18/2018 00:21:19]",
~ 中略 ~
]
]
}
PLAY RECAP *************************************************************************************************************************************
sbx-n9kv-ao : ok=2 changed=0 unreachable=0 failed=0
test : ok=2 changed=0 unreachable=0 failed=0
[centos@localhost ansible]$
失败的经历
最初我试图将自定义模块的输出结果(best_match自动检测的OS类型)存储到Playbook中定义的变量ansible_network_os中,并在后续的cli_command和cli_config任务执行时加载该变量。但是这种方法不起作用。
连接插件和与之相关的ansible_network_os在Playbook开始时被加载,所以似乎无法在中途更改其值。
最后
基本上可以制作一个可以运行的版本。虽然在ansible_network_os上使用虚拟值或者需要花费一些处理时间,但在不急于完成的时候,或者只是想获取一下show version的时候,似乎可以使用。
在未来的Ansible版本中,如果在ansible_network_os中指定了autodetect(或者不指定任何内容),能够自动检测并实现这个功能,那就会变得更加方便呢~ 这是我今天的感想。

2018/12/16 补充记录
这个故事的困惑,在续作中,通过自己制作Ansible的命令系网络模块的故事,部分解决了喵!