diff --git a/action_plugins/aruba_cfg_restore.py b/action_plugins/aruba_cfg_restore.py new file mode 100644 index 0000000..de17d5e --- /dev/null +++ b/action_plugins/aruba_cfg_restore.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import base64 +import time +import functools +import urllib.parse + +import requests +from ansible.errors import AnsibleActionFail +from ansible.plugins.action import ActionBase + + +class Aruba: + def __init__(self, base_url): + self._session = requests.session() + self._base_url = base_url + + def _url(self, url): + return urllib.parse.urljoin(self._base_url, url) + + def login(self, username, password): + response = self._session.post( + self._url("/rest/v4/login-sessions"), + json={"userName": username, "password": password}, + ) + if response.status_code != requests.codes.created: + raise AnsibleActionFail("Login failed") + + def logout(self): + response = self._session.delete(self._url("/rest/v4/login-sessions")) + if response.status_code != requests.codes.no_content: + raise AnsibleActionFail("Logout failed") + + def restore(self, config): + response = self._session.post( + self._url( + "/rest/v4/system/config/cfg_restore/payload" + ), + json={ + "config_base64_encoded": base64.b64encode(config.encode()), + "is_forced_reboot_enabled": True, + }, + ) + if response.status_code != requests.codes.accepted: + raise AnsibleActionFail("Restore failed") + + response = self._session.get( + self._url( + "/rest/v4/system/config/cfg_restore/payload/status" + ) + ) + print(response.text) + + +class ActionModule(ActionBase): + + _VALID_ARGS = frozenset(("username", "password", "config", "url")) + + def _require_arg(self, name): + try: + return self._task.args[name] + except KeyError: + raise AnsibleActionFail("Missing argument: {}".format(name)) + + def run(self, tmp=None, task_vars=None): + task_vars = task_vars or {} + result = super().run(tmp, task_vars) + + base_url = self._task.args.get("url") + username = self._require_arg("username") + password = self._require_arg("password") + config = self._require_arg("config") + + aruba = Aruba(base_url) + aruba.login(username, password) + + try: + aruba.restore(config) + except: + raise + else: + aruba.logout() + + return result diff --git a/ansible.cfg b/ansible.cfg index b04e116..9b2d21c 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -4,6 +4,7 @@ roles_path = ./roles retry_files_enabled = False inventory = ./hosts filter_plugins = ./filter_plugins +action_plugins = ./action_plugins ansible_managed = Ansible managed, modified on %Y-%m-%d %H:%M:%S nocows = 1 forks = 15 diff --git a/filter_plugins/aruba.py b/filter_plugins/aruba.py new file mode 100644 index 0000000..c733394 --- /dev/null +++ b/filter_plugins/aruba.py @@ -0,0 +1,17 @@ +class FilterModule: + def filters(self): + return { + "aruba_ints": aruba_ints, + } + + +def aruba_ints(seq, sep=",", hyphen="-"): + ranges = [] + for value in sorted(seq): + if not ranges or ranges[-1][1] + 1 != value: + ranges.append((value, value)) + else: + ranges[-1] = (ranges[-1][0], value) + return sep.join( + (f"{a}" if a == b else f"{a}{hyphen}{b}" for a, b in ranges) + ) diff --git a/filter_plugins/enquote.py b/filter_plugins/enquote.py new file mode 100644 index 0000000..576bf3f --- /dev/null +++ b/filter_plugins/enquote.py @@ -0,0 +1,16 @@ +class FilterModule: + def filters(self): + return { + "enquote": enquote, + } + + +def enquote(string, delimiter='"', escape="\\"): + translation = str.maketrans( + { + delimiter: f"{escape}{delimiter}", + escape: f"{escape}{escape}", + } + ) + escaped = string.translate(translation) + return f"{delimiter}{escaped}{delimiter}" diff --git a/filter_plugins/list_utils.py b/filter_plugins/list_utils.py new file mode 100644 index 0000000..264a6f1 --- /dev/null +++ b/filter_plugins/list_utils.py @@ -0,0 +1,9 @@ +class FilterModule: + def filters(self): + return { + "contains": contains, + } + + +def contains(a, b): + return b in a diff --git a/filter_plugins/net_utils.py b/filter_plugins/net_utils.py index 5eecace..cbde396 100644 --- a/filter_plugins/net_utils.py +++ b/filter_plugins/net_utils.py @@ -8,6 +8,7 @@ class FilterModule: def filters(self): return { "remove_domain_suffix": remove_domain_suffix, + "hostname": hostname, "ipaddr_sort": ipaddr_sort, } @@ -17,6 +18,11 @@ def remove_domain_suffix(name): return parent.to_text() +def hostname(fqdn): + name = dns.name.from_text(fqdn) + return name.relativize(name.parent()).to_text() + + def ipaddr_sort(addrs, types, unknown_after=True): check_types = { "global": attrgetter("is_global"), diff --git a/filter_plugins/validators.py b/filter_plugins/validators.py new file mode 100644 index 0000000..b435511 --- /dev/null +++ b/filter_plugins/validators.py @@ -0,0 +1,11 @@ +class FilterModule: + def filters(self): + return { + "choices": choices, + } + + +def choices(value, choices): + if value not in choices: + raise ValueError(f"{value} not in {choices}") + return value diff --git a/hosts b/hosts index 52f5078..7c3c07d 100644 --- a/hosts +++ b/hosts @@ -96,6 +96,7 @@ radius-fleming.adm.auro.re dns-1.int.infra.auro.re isp-1.rtr.infra.auro.re isp-2.rtr.infra.auro.re +test-1.switch.infra.auro.re dhcp-1.isp.auro.re dhcp-2.isp.auro.re radius-fleming-backup.adm.auro.re diff --git a/playbooks/aruba.yml b/playbooks/aruba.yml new file mode 100755 index 0000000..d58582b --- /dev/null +++ b/playbooks/aruba.yml @@ -0,0 +1,84 @@ +#!/usr/bin/env ansible-playbook +--- +- hosts: + - test-1.switch.infra.auro.re + gather_facts: false + vars: + aruba__api_url: "http://{{ inventory_hostname }}" + aruba__api_username: "manager" + aruba__api_password: "manager" + aruba__model: J9773A + aruba__release: YA.16.11.002 + aruba__hostname: "{{ inventory_hostname | hostname }}" + aruba__rest_enabled: true + aruba__ssh_enabled: true + aruba__ntp_servers: + - 10.128.0.1 + - 2a09:6840:128:0:1::1 + aruba__timezone: Europe/Paris + aruba__dns_servers: + - 10.128.0.1 + - 2a09:6840:128:0:1::1 + aruba__dns_domain_names: + - switch.infra.auro.re + - infra.auro.re + - toto.auro.re + aruba__manager_password: "manager" + aruba__operator_password: "operator" + aruba__default_gateways: + - 10.131.0.1 + - 2a09:6840:131:0:1::1 + aruba__vlans: + 1: + name: Default + 131: + name: Switchs + addresses: + - 10.131.1.1/16 + - 2a09:6840:131:1:1::1/56 + 1000: + name: "Client 0" + 1001: + name: "Client 1" + 1002: + name: "Client 2" + 1003: + name: "Client 3" + 1004: + name: "Client 4" + aruba__interfaces: + 1: + name: Uplink + untagged: 131 + tagged: + - 1000 + - 1001 + - 1002 + - 1003 + - 1004 + loop_protect: true + lldp: true + 2: + name: "Client 0" + untagged: 1000 + loop_protect: true + 3: + name: "Client 1" + untagged: 1001 + loop_protect: true + 4: + name: "Client 2" + untagged: 1002 + speed_duplex: 100-full + loop_protect: true + 5: + name: "Client 3" + untagged: 1003 + loop_protect: true + 6: + name: "Client 4" + untagged: 1004 + loop_protect: true + roles: + - aruba +... diff --git a/roles/aruba/defaults/main.yml b/roles/aruba/defaults/main.yml new file mode 100644 index 0000000..408044a --- /dev/null +++ b/roles/aruba/defaults/main.yml @@ -0,0 +1,11 @@ +--- +aruba__ntp_servers: [] +aruba__vlans: {} +aruba__interfaces: {} +aruba__default_gateways: [] +aruba__ssh_enabled: False +aruba__rest_enabled: True +aruba__dns_domain_names: [] +aruba__loop_protect_disable_timer: 30 +aruba__loop_protect_tx_interval: 3 +... diff --git a/roles/aruba/tasks/main.yml b/roles/aruba/tasks/main.yml new file mode 100644 index 0000000..fc8d718 --- /dev/null +++ b/roles/aruba/tasks/main.yml @@ -0,0 +1,97 @@ +--- +- name: Generate configuration + set_fact: + aruba__config: "{{ lookup('template', './config.j2') }}" + when: "aruba__config is not defined" + +- name: Restore configuration + aruba_cfg_restore: + url: "http://{{ inventory_hostname }}/" + username: "{{ aruba__api_username }}" + password: "{{ aruba__api_password }}" + config: "{{ aruba__config }}" + +#- name: Login to switch +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/login-sessions" +# method: POST +# status_code: 201 +# body_format: json +# body: +# userName: "{{ aruba__api_username }}" +# password: "{{ aruba__api_password }}" +# register: login + +#- name: Get diff +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/system/config/cfg_restore/payload/latest_diff" +# method: POST +# body_format: json +# status_code: 202 +# body: +# config_base64_encoded: "{{ aruba__config | b64encode }}" +# headers: +# Cookie: "{{ login.json.cookie }}" +# register: diff + +#- name: Diff +# debug: +# msg: "{{ diff }}" + +#- name: Get diff +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/system/config/cfg_restore/payload/latest_diff/status" +# method: GET +# status_code: 200 +# headers: +# Cookie: "{{ login.json.cookie }}" +# register: diff + +#- name: Diff +# debug: +# msg: "{{ diff }}" + +#- name: Restore configuration +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/system/config/cfg_restore/payload" +# method: POST +# body_format: json +# status_code: 202 +# body: +# config_base64_encoded: "{{ aruba__config | b64encode }}" +# is_forced_reboot_enabled: true +# headers: +# Cookie: "{{ login.json.cookie }}" +# register: status +# +#- name: XX +# debug: +# msg: "{{ status }}" +# +#- name: Get diff +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/system/config/cfg_restore/payload/status" +# method: GET +# status_code: 200 +# headers: +# Cookie: "{{ login.json.cookie }}" +# register: diff + +#- name: Diff +# debug: +# msg: "{{ diff }}" + +#- name: Logout +# delegate_to: localhost +# uri: +# url: "{{ aruba__api_base_url }}/rest/v4/login-sessions" +# method: DELETE +# status_code: 204 +# headers: +# Cookie: "{{ login.json.cookie }}" +... diff --git a/roles/aruba/templates/config.j2 b/roles/aruba/templates/config.j2 new file mode 100644 index 0000000..44541b7 --- /dev/null +++ b/roles/aruba/templates/config.j2 @@ -0,0 +1,162 @@ +; {{ aruba__model }} Configuration Editor; Created on release #{{ aruba__release }} + +hostname {{ aruba__hostname | hostname | truncate(32) | enquote }} + +include-credentials + +{% if aruba__ntp_servers %} +timesync ntp +ntp unicast +{% for addr in aruba__ntp_servers %} +ntp server {{ addr | ipaddr }} iburst +{% endfor %} +{% if aruba__timezone == "Europe/Paris" %} +time daylight-time-rule western-europe +time timezone 60 +{% endif %} +{% endif %} + +{% for addr in aruba__dns_servers[:2] %} +ip dns server-address priority {{ loop.index }} {{ addr | ipaddr }} +{% endfor %} + +{% for domain in aruba__dns_domain_names[:5] %} +ip dns domain-name {{ domain | enquote }} +{% endfor %} + +activate provision disable +activate software-update disable + +{% if False %} +snmpv3 enable +snmpv3 only +snmpv3 user "re2o" +snmpv3 group ManagerPriv user "re2o" sec-model ver3 +snmp-server community "public" Operator +{% endif %} + +no cdp run +lldp run + +{% +set lldp_disabled = + aruba__interfaces.keys() + | difference(aruba__interfaces + | dict2items + | selectattr("value.lldp", "defined") + | selectattr("value.lldp", "==", True) + | map(attribute="key")) + | list +%} +{% if lldp_disabled %} +lldp admin-status {{ lldp_disabled | aruba_ints }} disable +{% endif %} + +password manager sha1 {{ aruba__manager_password | hash("sha1") }} +{% if aruba__operator_password is defined %} +password operator sha1 {{ aruba__operator_password | hash("sha1") }} +{% endif %} +#} + +{% if aruba__ssh_enabled %} +ip ssh +{# ip ssh cipher aes256–ctr #} +{# ip ssh kex ecdh-sha2-nistp521 #} +{# ip ssh mac hmac-sha2-256 #} +ip ssh filetransfer +{% else %} +no ip ssh +{% endif %} + +no telnet-server +no tftp + +{% if aruba__rest_enabled %} +{# FIXME: ssl #} +web-management plaintext +rest-interface +{% endif %} + +{% +set loop_protect = + aruba__interfaces + | dict2items + | selectattr("value.loop_protect", "defined") + | selectattr("value.loop_protect") + | map(attribute="key") + | list +%} +{% if loop_protect %} +loop-protect disable-timer {{ aruba__loop_protect_disable_timer | int }} +loop-protect transmit-interval {{ aruba__loop_protect_tx_interval | int }} +loop-protect {{ loop_protect | aruba_ints }} +{% endif %} + +{% if aruba__default_gateways | ipv4 %} +ip default-gateway {{ aruba__default_gateways | ipv4 | first }} +{% endif %} +{% if aruba__default_gateways | ipv6 %} +{# ipv6 default-gateway {{ aruba__default_gateways | ipv6 | first }} #} +{% endif %} + +{% for id, vlan in aruba__vlans.items() %} +vlan {{ id | int }} +{% if vlan.name is defined %} + name {{ vlan.name | truncate(32) | enquote }} +{% endif %} +{% +set untagged = + aruba__interfaces + | dict2items + | selectattr("value.untagged", "defined") + | selectattr("value.untagged", "==", id) + | map(attribute="key") + | list +%} +{% if untagged %} + untagged {{ untagged | aruba_ints }} +{% endif %} +{% +set tagged = + aruba__interfaces + | dict2items + | selectattr("value.tagged", "defined") + | selectattr("value.tagged", "contains", id) + | map(attribute="key") + | list +%} +{% if tagged %} + tagged {{ tagged | aruba_ints }} +{% endif %} +{% if vlan.addresses | default([]) %} +{% for addr in vlan.addresses | ipv4 %} + ip address {{ addr | ipaddr("host") }} +{% endfor %} +{% for addr in vlan.addresses | ipv6 %} + ipv6 address {{ addr | ipaddr("host") }} +{% endfor %} +{% else %} + no ip address +{% endif %} + exit + +{% endfor %} + +{% for id, iface in aruba__interfaces.items() %} +interface {{ id | int }} +{% if iface.name is defined %} + name {{ iface.name | truncate(32) | enquote }} +{% endif %} +{% if iface.enabled | default(True) %} + enable +{% else %} + no enable +{% endif %} +{# TODO: split and check speed/duplex #} +{% if iface.speed_duplex is defined %} + speed-duplex {{ iface.speed_duplex }} +{% endif %} + no flow-control + exit + +{% endfor %}