From ec13538cb73de60b0d22aaae0836abb60db7fe12 Mon Sep 17 00:00:00 2001 From: korenstin Date: Fri, 8 Aug 2025 19:50:09 +0200 Subject: [PATCH] Switch: creation of a module to configure the switchs with Ansible. --- README.md | 143 ++++++++- ansible.cfg | 2 + filter_plugins/switch_range.py | 38 +++ group_vars/switch.yml | 12 + host_vars/switch-dev.yml | 52 ++++ hosts | 3 + library/switch_config.py | 390 ++++++++++++++++++++++++ playbooks/switch.yml | 17 ++ roles/switch-ports/tasks/main.yml | 48 +++ roles/switch-system/tasks/main.yml | 67 ++++ roles/switch-vlans-ports/tasks/main.yml | 52 ++++ roles/switch-vlans/tasks/main.yml | 80 +++++ shell.nix | 1 + 13 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 filter_plugins/switch_range.py create mode 100644 group_vars/switch.yml create mode 100644 host_vars/switch-dev.yml create mode 100644 library/switch_config.py create mode 100755 playbooks/switch.yml create mode 100644 roles/switch-ports/tasks/main.yml create mode 100644 roles/switch-system/tasks/main.yml create mode 100644 roles/switch-vlans-ports/tasks/main.yml create mode 100644 roles/switch-vlans/tasks/main.yml diff --git a/README.md b/README.md index 5f2da81..8ca927f 100644 --- a/README.md +++ b/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. 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). diff --git a/ansible.cfg b/ansible.cfg index 805a0b1..b591cde 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,4 +1,6 @@ [defaults] +jinja2_native = true + ask_vault_pass = True roles_path = ./roles retry_files_enabled = False diff --git a/filter_plugins/switch_range.py b/filter_plugins/switch_range.py new file mode 100644 index 0000000..6eb0b37 --- /dev/null +++ b/filter_plugins/switch_range.py @@ -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)) diff --git a/group_vars/switch.yml b/group_vars/switch.yml new file mode 100644 index 0000000..74d8bdf --- /dev/null +++ b/group_vars/switch.yml @@ -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 +... diff --git a/host_vars/switch-dev.yml b/host_vars/switch-dev.yml new file mode 100644 index 0000000..eda335f --- /dev/null +++ b/host_vars/switch-dev.yml @@ -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 +... diff --git a/hosts b/hosts index 6dc745b..37bbd6f 100644 --- a/hosts +++ b/hosts @@ -1,5 +1,8 @@ # Aurore servers inventory +[switch] +switch-dev + [vm_test] mx.test.infra.auro.re diff --git a/library/switch_config.py b/library/switch_config.py new file mode 100644 index 0000000..07b0fc6 --- /dev/null +++ b/library/switch_config.py @@ -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() diff --git a/playbooks/switch.yml b/playbooks/switch.yml new file mode 100755 index 0000000..ccd1b19 --- /dev/null +++ b/playbooks/switch.yml @@ -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 +... diff --git a/roles/switch-ports/tasks/main.yml b/roles/switch-ports/tasks/main.yml new file mode 100644 index 0000000..b535ddd --- /dev/null +++ b/roles/switch-ports/tasks/main.yml @@ -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 }}" +... diff --git a/roles/switch-system/tasks/main.yml b/roles/switch-system/tasks/main.yml new file mode 100644 index 0000000..36e0904 --- /dev/null +++ b/roles/switch-system/tasks/main.yml @@ -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 }}" +... diff --git a/roles/switch-vlans-ports/tasks/main.yml b/roles/switch-vlans-ports/tasks/main.yml new file mode 100644 index 0000000..8322908 --- /dev/null +++ b/roles/switch-vlans-ports/tasks/main.yml @@ -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) }}" +... diff --git a/roles/switch-vlans/tasks/main.yml b/roles/switch-vlans/tasks/main.yml new file mode 100644 index 0000000..1c84593 --- /dev/null +++ b/roles/switch-vlans/tasks/main.yml @@ -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') }}" +... diff --git a/shell.nix b/shell.nix index 83a7687..c04a11d 100644 --- a/shell.nix +++ b/shell.nix @@ -5,6 +5,7 @@ python313Packages.jinja2 python313Packages.requests python313Packages.pysocks + python313Packages.dns ]; LANG="C.UTF-8"; }