使用Nornir自动生成和填充网络设备配置元素

使用Nornir自动生成和填充网络设备配置元素

嘿哈布尔!

最近这里突然出现一篇文章 Mikrotik 和 Linux。 常规和自动化 使用化石手段解决了类似的问题。 尽管这项任务完全是典型的,但在哈布雷身上却没有任何相似之处。 我敢于向受人尊敬的 IT 社区提供我的自行车。

这并不是第一辆执行此类任务的自行车。 第一个选项是几年前实施的 ansible 版本 1.x.x。 这辆自行车很少使用,因此不断生锈。 从某种意义上说,任务本身并不像版本更新那样频繁出现 ansible。 而且每次需要开车的时候,链条都会脱落或者车轮会脱落。 然而,幸运的是,第一部分,生成配置,总是工作得非常清楚 神社2 该发动机已经建立很长时间了。 但第二部分——推出配置——通常会带来惊喜。 由于我必须将配置远程部署到 XNUMX 台设备,其中一些设备位于数千公里之外,因此使用此工具有点无聊。

在这里我必须承认我的不确定性很可能在于我不熟悉 ansible而非其缺点。 顺便说一句,这是很重要的一点。 ansible 是一个完全独立的、有自己的知识领域,有自己的 DSL(领域特定语言),必须保持在自信的水平。 嗯,那一刻 ansible 它发展得相当快,并且没有特别考虑向后兼容性,因此并没有增加信心。

因此,不久前,第二个版本的自行车诞生了。 这次在 蟒蛇,或者更确切地说,在一个编写的框架上 蟒蛇蟒蛇 标题 诺尼尔

所以 - 诺尼尔 是一个微框架编写的 蟒蛇蟒蛇 并专为自动化而设计。 与情况相同 ansible,要解决这里的问题,需要做好数据准备,即主机及其参数的清单,但脚本不是用单独的 DSL 编写的,而是用同样不是很旧但非常好的 p[i|i]ton 编写的。

让我们使用下面的实例来看看它是什么。

我在全国各地拥有数十个办事处的分支机构网络。 每个办公室都有一个 WAN 路由器,用于终止来自不同运营商的多个通信通道。 路由协议是BGP。 WAN 路由器有两种类型:Cisco ISG 或 Juniper SRX。

现在的任务是:您需要在分支网络的所有 WAN 路由器上的单独端口上配置视频监控专用子网 - 在 BGP 中通告此子网 - 配置专用端口的速度限制。

首先,我们需要准备几个模板,在此基础上分别为 Cisco 和 Juniper 生成配置。 还需要为每个点和连接参数准备数据,即收集相同的库存

思科现成模板:

$ cat templates/ios/base.j2 
class-map match-all VIDEO_SURV
 match access-group 111

policy-map VIDEO_SURV
 class VIDEO_SURV
    police 1500000 conform-action transmit  exceed-action drop

interface {{ host.task_data.ifname }}
  description VIDEOSURV
  ip address 10.10.{{ host.task_data.ipsuffix }}.254 255.255.255.0
  service-policy input VIDEO_SURV

router bgp {{ host.task_data.asn }}
  network 10.40.{{ host.task_data.ipsuffix }}.0 mask 255.255.255.0

access-list 11 permit 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255
access-list 111 permit ip 10.10.{{ host.task_data.ipsuffix }}.0 0.0.0.255 any

瞻博网络模板:

$ cat templates/junos/base.j2 
set interfaces {{ host.task_data.ifname }} unit 0 description "Video surveillance"
set interfaces {{ host.task_data.ifname }} unit 0 family inet filter input limit-in
set interfaces {{ host.task_data.ifname }} unit 0 family inet address 10.10.{{ host.task_data.ipsuffix }}.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.{{ host.task_data.ipsuffix }}.0/24 exact
set security zones security-zone WAN interfaces {{ host.task_data.ifname }}
set firewall policer policer-1m if-exceeding bandwidth-limit 1m
set firewall policer policer-1m if-exceeding burst-size-limit 187k
set firewall policer policer-1m then discard
set firewall policer policer-1.5m if-exceeding bandwidth-limit 1500000
set firewall policer policer-1.5m if-exceeding burst-size-limit 280k
set firewall policer policer-1.5m then discard
set firewall filter limit-in term 1 then policer policer-1.5m
set firewall filter limit-in term 1 then count limiter

当然,模板并不是凭空产生的。 这些本质上是在不同型号的两个特定路由器上解决任务后的工作配置之间的差异。

从我们的模板中我们看到,要解决这个问题,我们只需要 Juniper 的两个参数和 Cisco 的 3 个参数。 他们来了:

  • 如果名称
  • ip后缀
  • ASN

现在我们需要为每个设备设置这些参数,即做同样的事 库存.

库存 我们将严格遵循文档 初始化诺尼尔

也就是说,让我们创建相同的文件骨架:

.
├── config.yaml
├── inventory
│   ├── defaults.yaml
│   ├── groups.yaml
│   └── hosts.yaml

config.yaml 文件是标准的nornir 配置文件

$ cat config.yaml 
---
core:
    num_workers: 10

inventory:
    plugin: nornir.plugins.inventory.simple.SimpleInventory
    options:
        host_file: "inventory/hosts.yaml"
        group_file: "inventory/groups.yaml"
        defaults_file: "inventory/defaults.yaml"

我们会在文件中注明主要参数 主机.yaml,组(在我的例子中,这些是登录名/密码) 组.yaml而在 默认值.yaml 我们不会指出任何内容,但您需要在那里输入三个减号 - 表明它是 雅姆 但该文件是空的。

这是hosts.yaml 的样子:

---
srx-test:
    hostname: srx-test
    groups: 
        - juniper
    data:
        task_data:
            ifname: fe-0/0/2
            ipsuffix: 111

cisco-test:
    hostname: cisco-test
    groups: 
        - cisco
    data:
        task_data:
            ifname: GigabitEthernet0/1/1
            ipsuffix: 222
            asn: 65111

这是 groups.yaml:

---
cisco:
    platform: ios
    username: admin1
    password: cisco1

juniper:
    platform: junos
    username: admin2
    password: juniper2

这就是发生的事情 库存 为了我们的任务。 在初始化期间,库存文件中的参数被映射到对象模型 库存元素.

剧透下方是 InventoryElement 模型的图

print(json.dumps(InventoryElement.schema(), indent=4))
{
    "title": "InventoryElement",
    "type": "object",
    "properties": {
        "hostname": {
            "title": "Hostname",
            "type": "string"
        },
        "port": {
            "title": "Port",
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "type": "string"
        },
        "password": {
            "title": "Password",
            "type": "string"
        },
        "platform": {
            "title": "Platform",
            "type": "string"
        },
        "groups": {
            "title": "Groups",
            "default": [],
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "data": {
            "title": "Data",
            "default": {},
            "type": "object"
        },
        "connection_options": {
            "title": "Connection_Options",
            "default": {},
            "type": "object",
            "additionalProperties": {
                "$ref": "#/definitions/ConnectionOptions"
            }
        }
    },
    "definitions": {
        "ConnectionOptions": {
            "title": "ConnectionOptions",
            "type": "object",
            "properties": {
                "hostname": {
                    "title": "Hostname",
                    "type": "string"
                },
                "port": {
                    "title": "Port",
                    "type": "integer"
                },
                "username": {
                    "title": "Username",
                    "type": "string"
                },
                "password": {
                    "title": "Password",
                    "type": "string"
                },
                "platform": {
                    "title": "Platform",
                    "type": "string"
                },
                "extras": {
                    "title": "Extras",
                    "type": "object"
                }
            }
        }
    }
}

这个模型可能看起来有点令人困惑,尤其是一开始。 为了弄清楚这一点,交互模式 蟒蛇.

 $ ipython3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.1.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from nornir import InitNornir                                                                           

In [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)                                                

In [3]: nr.inventory.hosts                                                                                      
Out[3]: 
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}

In [4]: nr.inventory.hosts['srx-test'].data                                                                                    
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}

In [5]: nr.inventory.hosts['srx-test']['task_data']                                                     
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}

In [6]: nr.inventory.hosts['srx-test'].platform                                                                                
Out[6]: 'junos'

最后,让我们继续讨论脚本本身。 我在这里没有什么值得特别自豪的。 我只是拿了一个现成的例子 教程 并几乎没有改变地使用它。 完成的工作脚本如下所示:

from nornir import InitNornir
from nornir.plugins.tasks import networking, text
from nornir.plugins.functions.text import print_title, print_result

def config_and_deploy(task):
    # Transform inventory data to configuration via a template file
    r = task.run(task=text.template_file,
                 name="Base Configuration",
                 template="base.j2",
                 path=f"templates/{task.host.platform}")

    # Save the compiled configuration into a host variable
    task.host["config"] = r.result

    # Save the compiled configuration into a file
    with open(f"configs/{task.host.hostname}", "w") as f:
        f.write(r.result)

    # Deploy that configuration to the device using NAPALM
    task.run(task=networking.napalm_configure,
             name="Loading Configuration on the device",
             replace=False,
             configuration=task.host["config"])

nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# run tasks
result = nr.run(task=config_and_deploy)
print_result(result)

注意参数 dry_run=真 行内对象初始化 nr.
这里与中相同 ansible 已实现测试运行,其中与路由器建立连接,准备新的修改配置,然后由设备验证(但这不确定;这取决于设备支持和 NAPALM 中的驱动程序实现) ,但新配置并未直接应用。 对于战斗使用,必须删除该参数 空运行 或将其值更改为 .

当脚本执行时,Nornir 会向控制台输出详细日志。

剧透下方是在两个测试路由器上运行的战斗输出:

config_and_deploy***************************************************************
* cisco-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : True ------------------------------------- INFO
class-map match-all VIDEO_SURV
 match access-group 111

policy-map VIDEO_SURV
 class VIDEO_SURV
    police 1500000 conform-action transmit  exceed-action drop

interface GigabitEthernet0/1/1
  description VIDEOSURV
  ip address 10.10.222.254 255.255.255.0
  service-policy input VIDEO_SURV

router bgp 65001
  network 10.10.222.0 mask 255.255.255.0

access-list 11 permit 10.10.222.0 0.0.0.255
access-list 111 permit ip 10.10.222.0 0.0.0.255 any
---- Loading Configuration on the device ** changed : True --------------------- INFO
+class-map match-all VIDEO_SURV
+ match access-group 111
+policy-map VIDEO_SURV
+ class VIDEO_SURV
+interface GigabitEthernet0/1/1
+  description VIDEOSURV
+  ip address 10.10.222.254 255.255.255.0
+  service-policy input VIDEO_SURV
+router bgp 65001
+  network 10.10.222.0 mask 255.255.255.0
+access-list 11 permit 10.10.222.0 0.0.0.255
+access-list 111 permit ip 10.10.222.0 0.0.0.255 any
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** changed : True *******************************************
vvvv config_and_deploy ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Base Configuration ** changed : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Video surveillance"
set interfaces fe-0/0/2 unit 0 family inet filter input limit-in
set interfaces fe-0/0/2 unit 0 family inet address 10.10.111.254/24
set policy-options policy-statement export2bgp term 1 from route-filter 10.10.111.0/24 exact
set security zones security-zone WAN interfaces fe-0/0/2
set firewall policer policer-1m if-exceeding bandwidth-limit 1m
set firewall policer policer-1m if-exceeding burst-size-limit 187k
set firewall policer policer-1m then discard
set firewall policer policer-1.5m if-exceeding bandwidth-limit 1500000
set firewall policer policer-1.5m if-exceeding burst-size-limit 280k
set firewall policer policer-1.5m then discard
set firewall filter limit-in term 1 then policer policer-1.5m
set firewall filter limit-in term 1 then count limiter
---- Loading Configuration on the device ** changed : True --------------------- INFO
[edit interfaces]
+   fe-0/0/2 {
+       unit 0 {
+           description "Video surveillance";
+           family inet {
+               filter {
+                   input limit-in;
+               }
+               address 10.10.111.254/24;
+           }
+       }
+   }
[edit]
+  policy-options {
+      policy-statement export2bgp {
+          term 1 {
+              from {
+                  route-filter 10.10.111.0/24 exact;
+              }
+          }
+      }
+  }
[edit security zones]
     security-zone test-vpn { ... }
+    security-zone WAN {
+        interfaces {
+            fe-0/0/2.0;
+        }
+    }
[edit]
+  firewall {
+      policer policer-1m {
+          if-exceeding {
+              bandwidth-limit 1m;
+              burst-size-limit 187k;
+          }
+          then discard;
+      }
+      policer policer-1.5m {
+          if-exceeding {
+              bandwidth-limit 1500000;
+              burst-size-limit 280k;
+          }
+          then discard;
+      }
+      filter limit-in {
+          term 1 {
+              then {
+                  policer policer-1.5m;
+                  count limiter;
+              }
+          }
+      }
+  }
^^^^ END config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

在ansible_vault中隐藏密码

文章开头我有点过分了 ansible,但这并没有那么糟糕。 我真的很喜欢他们 拱顶 就像,它的目的是将敏感信息隐藏在视线之外。 也许很多人已经注意到,我们在一个文件中以开放形式保存了所有战斗路由器的所有登录名/密码 组.yaml。 当然,这并不漂亮。 让我们保护这些数据 拱顶.

让我们将参数从 groups.yaml 传输到 creds.yaml,并使用 256 位密码使用 AES20 进行加密:

$ cd inventory
$ cat creds.yaml
---
cisco:
    username: admin1
    password: cisco1

juniper:
    username: admin2
    password: juniper2

$ pwgen 20 -N 1 > vault.passwd
ansible-vault encrypt creds.yaml --vault-password-file vault.passwd  
Encryption successful
$ cat creds.yaml 
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435

就是这么简单。 仍然需要教导我们 诺尼尔- 用于检索和应用此数据的脚本。
为此,请在我们的脚本中的初始化行之后 nr = InitNornir(config_file=... 添加以下代码:

...
nr = InitNornir(config_file="config.yaml", dry_run=True) # set dry_run=False, cross your fingers and run again

# enrich Inventory with the encrypted vault data
from ansible_vault import Vault
vault_password_file="inventory/vault.passwd"
vault_file="inventory/creds.yaml"
with open(vault_password_file, "r") as fp:
    password = fp.readline().strip()   
    vault = Vault(password)
    vaultdata = vault.load(open(vault_file).read())

for a in nr.inventory.hosts.keys():
    item = nr.inventory.hosts[a]
    item.username = vaultdata[item.groups[0]]['username']
    item.password = vaultdata[item.groups[0]]['password']
    #print("hostname={}, username={}, password={}n".format(item.hostname, item.username, item.password))

# run tasks
...

当然,vault.passwd 不应像我的示例中那样位于 creds.yaml 旁边。 不过用来玩还是可以的。

目前为止就这样了。 还有几篇关于 Cisco + Zabbix 的文章即将发布,但这与自动化无关。 在不久的将来我计划写一篇关于 Cisco 中的 RESTCONF 的文章。

来源: habr.com

添加评论