嘿哈布尔!
最近这里突然出现一篇文章
这并不是第一辆执行此类任务的自行车。 第一个选项是几年前实施的 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