Nornir – 探索Python自动化框架

该博客由Patrick Ogenstad提供。Patrick在瑞典的思科金牌合作伙伴Conscia Netsafe工作。他还在博客Networklore上发表了有关自动化和开发的文章。 

不久前,一位客户问我:“我猜你在Python多重处理方面很擅长吗?” 我以为他的目标不是评估我的技能。我没有回答,而是问了一个自己的问题。“您要解决什么问题?”

原来,他有上千个设备,需要从每个设备收集数据。由于从每台设备连接和收集信息需要一到两秒钟,因此运行整个task要花费几个小时,因此他需要添加并行化。我从北欧神话中告诉他有关诺恩人的信息。他们旋转连接所有众生的命运线。

我问他,他的目标是否是要拥有Nornir的功能,或者他是否想要关于Python多处理的讲座。当他保持沉默时,我告诉了他有关Nornir自动化框架的信息。

Python自动化框架

足够的戏剧。什么是Nonir?简而言之,Nornir是具有inventory管理功能的可插入多线程框架,可帮助操作设备集合。它与其他自动化框架的不同之处在于,它直接从Python使用。将此与Ansible进行比较,在Ansible中您可以基于YAML使用DSL(特定于域的语言)编写剧本。

对于介绍中的朋友来说,这不是问题。当他要求提供Python解决方案时,Nornir非常适合。

有一个神话认为编写代码很难,它对其他人有意义,但对您却没有意义。尽管掌握起来可能很困难并且需要数年时间,但目标应该是学习足够的知识以帮助您完成工作并对此感到满意。

Nornir会做什么?

Nornir处理数据收集。它针对此数据运行task,并跟踪所有线程。在网络环境中,这通常意味着您拥有一个设备inventory,其中包含与每个节点关联的数据。您可以定义task,并且这些task可以包含社区共享的插件或您的自定义插件或python代码。然后,Nornir将所有内容捆绑在一起,并让您针对所有或部分设备(处理数据,并行化并为您跟踪错误)运行这些task。

您与Nornir的首次互动

像大多数Python软件包一样,您可以使用pip安装Nornir。

pip install nornir

使用Nornir之前需要做的第一件事是inventory。从最基本的形式看,Nornirinventory是包含一个或多个host的Python字典。(可选)您可以使用第二个字典来定义组,因为您可以想象这可以使用任何源作为inventory的输入。为简化起见,Nornir包括一些inventory插件,默认插件是使用YAML文件的SimpleInventory。让我们创建一些示例host和组。


hosts.yaml:

---
al-rtr-1:
    site: alderaan
    groups:
      - routers
    loopback0: 10.230.12.1

en-rtr-1:
    site: endor
    groups:
      - routers
    nornir_username: wicket
    nornir_password: G0lden_god
    loopback0: 10.230.12.2
    nornir_host: 127.0.0.1

ta-rtr-1:
    site: tatooine
    groups:
      - routers
    loopback0: 10.230.12.3

ta-sw-1:
    site: tatooine
    groups:
      - switches

ta-sw-2:
    site: tatooine
    groups:
      - switches

大多数情况下,每个host下的密钥可以是任何东西。但是,您必须遵守一个限制。groups键必须是一个列表,但是groups的使用是可选的。您可能还已经注意到,其中一台host已定义了用户名和密码。Nornir附带的插件使用其中一些变量。如果像上面这样用明文输入密码的想法使您感到恐惧,请不要担心,这仅是示例。您可以从您选择的秘密存储中加载密码。\


groups.yaml

---
defaults:
    nornir_username: luke
    nornir_password: blue_saber
    domain: empire.space
    contact: Unknown

routers:
    nornir_username: han
    nornir_password: I_shot_first
    nornir_nos: iosxr
    contact: Router team

switches:
    nornir_nos: ios
    contact: Switch group

拥有这些文件后,您可以使用InitNornir函数使用Nornir进行浏览,这是使用合理默认值作为快速入门。

from nornir.core import InitNornir

nr = InitNornir()

您可以看到nornir_username已分配给每个host的username属性。您在“默认”组中设置的任何内容都将应用于所有host。您可以在组或host级别覆盖每个属性的值。您可以通过Host对象访问inventory中定义的大多数数据,就像访问Python字典中的键一样。例如,上面的“联系”键。从初始测试中可以看到,用户名值可以作为属性或作为字典中的键来访问。

>>> nr.inventory.hosts

{'al-rtr-1': Host: al-rtr-1, 'en-rtr-1': Host: en-rtr-1, 'ta-rtr-1': Host: ta-rtr-1,
 'ta-sw-1': Host: ta-sw-1, 'ta-sw-2': Host: ta-sw-2}

>>> nr.inventory.hosts['al-rtr-1'].username
'han'
>>> nr.inventory.hosts['en-rtr-1'].username
'wicket'
>>> nr.inventory.hosts['ta-sw-1'].username
'luke'
>>>

>>> nr.inventory.hosts['ta-sw-1']['nornir_username']
'luke'
>>>

>>> nr.inventory.hosts['ta-sw-1'].nos
'ios'
>>> nr.inventory.hosts['ta-sw-1']['contact']
'Switch group'
>>>

在现实世界的另一个线程中,我的朋友没有以SimpleInventory格式定义的inventory。他所拥有的只是一个包含10,000个设备的文本文件,或者更具体地说是mac地址。一种选择是将文本文件转换为YAML格式。
另一种选择是读取内容并将其直接发送到基本Inventory类。这种方法的结果是我们不能再使用InitNornir函数,而不得不初始化一些不同的代码。不过,不必担心,这可以被认为是高级话题,我在这里仅展示它是为了说明在Nornir中扩展功能是多么容易。

from nornir.core import Nornir
from nornir.core.configuration import Config
from nornir.core.inventory import Inventory


with open("source_mac.txt", 'r') as fs:
    hosts = {
        h: {
            'mac': h
        } for h in fs.read().splitlines()
    }


inv = Inventory(hosts=hosts)
conf = Config(num_workers=100)
nr = Nornir(inventory=inv, dry_run=False, config=conf)

source_mac.txt只是每行包含一个mac地址的文件。source_mac.txt的子集如下所示:

B0:98:2B:76:F3:E9
B0:98:2B:76:F9:C3
B0:98:2B:76:F5:94
B0:98:2B:76:ED:98
B0:98:2B:76:F1:5E
B0:98:2B:76:FA:56
B0:98:2B:76:FB:0C
B0:98:2B:76:F3:79
B0:98:2B:76:EF:12

编写Nornir应用程序

并不是说探索inventory的内部细节不是有教益的,但是除非您可以运行task,否则它没有什么帮助。返回星系超过15跳远,我们所有的host都属于一个特定站点。每个站点内的所有host共享公共数据,因此让我们为站点定义数据源。
endor.yaml

---
asn: 66401
networks:
  - net: 10.12.0.0
    mask: 255.255.0.0
  - net: 10.16.0.0
    mask: 255.255.0.0
  - net: 10.192.25.0
    mask: 255.255.255.0

tatooine.yaml

---
asn: 66801
networks:
  - net: 192.168.23.0
    mask: 255.255.255.0
  - net: 192.168.24.0
    mask: 255.255.255.0
  - net: 192.168.38.0
    mask: 255.255.255.0

我知道您在想什么,为什么他们不使用IPv6?请记住,所有这些都是很久以前的。我们的首要目标是从这些文件中加载数据,以便将其与该站点的每个host关联。我们将使用一个插件来读取Nornir中的YAML文件。但是,我们确实需要编写一些代码以将其捆绑在一起。让我们来看看!

from nornir.core import InitNornir
from nornir.plugins.tasks.data import load_yaml
from nornir.plugins.functions.text import print_result


def load_data(task):
    data = task.run(
           task=load_yaml,
           file=f'{task.host["site"]}.yaml'
    )

    task.host["asn"] = data.result["asn"]
    task.host["networks"] = data.result["networks"]


nr = InitNornir()
r = nr.run(task=load_data)
print_result(r)

这里介绍了一些新事物。load_yamltask插件,该插件从YAML文件读取数据。我们创建了一个load_datatask,该task又将load_yaml用作子task。目标是从与每个host关联的站点相对应的文件中加载数据。请注意Python 3.6中引入的f字符串。我们保存站点文件的内容,并将值存储在每个host上。最后一步,我们使用print_result函数查看会发生什么。

看起来Alderaan的站点文件丢失了,太糟糕了。默认情况下,Nornir并不特别在意Alderaan的命运。如果您有不同的感觉,则可能是因为某些问题而导致Nornir引发异常。

呈现配置

现在我们已经有了一些主机数据,无论如何,它们中的大多数还是可以开始做更多有趣的事情。让我们将此数据与Jinja2模板引擎一起使用。Nornir当前包括两个使用Jinja2渲染模板的插件,一个用于字符串,另一个用于文件。如果您想使用Mako或Velocity引擎,那么只需创建一个插件就足够简单了。
router bgp {{ asn }}
 bgp router-id {{ loopback0 }}
 address-family ipv4 unicast
{% for n in networks %}
  network {{ n.net }} {{ n.mask }}
{% endfor %}

细心的读者可能会注意到,我们正在引用模板中的loopback0变量。我们仅为路由器组中的host定义了此变量,因此如果我们尝试为交换机呈现此模板,则该变量将无效。为了确保我们仅生成路由器组的配置,我们可以使用过滤器功能。

from nornir.core import InitNornir
from nornir.plugins.tasks.data import load_yaml
from nornir.plugins.tasks.text import template_file
from nornir.plugins.functions.text import print_result


def load_data(task):
    data = task.run(
           task=load_yaml,
           file=f'{task.host["site"]}.yaml'
    )

    task.host["asn"] = data.result["asn"]
    task.host["networks"] = data.result["networks"]
    task.host["template_config"] = task.run(task=template_file,
                                            template="router.j2", path="")


nr = InitNornir()
routers = nr.filter(nornir_nos="iosxr"
r = routers.run(load_data)
print_result(r)

配置网络设备

有了渲染的配置后,就可以配置网络设备了。回顾我们早先所学到的知识,我们知道Nornir致力于数据收集,并让您针对该数据运行task。此说明中没有任何内容专门提及网络设备或如何连接它们。Nornir对于如何访问设备并不了解。因此,尽管它目前随附有Paramiko,Napalm和Netmiko的插件,但其他任何方法也都可以实现。如果您想为Restconf或其他东西创建一个插件,那会很好。《星球大战》中有很多爆炸和火灾,因此纳帕姆在这种情况下似乎很合适。在该代码的最后一个示例中,我们导入napalm_configure插件,并选择将过滤器进一步扩展到仅目标Endor。

from nornir.core import InitNornir
from nornir.plugins.tasks.data import load_yaml
from nornir.plugins.tasks.text import template_file
from nornir.plugins.tasks.networking import napalm_configure
from nornir.plugins.functions.text import print_result


def load_data(task):
    data = task.run(
           task=load_yaml,
           file=f'{task.host["site"]}.yaml'
    )

    task.host["asn"] = data.result["asn"]
    task.host["networks"] = data.result["networks"]
    r = task.run(task=template_file, template="router.j2", path="")
    task.host["template_config"] = r.result
    task.run(task=napalm_configure, configuration=task.host["template_config"])


nr = InitNornir()
routers = nr.filter(nornir_nos="iosxr")

endor = routers.filter(site="endor")
r = endor.run(load_data)
print_result(r)

在学习网络自动化时,您有可能会使用Vagrant,这是测试和学习的绝佳工具。它使您可以即时创建虚拟路由器以运行测试。如果您没有尝试过Vagrant,我强烈建议您这样做。上面我依靠每个host的DNS条目并连接到默认端口。要对在Vagrant中运行的设备使用napalm_configuretask,您必须对inventory进行少量修改。一个示例如下所示:

---
en-rtr-1:
    nornir_username: vagrant
    nornir_password: vagrant
    loopback0: 10.230.12.2
    nornir_host: 127.0.0.1
    nornir_network_api_port: 12202

保持真实

回到现实,我的朋友想做的是查找每个设备的IP地址,然后查询该设备的信息。Nornir并没有开箱即用地完成任何需要的task,因此他必须自己构建它们。使用python框架的优点是您自己的扩展将更强大,感觉更自然,实现起来更容易,更高效。Nornir附带的插件数量将随着时间的推移而增加,但它也将成为您编写自己的创作的平台。

到你了

尽管我们在上面的示例中确实创建了一个完整的可运行的Nornir应用程序,但仍有许多工作要做。您可以创建许多独立程序,也可以将Nornir集成到更大的程序中。上面的示例在学习环境中效果很好,但是通常在现实世界中发生的事情会更有趣。我期待听到您使用Nornir创建的内容。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁