[toc]

NAPALM概述

官方链接

其他drive链接(国人)

NAPALM全称为Network Automation and Programmability Abstraction Layer with Multivendor support,翻译起来就是支持"多厂商"网络自动化和可编程的抽象层.

NAPALM是一个python开源的第三方模块,截至2021.11月,支持的厂商如下所示:

虽然支持厂商不是很多,暂时没有支持国内厂商,但功能都是很强大的。

Cisco IOS
Cisco NX-OS
Cisco IOS-XR
Juniper JUNOS
Arista EOS

NX-API support on the Nexus 5k, 6k and 7k families was introduced in version 7.2

  • 依赖netmiko,本机安装即可.

支持的设备

通用支持模型

_EOSJunosIOS-XR (NETCONF)IOS-XR (XML-Agent)NX-OSNX-OS SSHIOS
Driver Nameeosjunosiosxr_netconfiosxrnxosnxos_sshios
Structured dataYesYesYesNoYesNoNo
Minimum version4.15.0F12.17.05.1.06.1 [1]12.4(20)T6.3.2
Backend librarypyeapijunos-ezncncclientpyIOSXRpynxosnetmikonetmiko
CaveatsEOS IOS-XR (NETCONF) NXOSNXOSIOS

说明:

  • driver name:后面我们写代码需要填写的,如get_network_driver(ios)
  • structured data:支持的结构化数据
  • Minimum version:这里还有最低的版本要求;

配置支持模型

支持配置替换、合并、提交、对比、原子配置、回滚等。
_EOSJunosIOS-XR (NETCONF)IOS-XR (XML-Agent)NX-OSIOS
Config. replaceYesYesYesYesYesYes
Config. mergeYesYesYesYesYesYes
Commit ConfirmYesYesNoNoNoNo
Compare configYesYesYesYes [2]Yes [4]Yes
Atomic ChangesYesYesYesYesYes/No [5]Yes/No [5]
RollbackYes [3]YesYesYesYes/No [5]Yes

[2] Hand-crafted by the API as the device doesn't support the feature.
[3] Not supported but emulated. Check caveats.
[4] For merges, the diff is very simplistic. See caveats.
[5] (1, 2, 3) No for merges. See caveats.
[6] NAPALM requires Junos OS >= 14.1 for commit-confirm functionality.

Getters支持的模型

该表官方会不定期自动更新。
EOSIOSIOSXRIOSXR_NETCONFJUNOSNXOSNXOS_SSH
get_arp_table
get_bgp_config
get_bgp_neighbors
get_bgp_neighbors_detail
get_config
get_environment
get_facts
get_firewall_policies
get_interfaces
get_interfaces_counters
get_interfaces_ip
get_ipv6_neighbors_table
get_lldp_neighbors
get_lldp_neighbors_detail
get_mac_address_table
get_network_instances
get_ntp_peers
get_ntp_servers
get_ntp_stats
get_optics
get_probes_config
get_probes_results
get_route_to
get_snmp_information
get_users
get_vlans
is_alive
ping
traceroute
  • ✅ - supported
  • ❌ - not supported
  • ☠ - broken

通过Getters类,比如get_config就可以得到设备的配置(running和startup)、get_facts获取设备的基本信息、get_interfaces获取接口信息等等,也是非常方便的。

其他方法

_EOSJunosIOS-XR (NETCONF)IOS-XRNX-OSIOS
load_template
ping
traceroute

可用的配置模板

  • set_hostname (JunOS, IOS-XR, IOS) - Configures the hostname of the device.
  • set_ntp_peers (JunOS, IOS-XR, EOS, NXOS, IOS) - Configures NTP peers of the device.
  • delete_ntp_peers (JunOS, IOS-XR, EOS, NXOS, IOS): Removes NTP peers from device’s configuration.
  • set_probes (JunOS, IOS-XR): Configures RPM/SLA probes.
  • schedule_probes (IOS-XR): On Cisco devices, after defining the SLA probes, it is mandatory to schedule them. Defined also for JunOS as empty template, for consistency reasons.
  • delete_probes (JunOS, IOS-XR): Removes RPM/SLA probes.

以上方法,大家可以在实验环境测试下。

如何安装

需要确保netmiko已经被安装,依赖与netmiko的。

  • 通过pip3安装
python -m pip install napalm

备注:从版本3.0.0之后,NAPALM仅支持Python 3.6+.

如何升级

pip install napalm -U

备注:如果napalm有升级了,可以通过该方法进行升级.

实操示例

视频中演示的代码,都在这里了.

基本的设备连接

from napalm import get_network_driver
import pprint

pp = pprint.PrettyPrinter(indent=2)

# drive是一个类class,传入drive_name
drive = get_network_driver('ios')
# 类实例化
conn = drive(hostname='192.168.0.20',
             username='cisco',
             password='cisco',
             optional_args={'port': 22})
# 建立连接会话
conn.open()
# 使用其中一个方法,获取接口ip地址
ouput = conn.get_interfaces_ip()
# 打印结果
pp.pprint(ouput)

# 关闭连接
conn.close()

执行脚本后,结果如下所示:

备份配置

from napalm import get_network_driver

import pprint
import os

pp = pprint.PrettyPrinter(indent=2)

host = "192.168.0.20"

# drive是一个类class
drive = get_network_driver('ios')
# 类实例化
conn = drive(hostname= host,
             username= 'cisco',
             password= 'cisco',
             optional_args= {'port': 22})
# 建立连接会话
conn.open()
# 获取设备配置信息
output = conn.get_config()
# 打印结果
pp.pprint(output)
running_config = output['running']
# 写入文件
with open(os.path.join('LOG', f'{host}-running.conf'), 'w+') as f:
    f.writelines(running_config)

# 关闭连接
conn.close()

执行完脚本,结果如下所示:

默认包含了3个备份配置:

  • running配置:就是当前运行配置.
  • startup配置:就是启动配置了.
  • candidate配置,这个一般为空.

这里,我保存了running.config配置文件.

获取设备基础信息

# drive是一个类class
drive = get_network_driver('ios')
# 类实例化
conn = drive(hostname='192.168.0.20',
             username='cisco',
             password='cisco',
             optional_args={'port': 22})
# 建立连接会话
conn.open()
# 获取接口ip地址
output = conn.get_facts()
# 打印结果
pp.pprint(ouput)
# 关闭连接
conn.close()

执行脚本后,结果如下所示:

get_facts()方法采集了如下字段:

  • 域名、主机名称、接口列表、型号、版本号、序列号、运行时间、厂商.

发送命令

# drive是一个类class
drive = get_network_driver('ios')
# 类实例化
conn = drive(hostname='192.168.0.20',
             username='cisco',
             password='cisco',
             optional_args={'port': 22, 'secret': 'cisco'})
# 建立连接会话
conn.open()
# 发送命令
output = conn.cli(['show ip int brief', 'show arp'])
pp.pprint(output)

# 关闭连接
conn.close()

执行脚本后,结果如下所示:

说明:这里要留意下如果用户权限不够或者需要进入enable的,需要optional_args增加secret参数,表明需要进入特权模式了.

如果执行的命令权限不允许,就会报如下错误:

ValueError: Failed to enter enable mode. Please ensure you pass the 'secret' argument to ConnectHandler.

配置类

通过scp协议下发配置,它不需要enable密码.

前提条件:

  • 设备要开启scp服务, ip scp server enable .
  • 开启archive服务,备份配置到本地flash上;

    archive
     path flash:R1.conf
     write-memory

合并配置

说明:

  • load_merge_candidate():加载配置文件.
  • commit_config():提交配置.
  • revert_in=30:如果平台支持,可以设置时间挂起配置提交,超时没commit就恢复配置。

    基础玩法

    模板文件路径:templates\ios_logging_config.cfg

    logging host 10.1.1.2
from napalm import get_network_driver
import pprint

pp = pprint.PrettyPrinter(indent=2)

# drive是一个类class
drive = get_network_driver('ios')
# 类实例化
conn = drive(hostname='192.168.0.20',
             username='cisco',
             password='cisco',
             optional_args={'port': 22})
# 建立连接会话
conn.open()
try:
    conn.load_merge_candidate(filename='templates/ios_logging_config.cfg')
    conn.commit_config()
except Exception as e:
    print(e)    

如果设备上没开启scp服务就会报错:

SCP file transfers are not enabled. Configure 'ip scp server enable' on the device.

高级一点玩法:

模板文件路径:templates\ios_logging_config.cfg

def merge_config(vendor, devices, template_file):
    try:
        # drive是一个类class
        drive = get_network_driver(vendor)
        # 类实例化
        conn = drive(**devices)
        # 建立连接会话
        conn.open()
    except Exception as e:
        print("连接错误:{}".format(e))
        return

    try:
        conn.load_merge_candidate(filename=template_file)
        new_config = conn.compare_config()
        if new_config:
            print('预推送的配置如下:')
            print('='*80)
            print(new_config)
            print('=' * 80)

            choice = input("你要开始推送这些配置嘛, [Y/N]: ")
            if choice.lower() == 'y':
                print('开始提交配置,请稍后...')
                conn.commit_config()
                rollback = input("是否需要回滚配置,输入Y:回滚, 输入N,不回滚. [Y/N]")
                if rollback.lower() == 'y':
                    print('开始回滚配置,请稍后...')
                    conn.rollback()
                    print('配置已回滚,请检查配置.')
                else:
                    print('配置已经下发成功.')
            else:
                conn.discard_config()
                print('本次预配置没有推送.')
        else:
            print('无需重复配置.')

    except Exception as e:
        print(e)
    finally:
        conn.close()

if __name__ == '__main__':
    vendor = 'ios'
    devices = {'hostname': '192.168.0.20',
               'username': 'cisco',
               'password': 'cisco',
               'optional_args':{'port': 22}
               }
    tmp = 'templates/ios_logging_config.cfg'

    merge_config(vendor, devices, template_file=tmp)

配置替换

方法:load_replace_candidate()

def replace_config(vendor, devices, template_file):
    try:
        # drive是一个类class
        drive = get_network_driver(vendor)
        # 类实例化
        conn = drive(**devices)
        # 建立连接会话
        conn.open()
    except Exception as e:
        print("连接错误:{}".format(e))
        return

    try:
        if not (os.path.exists(template_file) and os.path.isfile(template_file)):
            msg = '文件不存在或文件类型不可用.'
            raise ValueError(msg)

        print("开始加载候选替换配置...")
        # 这个还未加载到设备上的
        conn.load_replace_candidate(template_file)
        # 配置比较
        print("\n预览配置对比:")
        print(">"*80)
        compare_result = conn.compare_config()
        print(compare_result)
        print(">" * 80)

        # You can commit or discard the candidate changes.
        if compare_result:
            try:
                choice = input("\nWould you like to commit these changes? [yN]: ").lower()
            except NameError:
                choice = input("\nWould you like to commit these changes? [yN]: ").lower()
            if choice == "y":
                print("Committing ...")
                conn.commit_config()
            else:
                print("Discarding ...")
                conn.discard_config()
        else:
            print('没有新的配置.')
            conn.discard_config()

        conn.close()

        print("Done...")

    except Exception as e:
        print(e)

if __name__ == '__main__':
    vendor = 'ios'
    devices = {'hostname': '192.168.0.20',
               'username': 'cisco',
               'password': 'cisco',
               'optional_args':{'port': 22}
               }
    tmp = 'LOG/192.168.0.20-running.conf'

    replace_config(vendor, devices, template_file=tmp)

说明:这里我用思科的设备,注意的是,如果running的配置里使用了banner,我是都提前把它删除了,不然会报错,这块我暂时没测试出来,如果哪位小伙伴试出来了,请告知我,谢谢。

Jinja2模板

  • 安装jinja2

    python -m pip install jinja2
  • jiaja2模板配置
    模板文件路径:templates/ssh-acl.tpl

    {% for host in hosts -%}
        access-list 20 permit host {{ host }}
    {% endfor -%}
    line vty 0 4
        access-class 20 in
  • 下发配置

    from napalm import get_network_driver
    from jinja2 import FileSystemLoader, Environment
    
    import pprint
    import logging
    
    pp = pprint.PrettyPrinter(indent=2)
    
    def connect_device(vendor, devices, hosts):
        try:
            # drive是一个类class
            drive = get_network_driver(vendor)
            # 类实例化
            conn = drive(**devices)
            # 建立连接会话
            conn.open()
    
        except Exception as e:
            print("连接错误:{}".format(e))
            return
    
        try:
            loader = FileSystemLoader('templates')
            env = Environment(loader=loader)
            tpl = env.get_template('ssh-acl.tpl')
            config_tpl = tpl.render({'hosts': hosts})
            # print(config_tpl)
            conn.load_merge_candidate(config=config_tpl)
            conn.commit_config()
    
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    if __name__ == '__main__':
        vendor = 'ios'
        devices = {'hostname': '192.168.0.20',
                   'username': 'cisco',
                   'password': 'cisco',
                   'optional_args':{'secret': 'cisco', 'port': 22}  # 如果enable需要密码,加入'secret'参数
                   }
        hosts = ['192.168.0.1', '192.168.0.254']
        connect_device(vendor, devices, hosts)

    这里可以打印下config_tpl,看看下发的配置是啥:

    # print(config_tpl)的输出结果如下所示:
    access-list 20 permit host 192.168.0.1
    access-list 20 permit host 192.168.0.254
    line vty 0 4
        access-class 20 in

配置更改回滚

conn.rollback()

备注:见前面章节演示.

验证部署

验证设备的状态是否是你期望的.
  • 定义一个yaml文件,定义预期想要的状态

    模板文件路径:templates/napalm_t.yaml

    ---
    - get_facts:
        'serial_number': '99CBK1M35C217V5Z7ABWQ'
    - get_interfaces_ip:
        GigabitEthernet0/0:
          ipv4: 192.168.0.20
  • compliance_report方法

    import pprint
    from napalm import get_network_driver
    
    pp = pprint.PrettyPrinter(indent=2)
    
    def connect_device(vendor, devices, hosts):
        try:
            # drive是一个类class
            drive = get_network_driver(vendor)
            # 类实例化
            conn = drive(**devices)
            # 建立连接会话
            conn.open()
    
        except Exception as e:
            print("连接错误:{}".format(e))
            return
    
        try:
            out = conn.compliance_report('templates/napalm_t.yaml')
            pp.pprint(out)
    
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    if __name__ == '__main__':
        vendor = 'ios'
        devices = {'hostname': '192.168.0.20',
                   'username': 'cisco',
                   'password': 'cisco',
                   'optional_args':{'port': 22}
                   }
        hosts = ['192.168.0.1', '192.168.0.254']
        connect_device(vendor, devices, hosts)

    执行结果如下所示:

    { 'complies': True,
      'get_facts': { 'complies': True,
                     'extra': [],
                     'missing': [],
                     'present': { 'serial_number': { 'complies': True,
                                                     'nested': False}}},
      'get_interfaces_ip': { 'complies': True,
                             'extra': [],
                             'missing': [],
                             'present': { 'GigabitEthernet0/0': { 'complies': True,
                                                                  'nested': True}}},
      'skipped': []}

    说明:complies的value值为True说明正常,如果False表示和预期设想不太一致。

支持上下文管理

通过上下文管理就不需要调用open()和close()方法了.
# 类实例化
drive = get_network_driver('ios')
# with上下文管理
with drive(hostname='192.168.0.20',username='cisco',password='cisco',optional_args={'port': 22}) as conn:
    output = conn.get_facts()
    # 打印结果
    pp.pprint(ouput)

ping方法

默认格式:ping x.x.x.x timeout 2 size 100 repeat 5

#!/usr/bin/env python3
#-*- coding:UTF-8 -*-

from napalm import get_network_driver
import pprint
import logging

# logging.basicConfig(filename='debug.log', level=logging.DEBUG)
# logger = logging.getLogger('napalm')

pp = pprint.PrettyPrinter(indent=2)

def connect_device(vendor, devices, dst_ip):
    try:
        # drive是一个类class
        drive = get_network_driver(vendor)
        # 类实例化
        conn = drive(**devices)
        # 建立连接会话
        conn.open()

    except Exception as e:
        print("连接错误:{}".format(e))
        return

    try:
        for ip in dst_ip:
            output = conn.ping(ip)
            pp.pprint(output)

    except Exception as e:
        print(e)
    finally:
        conn.close()

if __name__ == '__main__':
    vendor = 'ios'
    devices = {'hostname': '192.168.0.20',
               'username': 'cisco',
               'password': 'cisco',
               'optional_args':{'port': 22}
               }
    dst_ip = ['192.168.0.19', '192.168.0.20']
    connect_device(vendor, devices, dst_ip)

回显的结果:

{ 'success': { 'packet_loss': 0,
               'probes_sent': 5,
               'results': [ {'ip_address': '192.168.0.19', 'rtt': 0.0},
                            {'ip_address': '192.168.0.19', 'rtt': 0.0},
                            {'ip_address': '192.168.0.19', 'rtt': 0.0},
                            {'ip_address': '192.168.0.19', 'rtt': 0.0},
                            {'ip_address': '192.168.0.19', 'rtt': 0.0}],
               'rtt_avg': 2.0,
               'rtt_max': 3.0,
               'rtt_min': 2.0,
               'rtt_stddev': 0.0}}
{ 'success': { 'packet_loss': 0,
               'probes_sent': 5,
               'results': [ {'ip_address': '192.168.0.20', 'rtt': 0.0},
                            {'ip_address': '192.168.0.20', 'rtt': 0.0},
                            {'ip_address': '192.168.0.20', 'rtt': 0.0},
                            {'ip_address': '192.168.0.20', 'rtt': 0.0},
                            {'ip_address': '192.168.0.20', 'rtt': 0.0}],
               'rtt_avg': 1.0,
               'rtt_max': 2.0,
               'rtt_min': 1.0,
               'rtt_stddev': 0.0}}    

好了,文章的讲解就到此结束了,各个功能已经给大家演示了,各位小伙伴如实操一遍,相信就能掌握了并根据需求进行改写了。

如果需要更详细的信息,请移动官方进行查阅。

别忘了,动动手分享下,点击、收藏、三连击! 哈哈...

如果喜欢的我的文章,欢迎关注我的公众号:点滴技术,扫码关注,不定期分享

公众号:点滴技术


singvis
7 声望1 粉丝