Nornir一个纯Python的自动化框架,可以帮助我们管理众多的设备,我们可以直接使用python来编写自动化脚本,Nornir让你通过Python掌控一切;我们还可以很灵活地将Nornir与django,celery框架集成,打造强大的可视化自动化运维工具。
下面是我结合实际运维场景,把NetDevOps与《IP地址管理系统》深度融合,利用Nornir框架开发的高效自动化运维模块。该模块可以实现批量的设备巡检,同步网络设备路由表,并与子网台账进行比对,防止人工的遗漏和错误录入;采集ARP表,自动完善活动IP的二层MAC地址,监测子网中的IP使用情况,辅助子网分配与回收管理,实现IP地址的追踪和溯源,达到IP资源的精细化管理;还提供了设备配置备份,防止设备故障后的配置丢失;同时预留了任务定制功能,留待今后扩展。通过与celery和redis的集成,把这类耗时任务变成异步执行和周期性执行。到目前为止已经有近50台设备同时完成自动巡检任务,后期设备数量还会陆续增加,预计将达到300台左右,最终目标实现全网IP地址的监控;
本次需求是批量获取网关设备的ARP表,格式化后入库,并同步IP台账,更新MAC地址字段。要实现这个需求,需要解决几方面的问题,一是,批量管理和操作设备,我们的实际网络场景体量不小,网络设备上千台,而nornir框架可以实现大规模的批量并发操作,但是原生的nornir框架是采用yaml文件来管理设备的,因此需要对nornir的inventory插件进行改造,实现与数据库的对接;二是,适配网络设备,仅存在一两种类型网络设备的网络场景是不存在的,现实中的网络场景,设备都是五花八门的,不同厂商、不同类型、不同型号、不同的连接类型,比如同样的获取ARP表这个操作,在不同厂商的设备上,执行命令都有区别,show arp,show ip arp, display arp,show arp dynamic,甚至连接类型都有区别,有telnet,ssh和http,因此需要针对实际场景对不同厂商的设备进行适配;三是,格式化执行结果,不同厂商的设备回显数据的格式都不一样,数据需要格式化后才能进行入库操作。
首先,改造nornir的设备管理插件,实现与数据库的对接
# logger = logging.getLogger(__name__)
def _get_connection_options(data: Dict[str, Any]) -> Dict[str, ConnectionOptions]:
cp = {}
for cn, c in data.items():
cp[cn] = ConnectionOptions(
hostname=c.get("hostname"),
port=c.get("port"),
username=c.get("username"),
password=c.get("password"),
platform=c.get("platform"),
extras=c.get("extras"),
)
return cp
def _get_defaults(data: Dict[str, Any]) -> Defaults:
return Defaults(
hostname=data.get("hostname"),
port=data.get("port"),
username=data.get("username"),
password=data.get("password"),
platform=data.get("platform"),
data=data.get("data"),
connection_options=_get_connection_options(data.get("connection_options", {})),
)
def _get_inventory_element(
typ: Type[HostOrGroup], data: Dict[str, Any], name: str, defaults: Defaults
) -> HostOrGroup:
return typ(
name=name,
hostname=data.get("hostname") or data.get('ip'),
port=data.get("port"),
username=data.get("username"),
password=data.get("password"),
platform=data.get("platform"),
data=data.get("data"),
groups=data.get(
"groups"
), # this is a hack, we will convert it later to the correct type
defaults=defaults,
connection_options=_get_connection_options(data.get("connection_options", {})),
)
class DBInventory:
def __init__(
self,
devices
) -> None:
"""
devices为queryset类型参数,可以从数据库查询后,把结果直接给这个设备管理插件
:param devices:
"""
host_info = ['hostname', 'username', 'password', 'port', 'platform', 'ip', 'secret', 'device_type']
reshape_devices = []
// 对设备字段整型,除了连接需要的字段外,其余字段放入data中
for device in devices:
reshape_device = {
'data': {}
}
for k, v in device.items():
if k in host_info:
if k == 'device_type':
reshape_device['platform'] = v
else:
reshape_device[k] = v
else:
reshape_device['data'][k] = v
reshape_devices.append(reshape_device)
self.devices = reshape_devices
def load(self) -> Inventory:
defaults = Defaults()
groups = Groups()
hosts = Hosts()
hosts_dict = {device['ip']: device for device in self.devices}
for name, hosts_dict in hosts_dict.items():
hosts[name] = _get_inventory_element(Host, hosts_dict, name, defaults)
return Inventory(hosts=hosts, groups=groups, defaults=defaults)
from nornir import InitNornir
from nornir.core.plugins.inventory import InventoryPluginRegister
from web.scripts.cmdb_inventory_plugin_v1 import DBInventory
//注册自定义的设备管理插件
InventoryPluginRegister.register("db_inventory", DBInventory)
//创建nornir对象
def get_nornir_obj(devices_obj):
devices_obj_list = []
for dev in devices_obj:
dev_info = {
'ip': dev.ip_address,
'name': dev.device_name,
'port': dev.connection_type,
'username': dev.username,
'password': dev.password,
'device_type': dev.platform.title,
'secret': dev.enable_password,
'type': dev.device_type,
'model': dev.model,
'vendor': dev.vendor.title,
}
devices_obj_list.append(dev_info)
runner = {
'plugin': 'threaded',
'options': {
'num_workers': 100,
}
}
inventory = {
'plugin': 'db_inventory',
'options': {
'devices': devices_obj_list
}
}
nr = InitNornir(runner=runner, inventory=inventory)
return nr
其次,编写不同设备的ARP表解析模板,netmiko会利用TextFSM进行自动解析,返回一个字典数据,TextFSM由text(文本)和FSM(有限状态机)两部分组成,它是谷歌开源的一个用于解析半格式化文本的Python模块,它是为了解析通过cli驱动的网络设备信息而诞生的。
最后,实现业务逻辑
from web.scripts.get_nornir_obj import get_nornir_obj
from nornir_netmiko.tasks import netmiko_send_command
from web import models
import re
from datetime import datetime
import os
//不同设备的命令映射
show_arp_cmd_mapping = {
'cisco_nxos': 'show ip arp',
'cisco_ios_telnet': 'show ip arp',
'juniper_junos': 'show arp no-resolve',
'generic': 'show arp dynamic',
'generic_telnet': 'show arp dynamic',
'huawei_telnet': 'display arp',
'hp_comware_telnet': 'display arp'
}
def insert_update_arp_table(active_hosts, host):
regx = r'([a-f0-9]{4}[\.\-:]?){2}[a-f0-9]{4}|([a-f0-9]{2}[\.\-:]?){5}[a-f0-9]{2}'
regx_mac_address = r'([a-f0-9]{2}[:]?){5}[a-f0-9]{2}'
active_hosts_list = []
host_list = []
for item in active_hosts:
mac_address_str = item.get('mac_address')
if re.match(regx, mac_address_str, re.IGNORECASE):
if re.match(regx_mac_address, mac_address_str, re.IGNORECASE):
mac_address = mac_address_str
else:
mac_address_temp = mac_address_str.replace('.', '').replace(':', '').replace('-', '')
mac_address = ':'.join([mac_address_temp[i:i + 2] for i in range(0, len(mac_address_temp), 2)])
access_port = item.get('access_port')
ip_address = item.get('ip_address')
host_obj = models.Hosts.objects.filter(ip_address=ip_address).first()
if host_obj:
if not (host_obj.mac_address == mac_address and host_obj.status == 1):
host_obj.status = 1
host_obj.mac_address = mac_address.upper()
host_list.append(host_obj)
current_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
temp_obj = models.HostOnlineRecord(
ip=host_obj,
ip_address=ip_address,
mac_address=mac_address.upper(),
online_time=current_date,
scan_time=current_date,
scan_time_datetime=datetime.now(),
access_switch=host.data.get('name'),
access_port=access_port
)
active_hosts_list.append(temp_obj)
host_list.append(host_obj)
models.HostOnlineRecord.objects.bulk_create(active_hosts_list, batch_size=100)
models.Hosts.objects.bulk_update(host_list, ['status', 'mac_address'], batch_size=100)
def get_arp_table_with_netmiko(task_context):
devices_obj = models.Devices.objects.filter(ip_address=task_context.host.name).first()
inspect_count = devices_obj.inspect_count + 1
start_time = datetime.now()
platform = task_context.host.platform
model = task_context.host.data.get('model')
vendor = task_context.host.data.get('vendor')
cmd = show_arp_cmd_mapping.get(platform)
if platform == 'generic':
platform = vendor
templates = 'web/scripts/templates/%s_%s.textfsm' % (platform, '_'.join(cmd.split(' ')))
if not os.path.exists(templates):
templates = 'web/scripts/templates/%s_%s_%s.textfsm' % (platform, model, '_'.join(cmd.split(' ')))
result = task_context.run(netmiko_send_command, command_string=cmd, use_textfsm=True,
textfsm_template=templates)
if isinstance(result.result, str):
devices_obj.begin_inspect_time = start_time
devices_obj.end_inspect_time = datetime.now()
devices_obj.insepct_status = 2
devices_obj.exception_cause = '无法解析数据格式'
devices_obj.save()
elif isinstance(result.result, list):
insert_update_arp_table(result.result, task_context.host)
end_time = datetime.now()
if not result.failed:
devices_obj.begin_inspect_time = start_time
devices_obj.end_inspect_time = end_time
devices_obj.insepct_status = 1
devices_obj.inspect_count = inspect_count
devices_obj.exception_cause = ''
devices_obj.save()
return result
def collection_arp_table(queryset):
nr = get_nornir_obj(queryset)
result = nr.run(task=get_arp_table_with_netmiko)
for host, multiresult in result.failed_hosts.items():
if len(multiresult) > 1:
faild_info = '%s:%s' % (str(multiresult[0].exception).strip(), str(multiresult[1].exception).strip())
else:
faild_info = '%s' % str(multiresult[0].exception).strip()
models.Devices.objects.filter(ip_address=host).update(
exception_cause=faild_info,
insepct_status=2
)
自动化框架究竟在做什么呢?框架可以把一些重复的事情或者说大家都要做的事情抽象成功能模块,让开发者去调用。比如对设备的管理,不用自己去定义Python对象,而是通过主机清单管理插件(inventory plugin)去管理设备。然后在任务模块中编排任务剧本(runbook),比如哪些设备要获取路由表,哪些设备要获取接口表,哪些设备要获取版本号等等,从而让开发者更专注于网络业务逻辑,包括连接设备的时候不用再去调用netmiko,传入用户名和密码等参数(ssh,telnet连接参数都在设备清单里),也不用维护设备的连接池,何时连接设备何时断开设备都由自动化框架完成。同时自动化框架还可以实现高效的并发操作,可以同时对1到N台设备进行批量操作,使运维的生产力大大提升。总之自动化运维框架让开发者更专注于网络业务逻辑的的实现。下面是Nornir工作的简化流程图。
一、设备管理
网络设备数量多,分布地域广,inventory插件可以根据设备类型,设备位置,连接类型等参数,快速从设备清单中把设备筛选出来。
二、执行与并发
task模块对筛选出来的设备执行任务,通过runner插件实现批量的逻辑,在nornir中默认是多线程;
三、逻辑控制
模块内可以进行单台设备的逻辑控制,整个runbook可以通过Python自己的逻辑进行控制;
四、设备连接
支持N种连接模块,包括netmiko,napalm,nclient等,还可以自己造轮子,只需配置基本信息,Nornir帮我们自动完成到设备的连接(ssh,telnet,api,netconf),维护连接池,最大限度减少连接的开销。
五、执行结果
任务执行完成后会有丰富的结果展示或者内置对象的表达,即方便运行,又方便开发调试;