Switch: creation of a module to configure the switchs with Ansible.
This commit is contained in:
parent
9c3dc75323
commit
ec13538cb7
13 changed files with 901 additions and 4 deletions
143
README.md
143
README.md
|
@ -16,7 +16,7 @@ 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 machine
|
||||||
avec ce hostname, car c'est ce qu'Ansible fera.
|
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.
|
||||||
|
@ -41,9 +41,9 @@ action. Elle est associée à un module Ansible.
|
||||||
un fichier avec le module `lineinfile`, copier une template avec le module `template`…
|
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 elle plante,
|
||||||
récupérer son résultat dans une varible, mettre une boucle dessus, mettre des conditions…
|
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
|
||||||
|
@ -83,7 +83,7 @@ ansible proxy.adm.auro.re -m setup --ask-vault-pass
|
||||||
### 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 :
|
||||||
|
@ -125,6 +125,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)
|
||||||
|
|
||||||
|
@ -144,3 +148,134 @@ workaround est le suivant :
|
||||||
|
|
||||||
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 voulu.
|
||||||
|
|
||||||
|
### 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`. Ensuite,
|
||||||
|
l'infrasutructue actuelle de Aurore nécéssite l'utilisation d'un proxy. Pour
|
||||||
|
cela, il suffit d'éxecuter 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écessité 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é 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és 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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
38
filter_plugins/switch_range.py
Normal file
38
filter_plugins/switch_range.py
Normal 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
12
group_vars/switch.yml
Normal 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.130.0.15
|
||||||
|
priority: 1
|
||||||
|
...
|
52
host_vars/switch-dev.yml
Normal file
52
host_vars/switch-dev.yml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
switch_vars:
|
||||||
|
name: switch-dev
|
||||||
|
location: Emilie du Chatelet
|
||||||
|
host: 10.130.4.199
|
||||||
|
port: 80
|
||||||
|
username: CHANGE
|
||||||
|
password: ME
|
||||||
|
vlans:
|
||||||
|
- id: 300
|
||||||
|
name: "VLAN_TEST_300"
|
||||||
|
ipaddresses:
|
||||||
|
- mode: IAAM_DHCP
|
||||||
|
tagged: "{{ '10-12' | range2list }}"
|
||||||
|
- id: 301
|
||||||
|
name: "VLAN_TEST_301"
|
||||||
|
ipaddresses:
|
||||||
|
- mode: IAAM_STATIC
|
||||||
|
ip: 10.203.4.199
|
||||||
|
mask: 255.255.0.0
|
||||||
|
- mode: IAAM_STATIC
|
||||||
|
ip: 10.204.4.199
|
||||||
|
delete: true
|
||||||
|
tagged: [10, 11]
|
||||||
|
untagged: [12]
|
||||||
|
- id: 302
|
||||||
|
name: "VLAN_TEST_302"
|
||||||
|
remove_ports: "{{ '10-12,13' | range2list }}"
|
||||||
|
delete_vlans:
|
||||||
|
- 400
|
||||||
|
ports:
|
||||||
|
- id: 10
|
||||||
|
name: "PORT_TEST_10"
|
||||||
|
enabled: false
|
||||||
|
loop_protect: true
|
||||||
|
lldp: true
|
||||||
|
- id: 11
|
||||||
|
name: "PORT_TEST_11"
|
||||||
|
loop_protect: false
|
||||||
|
lldp: true
|
||||||
|
- id: 12
|
||||||
|
name: "PORT_TEST_12"
|
||||||
|
loop_protect: true
|
||||||
|
- id: 13
|
||||||
|
name: "PORT_TEST_13"
|
||||||
|
- id: 14
|
||||||
|
name: "PORT_TEST_14"
|
||||||
|
loop_protect: true
|
||||||
|
- id: 15
|
||||||
|
name: "PORT_TEST_15"
|
||||||
|
loop_protect: true
|
||||||
|
...
|
3
hosts
3
hosts
|
@ -1,5 +1,8 @@
|
||||||
# Aurore servers inventory
|
# Aurore servers inventory
|
||||||
|
|
||||||
|
[switch]
|
||||||
|
switch-dev
|
||||||
|
|
||||||
[vm_test]
|
[vm_test]
|
||||||
mx.test.infra.auro.re
|
mx.test.infra.auro.re
|
||||||
|
|
||||||
|
|
390
library/switch_config.py
Normal file
390
library/switch_config.py
Normal 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
17
playbooks/switch.yml
Executable 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
|
||||||
|
...
|
48
roles/switch-ports/tasks/main.yml
Normal file
48
roles/switch-ports/tasks/main.yml
Normal 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 }}"
|
||||||
|
...
|
67
roles/switch-system/tasks/main.yml
Normal file
67
roles/switch-system/tasks/main.yml
Normal 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 }}"
|
||||||
|
...
|
52
roles/switch-vlans-ports/tasks/main.yml
Normal file
52
roles/switch-vlans-ports/tasks/main.yml
Normal 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) }}"
|
||||||
|
...
|
80
roles/switch-vlans/tasks/main.yml
Normal file
80
roles/switch-vlans/tasks/main.yml
Normal 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') }}"
|
||||||
|
...
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue