Switch: creation of a module to configure the switchs with Ansible.

This commit is contained in:
korenstin 2025-08-08 19:50:09 +02:00
parent 9c3dc75323
commit ec13538cb7
Signed by: korenstin
GPG key ID: 0FC4734F279D20A1
13 changed files with 901 additions and 4 deletions

143
README.md
View file

@ -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.
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.
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`
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 !
### 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
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`.
Dans la configuration SSH :
@ -125,6 +125,10 @@ for ip in `cat hosts|grep .adm.auro.re`; do
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)
@ -144,3 +148,134 @@ workaround est le suivant :
Notez l'espace au début pour ne pas log la commande dans votre historique
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).

View file

@ -1,4 +1,6 @@
[defaults]
jinja2_native = true
ask_vault_pass = True
roles_path = ./roles
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.130.0.15
priority: 1
...

52
host_vars/switch-dev.yml Normal file
View 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
View file

@ -1,5 +1,8 @@
# Aurore servers inventory
[switch]
switch-dev
[vm_test]
mx.test.infra.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.requests
python313Packages.pysocks
python313Packages.dns
];
LANG="C.UTF-8";
}