我尝试通过 REST API 来操作 Ansible Core
首先
虽然Ansible有很多选择,但Automation Controller和AWX可以通过Web GUI和REST API进行操作,而Ansible Core则需要通过命令行进行操作。
如果已经引入了Ansible Core,随着使用范围的扩大,有些情况可能会面临无法与其他系统进行API集成的问题。因此,我尝试实现了通过REST API进行操作的功能。
在本次尝试中,被操作的设备(Managed node)是Cisco CSR1000v(IOS-XE)。
安装Ansible
我在CentOS的Python3.x虚拟环境(venv)中安装了以下内容。
pyATS是用于解析Cisco设备的show命令输出结果的工具,而Flask是构建API服务器所需要的包。
$ pip install ansible
$ pip install paramiko
$ pip install pyats[full]
$ pip install flask
$ pip install Flask-HTTPAuth
$ ansible --version
ansible [core 2.11.12]
(省略)
库存文件
作为目标设备,我们使用了Cisco DevNet Sandbox的CSR1000v 17.3.1。
CSR上的IOS XE – 最新的代码始终在线。
[cisco]
csr1000v-1 ansible_host=sandbox-iosxe-latest-1.cisco.com ansible_user=developer ansible_password=XXXXXX
[cisco:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
游戏规则
这是一个使用pyATS解析show version命令执行结果,并以结构化数据获取型号和版本信息的Playbook。
---
- hosts: cisco
gather_facts: no
tasks:
- name: issue show version
ansible.netcommon.cli_parse:
command: "show version"
parser:
name: ansible.netcommon.pyats
执行Playbook
对于REST API,将响应数据转为JSON格式可以使后续的API客户端数据处理更加容易。
为了进行 REST API 的支持预备工作,一方面 Playbook 执行结果默认以文本形式输出,我们尝试以 JSON 格式输出。具体来说,在执行 `ansible-playbook` 命令之前,需要在环境变量中添加 `ANSIBLE_STDOUT_CALLBACK=json`,以指定 Callback 插件为 JSON 形式。
$ ANSIBLE_STDOUT_CALLBACK=json ansible-playbook -i inventory_devnet_csr.ini playbook_get_inventory.yml
查看执行结果,可以看到在开头出现了[DEPRECATION WARNING]警告。原因是Python版本过旧。
之后大量输出了JSON数据。想要的是show命令解析的结果(”parsed”的内容),但还有解析前的结果(”stdout”的内容)和转换为列表形式的结果(”stdout_lines”的内容)。
最后,还显示了每个目标设备的ok/changed/failures等计数(”stats”的内容)。
[DEPRECATION WARNING]: Ansible will require Python 3.8 or newer on the controller starting with Ansible 2.12. Current version: 3.X.X
(default, May 2 2019, 20:40:44) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]. This feature will be removed from ansible-core in version
2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
(省略)
{
"custom_stats": {},
"global_custom_stats": {},
"plays": [
{
"play": {
"duration": {
"end": "2022-12-10T06:27:53.053417Z",
"start": "2022-12-10T06:27:29.534216Z"
},
"id": "000c29c8-132f-e4f6-f5db-000000000006",
"name": "cisco"
},
"tasks": [
{
"hosts": {
"csr1000v-1": {
"_ansible_no_log": false,
"action": "ansible.netcommon.cli_parse",
"changed": false,
"parsed": {
"version": {
"chassis": "CSR1000V",
"chassis_sn": "XXXXXX",
"compiled_by": "mcpre",
"compiled_date": "Wed 12-Aug-20 00:16",
"copyright_years": "1986-2020",
"curr_config_register": "0x2102",
"disks": {
"bootflash:.": {
"disk_size": "6188032",
"type_of_disk": "virtual hard disk"
}
},
"hostname": "STOYANTESTHOSTNAME",
"image_id": "X86_64_LINUX_IOSD-UNIVERSALK9-M",
"image_type": "production image",
"label": "RELEASE SOFTWARE (fc3)",
"last_reload_reason": "reload",
"license_level": "ax",
"license_type": "N/A(Smart License Enabled)",
"location": "Amsterdam",
"main_mem": "715705",
"mem_size": {
"non-volatile configuration": "32768",
"physical": "3978420"
},
"next_reload_license_level": "ax",
"number_of_intfs": {
"Gigabit Ethernet": "3"
},
"os": "IOS-XE",
"platform": "Virtual XE",
"processor_type": "VXE",
"returned_to_rom_by": "reload",
"rom": "IOS-XE ROMMON",
"router_operating_mode": "Autonomous",
"rtr_type": "CSR1000V",
"system_image": "bootflash:packages.conf",
"uptime": "5 hours, 55 minutes",
"uptime_this_cp": "5 hours, 56 minutes",
"version": "17.3.1a",
"version_short": "17.3",
"xe_version": "17.03.01a"
}
},
"stdout": "Cisco IOS XE Software, Version 17.03.01a\nCisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2020 by Cisco Systems, Inc.\nCompiled Wed 12-Aug-20 00:16 by mcpre\n\n\nCisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.\nAll rights reserved. Certain components of Cisco IOS-XE software are\nlicensed under the GNU General Public License (\"GPL\") Version 2.0. The\nsoftware code licensed under GPL Version 2.0 is free software that comes\nwith ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such\nGPL code under the terms of GPL Version 2.0. For more details, see the\ndocumentation or \"License Notice\" file accompanying the IOS-XE software,\nor the applicable URL provided on the flyer accompanying the IOS-XE\nsoftware.\n\n\nROM: IOS-XE ROMMON\n\nSTOYANTESTHOSTNAME uptime is 5 hours, 55 minutes\nUptime for this control processor is 5 hours, 56 minutes\nSystem returned to ROM by reload\nSystem image file is \"bootflash:packages.conf\"\nLast reload reason: reload\n\n\n\nThis product contains cryptographic features and is subject to United\nStates and local country laws governing import, export, transfer and\nuse. Delivery of Cisco cryptographic products does not imply\nthird-party authority to import, export, distribute or use encryption.\nImporters, exporters, distributors and users are responsible for\ncompliance with U.S. and local country laws. By using this product you\nagree to comply with applicable laws and regulations. If you are unable\nto comply with U.S. and local laws, return this product immediately.\n\nA summary of U.S. laws governing Cisco cryptographic products may be found at:\nhttp://www.cisco.com/wwl/export/crypto/tool/stqrg.html\n\nIf you require further assistance please contact us by sending email to\nexport@cisco.com.\n\nLicense Level: ax\nLicense Type: N/A(Smart License Enabled)\nNext reload license Level: ax\n\nThe current throughput level is 1000 kbps \n\n\nSmart Licensing Status: UNREGISTERED/No Licenses in Use\n\ncisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.\nProcessor board ID XXXXXX\nRouter operating mode: Autonomous\n3 Gigabit Ethernet interfaces\n32768K bytes of non-volatile configuration memory.\n3978420K bytes of physical memory.\n6188032K bytes of virtual hard disk at bootflash:.\n\nConfiguration register is 0x2102",
"stdout_lines": [
"Cisco IOS XE Software, Version 17.03.01a",
"Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)",
"Technical Support: http://www.cisco.com/techsupport",
"Copyright (c) 1986-2020 by Cisco Systems, Inc.",
"Compiled Wed 12-Aug-20 00:16 by mcpre",
"",
"",
"Cisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.",
"All rights reserved. Certain components of Cisco IOS-XE software are",
"licensed under the GNU General Public License (\"GPL\") Version 2.0. The",
"software code licensed under GPL Version 2.0 is free software that comes",
"with ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such",
"GPL code under the terms of GPL Version 2.0. For more details, see the",
"documentation or \"License Notice\" file accompanying the IOS-XE software,",
"or the applicable URL provided on the flyer accompanying the IOS-XE",
"software.",
"",
"",
"ROM: IOS-XE ROMMON",
"",
"STOYANTESTHOSTNAME uptime is 5 hours, 55 minutes",
"Uptime for this control processor is 5 hours, 56 minutes",
"System returned to ROM by reload",
"System image file is \"bootflash:packages.conf\"",
"Last reload reason: reload",
"",
"",
"",
"This product contains cryptographic features and is subject to United",
"States and local country laws governing import, export, transfer and",
"use. Delivery of Cisco cryptographic products does not imply",
"third-party authority to import, export, distribute or use encryption.",
"Importers, exporters, distributors and users are responsible for",
"compliance with U.S. and local country laws. By using this product you",
"agree to comply with applicable laws and regulations. If you are unable",
"to comply with U.S. and local laws, return this product immediately.",
"",
"A summary of U.S. laws governing Cisco cryptographic products may be found at:",
"http://www.cisco.com/wwl/export/crypto/tool/stqrg.html",
"",
"If you require further assistance please contact us by sending email to",
"export@cisco.com.",
"",
"License Level: ax",
"License Type: N/A(Smart License Enabled)",
"Next reload license Level: ax",
"",
"The current throughput level is 1000 kbps ",
"",
"",
"Smart Licensing Status: UNREGISTERED/No Licenses in Use",
"",
"cisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.",
"Processor board ID XXXXXX",
"Router operating mode: Autonomous",
"3 Gigabit Ethernet interfaces",
"32768K bytes of non-volatile configuration memory.",
"3978420K bytes of physical memory.",
"6188032K bytes of virtual hard disk at bootflash:.",
"",
"Configuration register is 0x2102"
]
}
},
"task": {
"duration": {
"end": "2022-12-10T06:27:53.053417Z",
"start": "2022-12-10T06:27:29.982151Z"
},
"id": "000c29c8-132f-e4f6-f5db-000000000008",
"name": "issue show version"
}
}
]
}
],
"stats": {
"csr1000v-1": {
"changed": 0,
"failures": 0,
"ignored": 0,
"ok": 1,
"rescued": 0,
"skipped": 0,
"unreachable": 0
}
}
}
使用Flask搭建简易的API服务器
本文所述的是REST API的相应部分。在已安装Ansible/Flask的服务器上,将以下Python代码放在其中并执行。
(1) 用户认证
这次我们使用基本认证。允许使用用户名test和密码password访问API。(这是一个简单的实现,没有考虑安全性。)
import json
import os
import re
import subprocess
from flask import Flask, jsonify, request
from flask_httpauth import HTTPBasicAuth
app = Flask(__name__)
auth = HTTPBasicAuth()
users = {
"test": "password"
}
# ユーザ認証
@auth.verify_password
def authenticate(username, password):
if username and password:
if username in users and password == users[username]:
return True
(2) 获取Ansible版本的API(GET方法)
尝试创建一个可通过以下URI发送GET请求并返回Ansible版本的API:
http://<Ansible的IP地址>:100080/api/system/version
首先使用subprocess.run()函数执行ansible –version命令。然后,对于标准输出结果res.stdout,使用re.search()函数进行基于正则表达式的搜索,并只返回版本信息。
如果处理失败,则返回错误消息{“error”: “无法获取输出”}。
# Ansibleバージョン取得API(GET)
@app.route("/api/system/version", methods=["GET"])
@auth.login_required
def get_version():
try:
res = subprocess.run(["ansible", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
m = re.search(r'^ansible\s(.*)', res.stdout)
return jsonify({"version": m.group(1)})
except:
return jsonify({"error": "Fail to get the output"})
(3) 运行Playbook的API(POST方法)
接下来,我们将通过发送POST请求到以下URI来创建一个API,在请求的正文中执行指定的Inventory文件和Playbook。
http://<Ansible的IP地址>:100080/api/playbook/run
首先使用subprocess.run()函数执行ansible-playbook命令。在这里,回调插件的环境变量可以在参数env中指定。
将标准输出结果res.stdout作为先前提到的长JSON数据,将标准错误输出结果res.stderr作为[DEPRECATION WARNING]字符串数据输出,然后将它们整合成一个JSON数据[{<标准输出>}, {标准错误输出}]并返回。
如果处理失败,则返回错误消息{“error”: “获取输出失败”}.
# Playbook実行API(POST)
@app.route("/api/playbook/run", methods=["POST"])
@auth.login_required
def run_playbook():
try:
my_env = os.environ.copy()
my_env["ANSIBLE_STDOUT_CALLBACK"] = "json"
res = subprocess.run(["ansible-playbook", "-i", request.json["inventory"], request.json["playbook"]],
env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
stdout_dict = json.loads(res.stdout)
data = json.dumps([stdout_dict, {"stderr": res.stderr}], indent=2)
return data
except:
return jsonify({"error": "Fail to get the output"})
# TCPポート100080で待ち受け
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=10080)
使用Python运行代码
最後,通过以下命令启动API服务器。
为了简便实现,需要先激活Python虚拟环境,并将其移动到Inventory/Playbook执行目录中,然后按照以下步骤执行Python代码。
执行后,通过Python代码末尾的app.run()函数,可以监听来自其他设备的API访问,使用TCP端口号10080。
$ source <作成したvenv>/bin/activate
$ cd <Inventory/Playbook実行ディレクトリ>
$ python ansible_api.py
* Serving Flask app 'ansible_api' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://192.168.XX.XX:10080/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 908-605-438
import json
import os
import re
import subprocess
from flask import Flask, jsonify, request
from flask_httpauth import HTTPBasicAuth
app = Flask(__name__)
auth = HTTPBasicAuth()
users = {
“test”: “password”
}
# 基本认证
@auth.verify_password
def authenticate(username, password):
if username and password:
if username in users and password == users[username]:
return True
# 获取Ansible版本的API(GET)
@app.route(“/api/system/version”, methods=[“GET”])
@auth.login_required
def get_version():
try:
res = subprocess.run([“ansible”, “–version”], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding=’utf-8′)
m = re.search(r’^ansible\s(.*)’, res.stdout)
return jsonify({“version”: m.group(1)})
except:
return jsonify({“error”: “获取输出失败”})
# 执行Playbook的API(POST)
@app.route(“/api/playbook/run”, methods=[“POST”])
@auth.login_required
def run_playbook():
try:
my_env = os.environ.copy()
my_env[“ANSIBLE_STDOUT_CALLBACK”] = “json”
res = subprocess.run([“ansible-playbook”, “-i”, request.json[“inventory”], request.json[“playbook”]],
env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding=’utf-8′)
stdout_dict = json.loads(res.stdout)
data = json.dumps([stdout_dict, {“stderr”: res.stderr}], indent=2)
return data
except:
return jsonify({“error”: “获取输出失败”})
# 在TCP端口10080上监听
if __name__ == ‘__main__’:
app.run(debug=True, host=’0.0.0.0′, port=10080)
进行API执行
(1) 从邮递员中获取 Ansible 版本

(2)使用邮差来执行Playbook。

(3)使用Python请求执行Playbook
最后,我将从Python中执行API并获取解析后的结果的代码编写出来。
具体来说,我将使用requests.post()发送与(2)相同的请求,如果状态码为200,则从响应数据中提取解析结果并进行打印输出。
import json
import requests
from requests.auth import HTTPBasicAuth
url = "http://192.168.XX.XX:10080/api/playbook/run"
headers = {"Content-Type": "application/json"}
basic = HTTPBasicAuth("test", "password")
body = {"inventory": "inventory_devnet_csr.ini", "playbook": "playbook_get_inventory.yml"}
response = requests.post(url, headers=headers, auth=basic, json=body)
if response.status_code == 200:
data = response.json()
parsed_data = data[0]["plays"][0]["tasks"][0]["hosts"]["csr1000v-1"]["parsed"]
print(json.dumps(parsed_data, indent=2))
else:
print("Connection Error")
执行结果如下所示。
>python ansible_get_inventory.py
{
"version": {
"chassis": "CSR1000V",
"chassis_sn": "XXXXXX",
"compiled_by": "mcpre",
"compiled_date": "Wed 12-Aug-20 00:16",
"copyright_years": "1986-2020",
"curr_config_register": "0x2102",
"disks": {
"bootflash:.": {
"disk_size": "6188032",
"type_of_disk": "virtual hard disk"
}
},
"hostname": "csr1000v-1",
"image_id": "X86_64_LINUX_IOSD-UNIVERSALK9-M",
"image_type": "production image",
"label": "RELEASE SOFTWARE (fc3)",
"last_reload_reason": "reload",
"license_level": "ax",
"license_type": "N/A(Smart License Enabled)",
"location": "Amsterdam",
"main_mem": "715705",
"mem_size": {
"non-volatile configuration": "32768",
"physical": "3978420"
},
"next_reload_license_level": "ax",
"number_of_intfs": {
"Gigabit Ethernet": "3"
},
"os": "IOS-XE",
"platform": "Virtual XE",
"processor_type": "VXE",
"returned_to_rom_by": "reload",
"rom": "IOS-XE ROMMON",
"router_operating_mode": "Autonomous",
"rtr_type": "CSR1000V",
"system_image": "bootflash:packages.conf",
"uptime": "2 hours, 34 minutes",
"uptime_this_cp": "2 hours, 36 minutes",
"version": "17.3.1a",
"version_short": "17.3",
"xe_version": "17.03.01a"
}
}
最后
在实际环境中,虽然确认能够进行一些操作,但是需要考虑到认证、安全性和可用性等因素来进行实际应用的实现。
此外,如果不仅仅局限于使用Ansible,也可以使用NAPALM、Netmiko、ncclient和Python requests等工具通过API客户端来操作不支持REST API的网络设备。这些开源软件已经公开发布,也可以考虑使用它们。