
Salut, Habr !
Récemment, un article a circulé ici où une tâche similaire a été résolue avec des moyens archaïques. Et bien que la tâche soit totalement typique, je ne trouve rien de similaire sur Habr à ce sujet. Je me permets de présenter à la respectable communauté IT mon propre vélo.
Ce n'est pas le premier vélo pour ce type de tâche. La première version a été réalisée il y a quelques années déjà sur ansible la version 1.x.x. Le vélo était rarement utilisé et, par conséquent, rouillait constamment. Dans le sens où la tâche elle-même ne se présente pas aussi souvent que les versions sont mises à jour ansible. Et chaque fois que j'avais besoin de l'utiliser, soit la chaîne tombait, soit la roue se détachait. Pourtant, la première partie, la génération des configs, fonctionne toujours très bien, heureusement jinja2 que le moteur est bien établi depuis longtemps. En revanche, la deuxième partie, le déploiement des configs, posait généralement des surprises. Et comme je dois déployer les configs à distance sur une cinquantaine d'appareils, dont certains se trouvent à des milliers de kilomètres, il était un peu stressant d'utiliser cet outil.
Il faut reconnaître que mon manque de confiance provient probablement de ma méconnaissance de ansible, plus que de ses défauts. Et c'est d'ailleurs un point important. ansible — c'est un domaine de connaissances complètement distinct, avec son propre DSL (Domain Specific Language) qu'il faut maintenir à un niveau sûr. Et le fait que ansible évolue assez rapidement, sans prendre en compte la rétrocompatibilité, n'ajoute pas à la confiance.
C'est pourquoi, il y a quelque temps, une deuxième version du vélo a été réalisée. Cette fois sur python, ou plutôt sur un framework écrit en python et pour python appelé
Ainsi — Nornir c'est un micro-framework écrit en python et pour python et destiné à l'automatisation. Comme dans le cas de ansible, pour résoudre les problèmes ici, une préparation des données adéquate est nécessaire, c'est-à-dire l'inventaire des hôtes et de leurs paramètres, alors que les scripts ne sont pas écrits dans un DSL distinct, mais tout dans ce Python pas très ancien, mais très bien.
Examinons ce que c'est à travers le prochain exemple vivant.
J'ai un réseau de filiales avec plusieurs dizaines de bureaux à travers le pays. Dans chaque bureau, il y a un routeur WAN qui termine plusieurs liens de différents opérateurs. Le protocole de routage est BGP. Les routeurs WAN sont de deux types : Cisco ISG ou Juniper SRX.
La tâche consiste à configurer sur tous les routeurs WAN du réseau de succursales un sous-réseau dédié pour la vidéo surveillance sur un port distinct — annoncer ce sous-réseau dans BGP — configurer une limitation de débit pour le port dédié.
Tout d'abord, nous devons préparer quelques modèles, à partir desquels seront générées les configurations séparément pour Cisco et Juniper. Nous devons également rassembler les données pour chaque point et les paramètres de connexion, c'est-à-dire créer l'inventaire.
Modèle prêt pour Cisco :
$ 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 anyModèle pour Juniper :
$ cat templates/junos/base.j2
set interfaces {{ host.task_data.ifname }} unit 0 description "Vidéo 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 limiterLes modèles ne viennent bien sûr pas de nulle part. Ce sont essentiellement des différences entre les configurations de travail avant-après la résolution de la tâche sur deux routeurs spécifiques de modèles différents.
D'après nos modèles, nous voyons qu'il nous suffit de deux paramètres pour Juniper et de trois paramètres pour Cisco. Les voici :
- ifname
- ipsuffix
- asn
Nous devons maintenant définir ces paramètres pour chaque appareil, c'est-à-dire faire ce qui est nécessaire. inventory.
Pour inventory Nous allons suivre scrupuleusement la documentation.
Nous allons créer une structure de fichiers similaire :
.
├── config.yaml
├── inventory
│ ├── defaults.yaml
│ ├── groups.yaml
│ └── hosts.yamlLe fichier config.yaml — est le fichier de configuration standard de 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"Nous indiquerons les principaux paramètres dans le fichier hosts.yaml, groupes (dans mon cas, ce sont des identifiants/mots de passe) dans groups.yaml, et dans defaults.yaml nous n'allons rien indiquer, mais il est nécessaire d'écrire trois tirets — indiquant que c'est yaml le fichier, bien qu'il soit vide.
Voici à quoi ressemble 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: 65111Et voici à quoi ressemble groups.yaml :
---
cisco:
platform: ios
username: admin1
password: cisco1
juniper:
platform: junos
username: admin2
password: juniper2C'est ce qui a été réalisé inventory pour notre tâche. Lors de l'initialisation, les paramètres des fichiers d'inventaire sont mappés sur le modèle d'objet InventoryElement.
Sous le spoiler, le schéma du modèle 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"
}
}
}
}
}Ce modèle peut sembler un peu confus, surtout au début. Pour s'y familiariser, le mode interactif est très utile dans ipython.
$ ipython3
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
Tapez 'copyright', 'credits' ou 'license' pour plus d'informations
IPython 7.1.1 -- Un Python interactif amélioré. Tapez '?' pour obtenir de l'aide.
Dans [1]: from nornir import InitNornir
Dans [2]: nr = InitNornir(config_file="config.yaml", dry_run=True)
Dans [3]: nr.inventory.hosts
Out[3]:
{'srx-test': Host: srx-test, 'cisco-test': Host: cisco-test}
Dans [4]: nr.inventory.hosts['srx-test'].data
Out[4]: {'task_data': {'ifname': 'fe-0/0/2', 'ipsuffix': 111}}
Dans [5]: nr.inventory.hosts['srx-test']['task_data']
Out[5]: {'ifname': 'fe-0/0/2', 'ipsuffix': 111}
Dans [6]: nr.inventory.hosts['srx-test'].platform
Out[6]: 'junos'
Et enfin, passons au script proprement dit. Je n'ai pas grand-chose dont je peux être fier. J'ai simplement pris un exemple prêt à l'emploi de et l'ai utilisé presque sans modifications. Voici à quoi ressemble le script de travail complet :
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):
# Transformez les données d'inventaire en configuration à l'aide d'un fichier modèle
r = task.run(task=text.template_file,
name="Base Configuration",
template="base.j2",
path=f"templates/{task.host.platform}")
# Enregistrez la configuration compilée dans une variable d'hôte
task.host["config"] = r.result
# Enregistrez la configuration compilée dans un fichier
with open(f"configs/{task.host.hostname}", "w") as f:
f.write(r.result)
# Déployez cette configuration sur l'appareil en utilisant NAPALM
task.run(task=networking.napalm_configure,
name="Chargement de la configuration sur l'appareil",
replace=False,
configuration=task.host["config"])
nr = InitNornir(config_file="config.yaml", dry_run=True) # mettez dry_run=False, croisez les doigts et relancez
# exécutez les tâches
result = nr.run(task=config_and_deploy)
print_result(result)Veuillez prêter attention au paramètre dry_run=True dans la ligne d'initialisation de l'objet nr.
C'est aussi comme dans ansible un test où l'on se connecte au routeur, prépare une nouvelle configuration modifiée qui est ensuite validée par l'appareil (mais ce n'est pas sûr ; cela dépend du support par l'appareil et de la mise en œuvre du pilote dans NAPALM), mais l'application de la nouvelle configuration ne se produit pas. Pour une utilisation en production, il est nécessaire de retirer le paramètre dry_run ou de changer sa valeur en False.
Lors de l'exécution du scénario, Nornir génère des journaux détaillés dans la console.
Sous le spoiler, sortie du test sur deux routeurs de test :
config_and_deploy***************************************************************
* cisco-test ** changé : True *******************************************
vvvv config_and_deploy ** changé : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuration de base ** changé : 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
---- Chargement de la configuration sur le dispositif ** changé : 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
^^^^ FIN config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* srx-test ** changé : True *******************************************
vvvv config_and_deploy ** changé : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- Configuration de base ** changé : True ------------------------------------- INFO
set interfaces fe-0/0/2 unit 0 description "Vidéo 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
---- Chargement de la configuration sur le dispositif ** changé : True --------------------- INFO
[edit interfaces]
+ fe-0/0/2 {
+ unit 0 {
+ description "Vidéo 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;
+ }
+ }
+ }
+ }
^^^^ FIN config_and_deploy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Cacher les mots de passe dans ansible_vault
Au début de l'article, j'ai un peu critiqué ansible, mais tout n'est pas si mal. Je trouve leurs vault est conçu pour masquer des informations sensibles aux regards indiscrets. Et beaucoup ont probablement remarqué que tous nos identifiants/mots de passe pour tous les routeurs de production sont affichés en texte clair dans un fichier. gorups.yaml. Ce n'est pas très élégant, en effet. Protégeons ces données avec vault.
Transférons les paramètres de groups.yaml vers creds.yaml, et chiffrons-le avec AES256 à l'aide d'un mot de passe de 20 caractères :
$ 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
Chiffrement réussi
$ cat creds.yaml
$ANSIBLE_VAULT;1.1;AES256
39656463353437333337356361633737383464383231366233386636333965306662323534626131
3964396534396333363939373539393662623164373539620a346565373439646436356438653965
39643266333639356564663961303535353364383163633232366138643132313530346661316533
6236306435613132610a656163653065633866626639613537326233653765353661613337393839
62376662303061353963383330323164633162386336643832376263343634356230613562643533
30363436343465306638653932366166306562393061323636636163373164613630643965636361
34343936323066393763323633336366366566393236613737326530346234393735306261363239
35663430623934323632616161636330353134393435396632663530373932383532316161353963
31393434653165613432326636616636383665316465623036376631313162646435C'est aussi simple que cela. Reste à enseigner à notre Nornir-script d'extraire et d'appliquer ces données.
Pour cela, dans notre script après la ligne d'initialisation nr = InitNornir(config_file=… ajoutons le code suivant :
...
nr = InitNornir(config_file="config.yaml", dry_run=True) # définissez dry_run=False, croisez les doigts et relancez
# enrichir l'inventaire avec les données de coffre-fort chiffrées
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))
# exécuter des tâches
...Évidemment, vault.passwd ne doit pas être placé à côté de creds.yaml comme dans mon exemple. Mais pour jouer, cela ira.
C'est tout pour le moment. D'autres articles sur Cisco + Zabbix arriveront bientôt, mais cela sort un peu du cadre de l'automatisation. Dans un avenir proche, je prévois d'écrire sur RESTCONF dans Cisco.
Source : habr.com
