【Nornir系列】网络设备管理模块 nornir系列填坑第二弹,欢迎大家帮我转发分享在看,把我的公众号推荐给你们身边的网络工程师,毕竟酒香也怕巷子深,这绝对是目前你能找到的中文乃至所有语种中nornir的最佳指引,在一些细节甚至是可以秒杀官方的,我配置了大量的代码和截图演示。后续将不断展开,挖坑填坑,我都是一把好手。

同时,笔者也在挖很多坑,比如试图给大家讲清楚netconf与yang model,录一个nornir的系列视频,大家尽请期待!

Inventory 主机清单,或者大家喜欢叫设备清单,好像没有一个特别准确事实标准的翻译。一个自动化框架里,或大或小,都需要有一个类似的设备管理模块,记录了我们维护的设备、型号、厂商、IP、用户名等等,在处理自动化的时候,通过设备名或者IP地址来对设备进行批量配置。

Nornir也有一个inventory的模块,它是有hosts(主机)、groups(组)和defaults(默认值)三部分组成,这三个概念的抽象以及用法的灵活,使nornir的设备清单管理异常强大,与网络的一些场景异常契合,简直秒杀ansible或者是其他的一些web自动化的框架。

我们今天先起个头,介绍一下这三个基本的概念及灵活使用,后续再写一篇灵活筛选管理设备的文章。

Inventory plugin类 Nornir加载出设备清单,需要用到一个inventory的plugin类,它主要就是实现一个返回Inventory对象的功能load。Inventory就是由hosts、groups、defaults三个变量进行初始化的一个类。

image-20201210183356707 如上,nornir内置了一个简单的主机清单类,它的方法是通过指定三个yaml文件(host_file group_file defaults_file),加载hosts、groups、defaults三个变量,然后返回一个Inventory类,进而实现设备的管理和灵活筛选,以及连接(ssh netconf等等)的建立。

image-20201210183905682 这个过程大家了解即可,有兴趣的,有能力去对接的,按照这个思路主机去开发,想办法重写load,就可以对接自己的cmdb或者是从表格文件中加载自己的文件,也可以兼容ansible的主机清单,昨天晚上对照着写了一个对接自家CMDB的一个inventory的plugin,通过dict的list加载设备清单,非常简单。

目录结构 一般config.yaml放在根目录,invetory加载方式如果是文件的方式,或者再具体点,就来说nornir内置的SimpleInventory,会在根目录创建一个inventory的文件夹,然后写三个文件,在config.yaml(这个文件名可以按需改)分别指向这三个文件的相对路径

image-20201211213026789 详解官方默认的SimpleInventory加载 hosts_file 它加载的方式完全是靠三个文件来进行加载,我们一个个的过以下。先来看看一个稍微复杂的hosts

---
spine00.bma: # 主机的名称,理解为设备名称或者是必须全局唯一,就是个名字,你用ip也是可以的
     hostname: 127.0.0.1 # ip地址或者fqdn
     username: vagrant # 用户名
     password: vagrant # 密码
     port: 12444 # 端口,这个要和你使用的connection方式对接,如果是ssh一般是22,如果是rest 可能是80或者443,和具体的连接的plugin关联
     platform: eos # 设备平台类型,兼容netmiko和napalm
     groups: # 设备分到那个组里,可以是多个组,这个就很灵活,可以以等保区划分,也可以以运维组划分,也可以按地域,且可以不同维度组合,能加的都加上,然后在筛选的时候可以多维度筛选,前提是自己能控制住,人一定要控制住自己的代码。不然还是按照一个维度去设计group。
         - bma
     data: # 设备的一些相关参数变量,比如定义备份的命令,也可以把运维组、等保区放到data,data本质是一个字典,也可以设置多个维度。比如下面的例子,写了这个设备的位置、角色、类型,大家发挥自己的想像力。
         site: bma
         role: spine
         type: network_device
 
spine01.bma:
     hostname: 127.0.0.1
     username: vagrant
     password: ""
     port: 12204
     platform: junos
     groups:
         - bma
     data:
         site: bma
         role: spine
         type: network_device
 
leaf00.bma:
     hostname: 127.0.0.1
     username: vagrant
     password: vagrant
     port: 12443
     platform: eos
     groups:
         - bma
     data:
         site: bma
         role: leaf
         type: network_device
 
leaf01.bma:
     hostname: 127.0.0.1
     username: vagrant
     password: wrong_password
     port: 12203
     platform: junos
     groups:
         - bma
     data:
         site: bma
         role: leaf
         type: network_device
	

nornir通过上一次分享的初始化方式把以上yaml文件内容加载到设备管理模块,这些网络设备每一个都是Host对象,Host对象大家可以通过内置的schema函数获取其比较易读的模式。

from nornir.core.inventory import Host
import json
print(json.dumps(Host.schema(), indent=4))

打印出来结果如下


{
    "name": "str",
    "connection_options": {
        "$connection_type": {
            "extras": {
                "$key": "$value"
            },
            "hostname": "str",
            "port": "int",
            "username": "str",
            "password": "str",
            "platform": "str"
        }
    },
    "groups": [
        "$group_name"
    ],
    "data": {
        "$key": "$value"
    },
    "hostname": "str",
    "port": "int",
    "username": "str",
    "password": "str",
    "platform": "str"
}

这个就非常容易看到Host的模式,比直接看源代码简单

  • 首先它有一个name,是str,也就是leaf01.bma这种给人看的名字

  • 我们跳过connection,看最底下的,hostname是ip地址或者fqdn域名,username等是登录的基础信息,密码就不说了,端口(如果是ssh就是写22,如果有些特殊的连接类,比如基于netconf的就写830等等),platform标明设备的平台,可以无缝对接netmiko与napalm,我们也可以根据自己需要,写一个自己的连接类,使用自己的platform规则。

  • nornir有相关的connection的类来处理和设备的连接,这个时候可以用以上的基本信息,也可以有连接用的专属参数字典,connection_options,里面有用户名、密码、端口、平台等等,以及可能用到的额外参数extras。我们在加载nornir的时候,每个设备会按需维系一个connection的连接对象,比如nornir_netmiko是实现了一个netmiko的连接,并可以按需保持一个长连接,或者按需关闭,适时发送命令行。napalm用到了自己的连接类,我。我们也可以按需创建自己的连接类,比如给予paramiko或者是pexpect去自己封装,如果你觉得netmiko太重,或者你有自己的想法。

连接类的connection_options不一定非要有数据,我们可以从基础信息里读取,也可以从connection_options的参数里读取,一切都是由具体的连接的plugin实现的,因插件而异。

  • groups是设备所属组的名字,是一个列表,这个功能很强大。host会自动继承自己本身没有的值,但是在group里定义了的值。

  • data里是我们在执行runbook时候可能用到的参数,是字典,vlaue可以是复杂结构,比如我们可以设置一个backup_cmds:['show run ','show ip route'],每次去批量备份一些基础信息,打一个快照类似的东西。甚至可以更复杂,按需。host会自动继承自己本身没有的值,group里也没定义,但是default里定义了的值。

在task中取host信息 这些信息在执行对应task的时候都可以按需取出来,非常方便,我们截图演示一下host。后续会在task进一步演示。

image-20201211210633540 我们想用name就在task里取到host属性,这就是一个Host对象了,然后继续取Host的属性,用标点符号点来取属性值。

image-20201211211756269 这些数据都可以取到,非常方便,所以我们可以按需在hosts及其所在groups以及defaults里获取,非常非常灵活。缺什么参数了,直接yaml里配置即可,然后继续task里写。

好了我们来看看groups_file

在nornir对象中取出来 我们可以在nornir的inventory属性中取到hosts,然后按照name可以取到对应的host对象,非常像一个字典但是不是字典。

  • 在继续取具体到一个主机的时候,我们取属性值的时候,也就是schema中返回的那些属性,我们用“点”,直接取,就是对象的取值方法。

  • 在取data里的数据的时候,我们可以直接按照字典的方式取出来 image-20201211235751460 实际使用中,nr.inventory.hosts的打印输出,只是调试中使用,真正的针对host的操作一般使用会分成两种情况:

  • 在task中提取host属性值进行判断,进而对不同设备进行不同操作。每个task就是对每台设备批量执行,我们可以在这里根据host的一些值进行判断控制逻辑,或者跳过指定设备或者对制定设备进行操作。

  • nornir是经过筛选后的nornir对象,先筛选出所有leaf交换机,返回一个nornir对象,然后批量操作,在这之上封装逻辑,这个我们最后会起个头,讲讲如何简单筛选。下一节会详细展开。 groups_file 这个文件是用来指定分组的,上述host的groups如果指定了,在这个文件里必须定义,请务必记住。

分组是用来维护一些组内共同的属性,比如按厂商分配,他们的vendor应该一致,比如按一些区域分,区域的名字一致,ntp,探针,syslogserver,地域位置属性等等可能都是一致的。这样不用每台设备取维护,只要放到对应组,在host里取出对应的group之后可以取出公共属性,比如vrf、vlan等等都可以。

我们先来看看schema

from nornir.core.inventory import Group
import json
print(json.dumps(Group.schema(), indent=4))

结果


{
    "name": "str",
    "connection_options": {
        "$connection_type": {
            "extras": {
                "$key": "$value"
            },
            "hostname": "str",
            "port": "int",
            "username": "str",
            "password": "str",
            "platform": "str"
        }
    },
    "groups": [
        "$group_name"
    ],
    "data": {
        "$key": "$value"
    },
    "hostname": "str",
    "port": "int",
    "username": "str",
    "password": "str",
    "platform": "str"
}

绝大部分与host一致,如果host里的数据配置为空,则会使用所属group的属性填充到设备Host的相关属性。

比如我们把某设备的相关属性去掉

image-20201211233557240 同时在某个group里赋值,比如赋值到demo_for_tree中

image-20201212000120705 但当我们断点取看这个设备的相关信息时(或者直接打印都可以),发现这些属性都被填充上了,awesome!

image-20201211233514375 我们的例子比较极端,一般不会把hostname也置空。

group的继承与冲突处理逻辑 Group也有一个groups的属性,这是一个group的继承关系,一个设备,所属group为bj_leaf,它有两个group值,bj和leaf,那它从地域上继承了bj这个group,从角色上继承了leaf,相关的信息都会继承到属于bj_leaf的所有的host上,同时官方给的例子是一个全局的global的组,某些可以继承global,global相当于父节点基类,继承了这个group的其子类group如果有字段和这个冲突,肯定是以子节点为准,如果设备上的属性与某group冲突,则以设备上的属性为准。

官方例子:


---
global:
    data:
        domain: global.local
        asn: 1

eu:
    data:
        asn: 65100

bma:
    groups:
        - eu
        - global

cmh:
    data:
        asn: 65000
        vlans:
          100: frontend
          200: backend

个人举例:

leaf:
  data:
    role: leaf
    chongtu_k: 1

bj:
  data:
    city: beijing
    chongtu_k: 2

bj_leaf:
  groups:
    - leaf
    - bj

我们在设计继承的时候,个人认为尽量不要冲突,或者是冲突可控,冲突可控遵循的逻辑是,后面的会覆盖前面的,我们按照这个顺序原则取写,也可以,合情合理,但是我个人不建议。

有冲突的时候我们打印设备的冲突的k值

image-20201211235138992 group与host的结合,如果在一个大型网络中,可以是一个非常复杂的结构,给了我们足够的空间和灵活度的时候,也考验我们是否能hold住,所以初学者尽量简单使用,循序渐进,有能力的就可以脑洞大开的组织你的设备清单。

defaults_file defaults就更加简单了,就是默认值,全局的默认值,比如port可以设置成22。

它的schema:


{
    "data": {
        "$key": "$value"
    },
    "connection_options": {
        "$connection_type": {
            "extras": {
                "$key": "$value"
            },
            "hostname": "str",
            "port": "int",
            "username": "str",
            "password": "str",
            "platform": "str"
        }
    },
    "hostname": "str",
    "port": "int",
    "username": "str",
    "password": "str",
    "platform": "str"
}

例子:

image-20201212000835784 像这样,写个默认值,就不用每个设备取写了,这个优先级肯定是最低的,host>group>default,一个值如果在多个中出现,是按照这个优先级采信,如果只出现在一个配置里,那肯定就是它了。

简单的搜索 我们加载的nornir对象默认是所有设备,但是实际操作中,我们有时候需要筛选,这个时候就用到了filter中的F类,它有点像django的filter,非常赞。我们直接上代码

image-20201212083135502 图上的示例中,我们筛选出了设备name包含了leaf的设备清单,然后进行了打印输出,实际我们一般对接的是操作,比如执行一个task。同时这个nornir的筛选可以支持俄罗斯套娃,层层筛选,功能很强大,后续章节分享。

小结 好了,以上就是今天我们所有的内容,主要分享了nornir的设备管理体系的概念和逻辑以及使用。

最后 欢迎大家继续关注、点赞、分享、喜欢、收藏、订阅,推荐给你身边的网工!

同名知乎专栏和微信公众号:NetDevOps加油站,欢迎你的加入!