网络自动化运维利器-Nornir

     Nornir一个纯Python的自动化框架,可以帮助我们管理众多的设备,我们可以直接使用python来编写自动化脚本,Nornir让你通过Python掌控一切;我们还可以很灵活地将Nornir与django,celery框架集成,打造强大的可视化自动化运维工具。
     下面是我结合实际运维场景,把NetDevOps与《IP地址管理系统》深度融合,利用Nornir框架开发的高效自动化运维模块。该模块可以实现批量的设备巡检,同步网络设备路由表,并与子网台账进行比对,防止人工的遗漏和错误录入;采集ARP表,自动完善活动IP的二层MAC地址,监测子网中的IP使用情况,辅助子网分配与回收管理,实现IP地址的追踪和溯源,达到IP资源的精细化管理;还提供了设备配置备份,防止设备故障后的配置丢失;同时预留了任务定制功能,留待今后扩展。通过与celery和redis的集成,把这类耗时任务变成异步执行和周期性执行。到目前为止已经有近50台设备同时完成自动巡检任务,后期设备数量还会陆续增加,预计将达到300台左右,最终目标实现全网IP地址的监控;


自动化运维模块

设备管理模块

并发执行任务

2.png

子网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驱动的网络设备信息而诞生的。

arp表解析模板文件

arp表解析正则表达式

最后,实现业务逻辑

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工作的简化流程图。


Nornir工作流程
一、设备管理

    网络设备数量多,分布地域广,inventory插件可以根据设备类型,设备位置,连接类型等参数,快速从设备清单中把设备筛选出来。

二、执行与并发

    task模块对筛选出来的设备执行任务,通过runner插件实现批量的逻辑,在nornir中默认是多线程;

三、逻辑控制

    模块内可以进行单台设备的逻辑控制,整个runbook可以通过Python自己的逻辑进行控制;

四、设备连接

    支持N种连接模块,包括netmiko,napalm,nclient等,还可以自己造轮子,只需配置基本信息,Nornir帮我们自动完成到设备的连接(ssh,telnet,api,netconf),维护连接池,最大限度减少连接的开销。

五、执行结果

    任务执行完成后会有丰富的结果展示或者内置对象的表达,即方便运行,又方便开发调试;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容