Merge pull request 'switch_rest' (#107) from switch_rest into new-infra

Reviewed-on: #107
This commit is contained in:
v-lafeychine 2025-08-28 21:39:59 +02:00
commit 0ade5a4be1
12 changed files with 894 additions and 97 deletions

213
README.md
View file

@ -1,10 +1,8 @@
[![Linter Status](https://drone.auro.re/api/badges/Aurore/ansible/status.svg)](https://drone.auro.re/Aurore/ansible)
# Recettes Ansible d'Aurore # Recettes Ansible d'Aurore
Dépendances requises : Dépendances requises :
* Ansible 2.9 ou plus récent. * Ansible 2.9 ou plus récent.
## Ansible 101 ## Ansible 101
@ -15,8 +13,9 @@ Il contient la définition de chaque machine et le regroupement.
Quand on regroupe avec un `:children` en réalité on groupe des groupes. Quand on regroupe avec un `:children` en réalité on groupe des groupes.
Chaque machine est annoncée avec son hostname. Il faut pouvoir SSH sur cette machine Chaque machine est annoncée avec son hostname. Il faut pouvoir SSH sur cette
avec ce hostname, car c'est ce qu'Ansible fera. machine avec ce hostname, car c'est ce qu'Ansible fera (sauf pour les switchs,
voir plus bas).
**Playbook** : c'est une politique de déploiement. **Playbook** : c'est une politique de déploiement.
Il contient les associations des rôles avec les machines. Il contient les associations des rôles avec les machines.
@ -37,31 +36,42 @@ déployer un serveur prometheus, déployer une node prometheus…
**Tâche** : un rôle est composé de tâches. Une tâche effectue une et une seule **Tâche** : un rôle est composé de tâches. Une tâche effectue une et une seule
action. Elle est associée à un module Ansible. action. Elle est associée à un module Ansible.
*Exemples de tâche* : installer un paquet avec le module `apt`, ajouter une ligne dans *Exemples de tâche* : installer un paquet avec le module `apt`, ajouter une
un fichier avec le module `lineinfile`, copier une template avec le module `template` ligne dans un fichier avec le module `lineinfile`, copier une template avec le
module `template`
Une tâche peut avoir des paramètres supplémentaires pour la réessayer quand elle plante, Une tâche peut avoir des paramètres supplémentaires pour la réessayer quand
récupérer son résultat dans une varible, mettre une boucle dessus, mettre des conditions… elle plante, récupérer son résultat dans une variable, mettre une boucle
dessus, mettre des conditions…
N'oubliez pas d'aller lire l'excellent documentation de RedHat sur tous les modules N'oubliez pas d'aller lire l'excellente documentation de RedHat sur tous les modules
d'Ansible ! d'Ansible !
### Gestion des groupes de machines ### Gestion des groupes de machines
Pour la liste complète, je vous invite à lire le fichier `hosts`. Pour la liste complète, je vous invite à lire le fichier `hosts`.
* pour tester les versions de Debian, Exemple :
```YAML ```yaml
ansible_lsb.codename == 'stretch' [fleming_vm]
``` dhcp-fleming.adm.auro.re
dns-fleming.adm.auro.re
prometheus-fleming.adm.auro.re
routeur-fleming.adm.auro.re
* pour tester si c'est un CPU Intel x86_64, [fleming_pve]
pve1.adm.auro.re
```YAML [fleming:children]
ansible_processor[0].find('Intel') != -1 fleming_pve
and ansible_architecture == 'x86_64' fleming_vm
``` ```
> NB :
>
> L'exemple a été adapté de la configuration d'Aurore pour des raisons
> pédagogiques.
Pour les fonctions (`proxy-server`, `dhcp-dynamique`…) il a été choisi Pour les fonctions (`proxy-server`, `dhcp-dynamique`…) il a été choisi
de ne pas faire de groupe particulier mais plutôt de sélectionner/enlever de ne pas faire de groupe particulier mais plutôt de sélectionner/enlever
@ -74,21 +84,36 @@ qui peuvent ensuite être utilisés dans des variables.
Pour lister tous les faits qu'Ansible collecte nativement d'un serveur Pour lister tous les faits qu'Ansible collecte nativement d'un serveur
on peut exécuter le module `setup` manuellement. on peut exécuter le module `setup` manuellement.
``` ```bash
ansible proxy.adm.auro.re -m setup --ask-vault-pass ansible proxy.adm.auro.re -m setup --ask-vault-pass
``` ```
Il est notamment possible de :
* tester les versions de Debian,
```YAML
ansible_lsb.codename == 'stretch'
```
* tester si c'est un CPU Intel x86_64,
```YAML
ansible_processor[0].find('Intel') != -1
and ansible_architecture == 'x86_64'
```
## Exécution d'Ansible ## Exécution d'Ansible
### Configurer la connexion au vlan adm ### Configurer la connexion au vlan adm
Envoyer son agent SSH peut être dangereux Envoyer son agent SSH peut être dangereux
([source](https://heipei.io/2015/02/26/SSH-Agent-Forwarding-considered-harmful/)). ([source](https://heipei.github.io/2015/02/26/SSH-Agent-Forwarding-considered-harmful/)).
On va utiliser plutôt `ProxyJump`. On va utiliser plutôt `ProxyJump`.
Dans la configuration SSH : Dans la configuration SSH :
``` ```text
Host *.adm.auro.re *.pve.auro.re Host *.adm.auro.re *.pve.auro.re
# Accept new host keys # Accept new host keys
StrictHostKeyChecking accept-new StrictHostKeyChecking accept-new
@ -97,7 +122,8 @@ Host *.adm.auro.re *.pve.auro.re
ProxyJump passerelle.auro.re ProxyJump passerelle.auro.re
``` ```
Il faut sa clé SSH configurée sur le serveur que l'on déploit. Il faut sa clé SSH configurée sur le serveur que l'on déploie.
```bash ```bash
ssh-copy-id proxy.adm.auro.re ssh-copy-id proxy.adm.auro.re
``` ```
@ -107,6 +133,7 @@ ssh-copy-id proxy.adm.auro.re
Il faut `python3-netaddr` sur sa machine. Il faut `python3-netaddr` sur sa machine.
Pour tester le playbook `base.yml` : Pour tester le playbook `base.yml` :
```bash ```bash
ansible-playbook --ask-vault-pass base.yml --check ansible-playbook --ask-vault-pass base.yml --check
``` ```
@ -116,7 +143,7 @@ Vous pouvez ensuite enlever `--check` si vous voulez appliquer les changements !
Si vous avez des soucis de fingerprint ECDSA, vous pouvez ignorer une Si vous avez des soucis de fingerprint ECDSA, vous pouvez ignorer une
première fois (dangereux !) : `ANSIBLE_HOST_KEY_CHECKING=0 ansible-playbook...`. première fois (dangereux !) : `ANSIBLE_HOST_KEY_CHECKING=0 ansible-playbook...`.
### Ajouter tous les empruntes de serveur ### Ajouter toutes les empreintes de serveur
```bash ```bash
#!/bin/bash #!/bin/bash
@ -125,6 +152,10 @@ for ip in `cat hosts|grep .adm.auro.re`; do
done done
``` ```
> Remarque :
>
> L'utilisation d'un certificat permet d'éviter d'avoir à ajouter sa clé ssh
> sur les serveurs.
### Passage à Ansible 2.10 (release: 30 juillet) ### Passage à Ansible 2.10 (release: 30 juillet)
@ -136,11 +167,141 @@ ansible-galaxy collection install community.general
ansible-galaxy collection install ansible.posix ansible-galaxy collection install ansible.posix
``` ```
Si vous n'arrivez pas à entrer votre *become password* (bug dans ansible?), un
Si vous n'arrivez pas à entrer votre _become password_ (bug dans ansible?), un
workaround est le suivant : workaround est le suivant :
`$ export ANSIBLE_BECOME_PASS='<votre mot de passe LDAP>'` `$ export ANSIBLE_BECOME_PASS='<votre mot de passe LDAP>'`
Notez l'espace au début pour ne pas log la commande dans votre historique Notez l'espace au début pour ne pas log la commande dans votre historique
shell. shell.
## Configuration des switchs depuis Ansible
Afin d'acquérir de l'indépendance vis-à-vis de re2o, un module permettant de
configurer les switchs depuis Ansible a été créé. Il utilise l'api rest des
switchs afin de récupérer et appliquer la configuration voulue.
### Prérequis
Pour utiliser le module, il faut d'abord annoncer à Ansible qu'il ne faut pas
effectuer de connexion ssh et de ne pas récupérer les faits. Cela se fait à
l'aide des variables `connection: httpapi` et `gather_facts: false` à placer
dans le playbook (pour une configuration locale) ou dans ansible.cfg (pour une
configuration globale). Ensuite, l'infrastructure actuelle de Aurore nécessite
l'utilisation d'un proxy. Pour cela, il suffit d'exécuter la commande :
```bash
ssh -D 3000 switchs-manager.adm.auro.re
```
et d'annoncer l'utilisation du proxy dans la configuration en exportant la
variable d'environnement `HTTP_PROXY=socks5://localhost:3000` et en
configurant la variable du module `use_proxy: true`.
Exemple :
```yaml
environment:
HTTP_PROXY: "socks5://localhost:3000"
tasks:
- name: vlans
switch_config:
username: ****
password: ****
port: 80
host: 192.168.1.42
use_proxy: true
config:
path: vlans/42
data:
name: VLAN42
vlan_id: 42
status: VS_PORT_BASED
type: VT_STATIC
```
Le module est alors utilisable, il ne reste plus qu'à le configurer.
### Écrire la configuration
Le module se veut assez libre. Ainsi, l'ensemble de la requête doit être écrite
dans les `tasks`. Voici un exemple pour configurer un vlan :
```yaml
tasks:
- name: vlans
switch_config:
username: ****
password: ****
port: 80
host: 192.168.1.42
config:
path: vlans/42
data:
name: VLAN42
vlan_id: 42
status: VS_PORT_BASED
type: VT_STATIC
```
Le `path` correspond à l'url de l'objet que l'on souhaite éditer et `data`
correspond aux données qui seront envoyées dans une requête `PUT` (au format
`json`). Cependant, la configuration d'un vlan peut nécessiter de le créer.
Pour remédier à ce problème, il est possible d'utiliser la syntaxe suivante :
```yaml
tasks:
- name: vlans
switch_config:
username: ****
password: ****
port: 80
host: 192.168.1.42
config:
path: vlans
create_method: POST
subpath:
- path: 42
data:
name: VLAN42
vlan_id: 42
status: VS_PORT_BASED
type: VT_STATIC
```
Le variable `create_method` correspond au type de la requête pour effectuer une
action de création de l'objet. Il s'agit généralement de `POST`. Dans le cas
où la variable n'est pas définit, la création sera désactivée et ainsi, si
l'url indiquée dans les `subpath` n'existe pas, alors la configuration échouera.
Par conséquent, si le vlan 42 a besoin d'être créé, une requête `POST` sera
effectué sur l'url `vlans` avec les données dans `data`.
Il est également possible d'éxecuter une action de suppression d'un vlan à l'aide
de la variable `delete` :
```yaml
tasks:
- name: vlans
switch_config:
username: ****
password: ****
port: 80
host: 192.168.1.42
config:
path: vlans/42
delete: true
```
Si la variable `delete` est activée, alors une requête `DELETE` sera envoyée
sur l'url indiquée. Pour vérifier si la suppression est déjà effective avant
l'éxecution, le module vérifiera si un `GET` sur l'url retourne une 404.
> Remarque :
>
> Si les variables `delete` et `data` sont définies (dont `delete` à `true`),
> alors il en résultera une action de suppression malgré tout.
Puisque `subpath` est une liste, il est possible de configurer plusieurs requête
en même temps. Cela à l'avantage d'effectuer toutes les modifications à la suite
(sans avoir à se connecter plusieurs sur l'api).

View file

@ -1,4 +1,6 @@
[defaults] [defaults]
jinja2_native = true
ask_vault_pass = True ask_vault_pass = True
roles_path = ./roles roles_path = ./roles
retry_files_enabled = False retry_files_enabled = False

View file

@ -0,0 +1,38 @@
#!/usr/bin/python
class FilterModule(object):
def filters(self):
return {
'range2list': self.range2list,
}
def range2list(self, port_range):
"""
Convert a range into list
Exemple:
```
>>> FilterModule.range2list("1-10,42")
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42]
````
"""
port_range = port_range.replace(" ", "").split(",")
ports = []
for r in port_range:
if "-" in r:
try:
a, b = r.split("-")
except:
raise Exception("A range must contain 2 values")
try:
a = int(a)
b = int(b)
except:
raise TypeError("A range must contain integer")
for n in range(a, b+1):
ports.append(n)
else:
try:
ports.append(int(r))
except:
raise TypeError("Value must be integer")
return list(set(ports))

12
group_vars/switch.yml Normal file
View file

@ -0,0 +1,12 @@
---
glob_switch:
loop_protect:
port_disable_timer_in_seconds: 30
transmit_interval_in_seconds: 3
sntp:
operation_mode: SNTP_UNICAST_MODE
poll_interval: 720
servers:
- ip: 10.206.1.5
priority: 1
...

71
hosts
View file

@ -294,59 +294,6 @@ fl-3-2.borne.auro.re
fl-4-1.borne.auro.re fl-4-1.borne.auro.re
fl-4-2.borne.auro.re fl-4-2.borne.auro.re
###############################################################################
# Pacaterie
[pacaterie_pve]
mordred.adm.auro.re
titan.adm.auro.re
[pacaterie_vm]
ldap-replica-pacaterie.adm.auro.re
dhcp-pacaterie.adm.auro.re
dhcp-pacaterie-backup.adm.auro.re
dns-pacaterie.adm.auro.re
dns-pacaterie-backup.adm.auro.re
prometheus-pacaterie.adm.auro.re
#prometheus-pacaterie-fo.adm.auro.re
radius-pacaterie.adm.auro.re
radius-pacaterie-backup.adm.auro.re
unifi-pacaterie.adm.auro.re
routeur-pacaterie.adm.auro.re
routeur-pacaterie-backup.adm.auro.re
[pacaterie_unifi]
pc-1-1.borne.auro.re
pn-0-1.borne.auro.re
pn-0-2.borne.auro.re
pn-0-3.borne.auro.re
pn-1-1.borne.auro.re
pn-1-2.borne.auro.re
pn-1-3.borne.auro.re
pn-2-1.borne.auro.re
pn-2-2.borne.auro.re
pn-2-3.borne.auro.re
pn-3-1.borne.auro.re
pn-3-2.borne.auro.re
pn-3-3.borne.auro.re
pn-4-1.borne.auro.re
pn-4-2.borne.auro.re
pn-4-3.borne.auro.re
ps-0-1.borne.auro.re
ps-0-2.borne.auro.re
ps-0-3.borne.auro.re
ps-1-1.borne.auro.re
ps-1-2.borne.auro.re
ps-1-3.borne.auro.re
ps-2-1.borne.auro.re
ps-2-2.borne.auro.re
ps-2-3.borne.auro.re
ps-3-1.borne.auro.re
ps-3-2.borne.auro.re
ps-3-3.borne.auro.re
ps-4-1.borne.auro.re
ps-4-2.borne.auro.re
ps-4-3.borne.auro.re
############################################################################### ###############################################################################
# Emilie du Chatelet # Emilie du Chatelet
@ -550,12 +497,6 @@ fleming_pve
fleming_vm fleming_vm
fleming_unifi fleming_unifi
# everything at pacaterie
[pacaterie:children]
pacaterie_pve
pacaterie_vm
pacaterie_unifi
# everything at edc # everything at edc
[edc:children] [edc:children]
edc_server edc_server
@ -586,7 +527,6 @@ ovh_container
#[vm:children] #[vm:children]
#ovh_vm #ovh_vm
#fleming_vm #fleming_vm
#pacaterie_vm
#edc_vm #edc_vm
#gs_vm #gs_vm
#rives_vm #rives_vm
@ -600,7 +540,6 @@ edc_server
#[pve:children] #[pve:children]
#ovh_pve #ovh_pve
#fleming_pve #fleming_pve
#pacaterie_pve
#edc_pve #edc_pve
#gs_pve #gs_pve
#rives_pve #rives_pve
@ -611,7 +550,6 @@ gs_unifi
edc_unifi edc_unifi
fleming_unifi fleming_unifi
rives_unifi rives_unifi
pacaterie_unifi
############################################################################### ###############################################################################
# Groups by service # Groups by service
@ -619,8 +557,6 @@ pacaterie_unifi
[routeur] [routeur]
routeur-fleming.adm.auro.re routeur-fleming.adm.auro.re
routeur-fleming-backup.adm.auro.re routeur-fleming-backup.adm.auro.re
routeur-pacaterie.adm.auro.re
routeur-pacaterie-backup.adm.auro.re
routeur-edc.adm.auro.re routeur-edc.adm.auro.re
routeur-edc-backup.adm.auro.re routeur-edc-backup.adm.auro.re
routeur-gs.adm.auro.re routeur-gs.adm.auro.re
@ -632,7 +568,6 @@ routeur-aurore-backup.adm.auro.re
[ldap_replica:children] [ldap_replica:children]
ldap_replica_fleming ldap_replica_fleming
ldap_replica_pacaterie
ldap_replica_edc ldap_replica_edc
ldap_replica_gs ldap_replica_gs
ldap_replica_ovh ldap_replica_ovh
@ -641,9 +576,6 @@ ldap_replica_rives
[ldap_replica_fleming] [ldap_replica_fleming]
ldap-replica-fleming.adm.auro.re ldap-replica-fleming.adm.auro.re
[ldap_replica_pacaterie]
ldap-replica-pacaterie.adm.auro.re
[ldap_replica_edc] [ldap_replica_edc]
ldap-replica-edc.adm.auro.re ldap-replica-edc.adm.auro.re
@ -685,8 +617,6 @@ radius-edc.adm.auro.re
radius-edc-backup.adm.auro.re radius-edc-backup.adm.auro.re
radius-gs.adm.auro.re radius-gs.adm.auro.re
radius-gs-backup.adm.auro.re radius-gs-backup.adm.auro.re
radius-pacaterie.adm.auro.re
radius-pacaterie-backup.adm.auro.re
radius-rives.adm.auro.re radius-rives.adm.auro.re
radius-rives-backup.adm.auro.re radius-rives-backup.adm.auro.re
@ -696,6 +626,5 @@ prometheus-aurore.adm.auro.re
prometheus-rives.adm.auro.re prometheus-rives.adm.auro.re
prometheus-gs.adm.auro.re prometheus-gs.adm.auro.re
prometheus-edc.adm.auro.re prometheus-edc.adm.auro.re
prometheus-pacaterie.adm.auro.re
prometheus-fleming.adm.auro.re prometheus-fleming.adm.auro.re
prometheus-federate.adm.auro.re prometheus-federate.adm.auro.re

390
library/switch_config.py Normal file
View file

@ -0,0 +1,390 @@
#!/usr/bin/python
DOCUMENTATION = """
---
module: Switch
short_description: Allow the setup of switches using rest API
description: Allow the setup of switches using rest API
options:
config:
description: configuration to send to the switch.
required: true
type: dict
host:
description: host of switch.
required: false
type: str
password:
description: password of the user.
required: true
type: str
port:
description: port of rest api.
required: false
type: int
use_proxy:
description:
Use a proxy to communicate with the switch.
HTTP_PROXY or ALL_PROXY must be set.
required: false
type: bool
username:
description: username of the rest API.
required: true
type: str
version:
description: version of the rest API.
required: false
type: str
"""
EXAMPLES = """
- name: Setup switch name
switch_config:
username: test
password: 1234
host: 192.168.1.1
port: 80
version: v8
config:
path: system
data:
name: "SwitchName"
- name: Setup vlans
switch_config:
username: test
password: 1234
host: 192.168.1.1
port: 80
config:
path: vlans
create_method: POST
subpath:
- path: 42
data:
name: "TheAnswer"
vlan_id: 42
status: VS_PORT_BASED
type: VT_STATIC
"""
from ansible.module_utils.basic import AnsibleModule
import json
import os
import requests
class SwitchApi:
def __init__(self, port, host, use_proxy, api="v1"):
self.headers = {'Content-Type': 'application/json'}
self.url_base = f"http://{host}:{port}/rest/{api}"
self.proxies = None
if use_proxy:
http_proxy = os.getenv("HTTP_PROXY")
all_proxy = os.getenv("ALL_PROXY")
if http_proxy != "":
self.proxies = {'http': http_proxy}
elif all_proxy != "":
self.proxies = {'http': all_proxy}
def login(self, username, password):
"""
Log in to the rest api.
Return True if the connection has succeeded and False otherwise.
"""
data = {"userName": username, "password": password}
response = self.post("/login-sessions", data = json.dumps(data),)
if response.status_code != 201:
return False
data = response.json()
if not 'cookie' in data:
return False
self.headers['cookie'] = data['cookie']
return True
def logout(self):
"""
Log out of the rest api.
Return True if connection has succeeded and False otherwise
"""
response = self.delete("/login-sessions")
if response.status_code != 204:
return False
self.headers.pop('cookie')
return True
def post(self, url, data = None):
kwargs = {
"headers": self.headers
}
if data is not None:
kwargs["data"] = data
if self.proxies is not None:
kwargs["proxies"] = self.proxies
return requests.post(self.url_base + url, **kwargs)
def get(self, url, data = ""):
kwargs = {
"headers": self.headers
}
if data is not None:
kwargs["data"] = data
if self.proxies is not None:
kwargs["proxies"] = self.proxies
return requests.get(self.url_base + url, **kwargs)
def delete(self, url, data = ""):
kwargs = {
"headers": self.headers
}
if data is not None:
kwargs["data"] = data
if self.proxies is not None:
kwargs["proxies"] = self.proxies
return requests.delete(self.url_base + url, **kwargs)
def put(self, url, data = ""):
kwargs = {
"headers": self.headers
}
if data is not None:
kwargs["data"] = data
if self.proxies is not None:
kwargs["proxies"] = self.proxies
return requests.put(self.url_base + url, **kwargs)
def required_modification(current_conf, modification):
for k, v in modification.items():
if not k in current_conf:
return True
if current_conf[k] != v:
return True
return False
def configure(module, config, api, current_path="", create_method=None):
path = "/" + str(config["path"])
url = current_path + path
check_mode = module.check_mode
changed = False
before = {"path": path}
after = {"path": path}
if not "path" in config:
api.logout()
raise Exception("A path must be specified.")
# If removing configuration
if "delete" in config and config["delete"]:
# Get the configuration
response = api.get(url)
if response.status_code == 404:
before["delete"] = True
elif response.status_code in (200, 201, 202, 203, 204):
before["data"] = response.json()
else:
api.logout()
raise Exception(
"Failed to check the old configuration:",
f"Url: {response.url}",
f"Status code: {response.status_code}",
f"Response: {response.text}",
)
# If required, delete
if not "delete" in before and not check_mode:
response = api.delete(url)
if response.status_code >= 400:
api.logout()
raise Exception(
"Failed to delete:",
f"Url: {response.url}",
f"Status code: {response.status_code}",
f"Response: {response.text}",
)
else:
# Verify that everything is ok
response = api.get(url)
if response.status_code == 404:
changed = True
after["delete"] = True
elif response.status_code in (200, 201, 202, 203, 204):
after["data"] = response.json()
after["delete"] = False
else:
api.logout()
raise Exception(
"Failed to check the configuration after delete:",
f"Url: {response.url}",
f"Status code: {response.status_code}",
f"Response: {response.text}",
)
elif not "delete" in before:
after["delete"] = True
changed = True
else:
after["delete"] = True
# If create or edit
elif "data" in config and type(config["data"]) is dict:
# Get the configuration
response = api.get(url)
new_data = {}
if response.status_code == 404:
before["delete"] = True
elif response.status_code in (200, 201, 202, 203, 204):
before["data"] = response.json()
new_data = before["data"].copy()
else:
api.logout()
raise Exception(
"Failed to check the old configuration:",
f"Url: {response.url}",
f"Status code: {response.status_code}",
f"Response: {response.text}",
)
# If required, modify
if "delete" in before and not check_mode:
# Create
if create_method == "POST":
response = api.post(current_path, json.dumps(config["data"]))
elif create_method == "PUT":
response = api.put(current_path, json.dumps(config["data"]))
else:
api.logout()
raise Exception(
"Failed to create:",
"Variable create_method must be set to PUT or POST",
)
if response.status_code >= 400:
api.logout()
raise Exception(
"Failed to create:",
f"Url: {response.url}",
f"Status code: {response.status_code}",
f"Data: {config["data"]}",
f"Response: {response.text}",
)
after["data"] = response.json()
changed = True
elif not check_mode:
# Edit
if required_modification(before["data"], config["data"]):
response = api.put(url, json.dumps(config["data"]))
if response.status_code >= 400:
api.logout()
raise Exception(
"Failed to edit:",
f"Url: {response.url}",
f"Status code : {response.status_code}",
f"Data: {config["data"]}",
f"Response: {response.text}",
)
changed = True
after["data"] = response.json()
else:
after["data"] = before["data"].copy()
else:
if "delete" in before and create_method is None:
api.logout()
raise Exception(
"Failed to create:",
"Variable create_method must be set to PUT or POST",
)
new_data.update(config["data"])
after["data"] = new_data
changed = changed or (not "data" in before) or after["data"] != before["data"]
# Configure the subpaths
if "subpath" in config and type(config["subpath"]) is list:
create_method = None
if "create_method" in config:
create_method = config["create_method"]
before["subpath"] = []
after["subpath"] = []
for subconf in config["subpath"]:
response = configure(
module, subconf,
api,
current_path=url,
create_method=create_method
)
changed = changed or response["changed"]
before["subpath"].append(response["diff"]["before"])
after["subpath"].append(response["diff"]["after"])
return {
"changed": changed,
"diff": {"after": after, "before": before}
}
def run_module():
module_args = dict(
config=dict(type='dict', required=True),
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
port=dict(type='int', required=True),
host=dict(type='str', required=True),
version=dict(type='str', required=False, default='v1'),
use_proxy=dict(type='bool', required=False, default=False),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
result = {
"changed": False,
}
# api connection
api = SwitchApi(
module.params["port"],
module.params["host"],
module.params["use_proxy"],
api = module.params["version"]
)
login_success = api.login(
module.params["username"],
module.params["password"],
)
if not login_success:
module.fail_json(msg='login failed', **result)
return
try:
response = configure(module, module.params["config"], api)
except Exception as msg:
module.fail_json(msg="\n".join(msg.args), **result)
return
api.logout()
result.update(response)
if module.check_mode:
module.exit_json(**result)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

17
playbooks/switch.yml Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env ansible-playbook
---
- hosts:
- switch
connection: httpapi
gather_facts: false
environment:
HTTP_PROXY: "socks5://localhost:3000"
vars:
switch:
use_proxy: true
roles:
- switch-system
- switch-vlans
- switch-ports
- switch-vlans-ports
...

View file

@ -0,0 +1,48 @@
---
- name: Configure ports
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
config:
path: ports
subpath:
- path: "{{ item.id }}"
data:
name: "{{ item.name }}"
is_port_enabled: "{{ item.enabled | default(true) }}"
loop: "{{ switch_vars.ports }}"
- name: Configure lldp
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
config:
path: lldp/local-port
subpath:
- path: "{{ item.id }}"
data:
port_id: "{{ item.id | string }}"
admin_status: "{{ 'LPAS_TX_AND_RX' if item.lldp is defined and item.lldp else 'LPAS_DISABLED' }}"
loop: "{{ switch_vars.ports }}"
- name: Configure loop-protect
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: "loop_protect/ports/{{ item.id }}"
data:
port_id: "{{ item.id | string }}"
is_loop_protection_enabled: "{{ item.loop_protect | default(False) }}"
loop: "{{ switch_vars.ports }}"
...

View file

@ -0,0 +1,67 @@
---
- name: Configure switch
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
config:
path: system
data:
name: "{{ switch_vars.name | default('') }}"
location: "{{ switch_vars.location | default('') }}"
contact: "{{ switch_vars.contact | default('')}}"
- name: Configure sntp
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: system/sntp
data:
sntp_client_operation_mode: "{{ glob_switch.sntp.operation_mode }}"
sntp_config_poll_interval: "{{ glob_switch.sntp.poll_interval }}"
- name: Configure sntp servers
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: system/sntp_server
create_method: POST
subpath:
path: "{{ item.priority }}-{{ item.ip }}"
delete: "{{ item.delete | default(False) }}"
data:
sntp_servers:
- sntp_server_address:
version: "{{ item.ip_version | default('IAV_IP_V4') }}"
octets: "{{ item.ip }}"
sntp_server_priority: "{{ item.priority }}"
sntp_server_version: "{{ item.version | default(4) }}"
sntp_server_is_oobm: "{{ item.is_oobm | default(None) }}"
loop: "{{ glob_switch.sntp.servers }}"
- name: Configure loop-protect
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: loop_protect
data:
port_disable_timer_in_seconds: "{{ glob_switch.loop_protect.port_disable_timer_in_seconds }}"
transmit_interval_in_seconds: "{{ glob_switch.loop_protect.transmit_interval_in_seconds }}"
...

View file

@ -0,0 +1,52 @@
---
- name: Configure tagged vlans
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v1
config:
path: vlans-ports
create_method: POST
subpath:
- path: "{{ item.0.id }}-{{ item.1 }}"
data:
vlan_id: "{{ item.0.id }}"
port_id: "{{ item.1 | string }}"
port_mode: POM_TAGGED_STATIC
loop: "{{ switch_vars.vlans | subelements('tagged', skip_missing=True) }}"
- name: Configure untagged vlans
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
config:
path: vlans-ports
create_method: POST
subpath:
- path: "{{ item.0.id }}-{{ item.1 }}"
data:
vlan_id: "{{ item.0.id }}"
port_id: "{{ item.1 | string }}"
port_mode: POM_UNTAGGED
loop: "{{ switch_vars.vlans | subelements('untagged', skip_missing=True) }}"
- name: Remove vlans-ports
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
config:
path: vlans-ports
subpath:
- path: "{{ item.0.id }}-{{ item.1 }}"
delete: true
loop: "{{ switch_vars.vlans | subelements('remove_ports', skip_missing=True) }}"
...

View file

@ -0,0 +1,80 @@
---
- name: Configure vlans
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: vlans
create_method: POST
subpath:
- path: "{{ item.id }}"
data:
name: "{{ item.name }}"
vlan_id: "{{ item.id }}"
status: VS_PORT_BASED
type: VT_STATIC
loop: "{{ switch_vars.vlans }}"
- name: Remove vlans
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: vlans
subpath:
- path: "{{ item }}"
delete: true
loop: "{{ switch_vars.delete_vlans }}"
- name: Configure IP
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: "vlans/{{ item.0.id }}/ipaddresses"
create_method: POST
subpath:
- path: "{{ item.1.mode }}-{{ item.1.ip }}"
delete: "{{ item.1.delete | default(False) }}"
data:
ip_address_mode: "{{ item.1.mode }}"
vlan_id: "{{ item.0.id }}"
ip_address:
version: "{{ item.1.version | default('IAV_IP_V4') }}"
octets: "{{ item.1.ip }}"
ip_mask:
version: "{{ item.1.version | default('IAV_IP_V4') }}"
octets: "{{ item.1.mask | default('255.255.255.0') }}"
loop: "{{ switch_vars.vlans | subelements('ipaddresses', skip_missing=True) | selectattr('1.mode', '==', 'IAAM_STATIC') }}"
- name: Configure vlan without IP
switch_config:
username: "{{ switch_vars.username }}"
password: "{{ switch_vars.password }}"
port: "{{ switch_vars.port }}"
host: "{{ switch_vars.host }}"
use_proxy: "{{ switch.use_proxy }}"
version: v8
config:
path: "vlans/{{ item.0.id }}/ipaddresses"
create_method: POST
subpath:
- path: "{{ item.1.mode }}-0.0.0.0"
delete: "{{ item.1.delete | default(False) }}"
data:
ip_address_mode: "{{ item.1.mode }}"
vlan_id: "{{ item.0.id }}"
loop: "{{ switch_vars.vlans | subelements('ipaddresses', skip_missing=True) | rejectattr('1.mode', '==', 'IAAM_STATIC') }}"
...

View file

@ -5,6 +5,7 @@
python313Packages.jinja2 python313Packages.jinja2
python313Packages.requests python313Packages.requests
python313Packages.pysocks python313Packages.pysocks
python313Packages.dns
]; ];
LANG="C.UTF-8"; LANG="C.UTF-8";
} }