From 92be56731dab038813a1340eee738e21b21c6ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Sun, 27 Sep 2020 21:50:16 +0200 Subject: [PATCH 1/6] start adding support for query to re2o instance --- generate-conf.py | 99 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/generate-conf.py b/generate-conf.py index 4ac7c64..f47752f 100644 --- a/generate-conf.py +++ b/generate-conf.py @@ -1,10 +1,12 @@ import argparse import difflib +import getpass import logging from pprint import pprint import colorlog +import requests import termcolor import yaml @@ -142,8 +144,17 @@ def gen_interfaces(switch_config): interfaces.sort(key=lambda x: x["number"]) return interfaces, vlans, mac_based_ports, ra_guard_ports, dhcp_snooping_vlans -def gen_conf(master_config, switch_config, old_config): +def get_header(old_config): header = "\n".join(old_config.split("\n")[:2]) + return header + +def conf_from_dict(config_dict): + with open("configs/config.j2", "r") as template_file: + template = Template(template_file.read()) + configuration = template.render(config_dict) + return configuration + +def gen_conf(master_config, switch_config, header): interfaces, vlans, mac_based_ports, ra_guard_ports, dhcp_snooping_vlans = gen_interfaces(switch_config) config_dict = { "header": header, @@ -164,13 +175,49 @@ def gen_conf(master_config, switch_config, old_config): "unauth_redirect": master_config.get("unauth_redirect"), "dhcp_snooping_vlans": syntax_from_range(dhcp_snooping_vlans, vlan_syntax=True), } - with open("configs/config.j2", "r") as template_file: - template = Template(template_file.read()) - configuration = template.render(config_dict) - return configuration + return conf_from_dict(config_dict) +def gen_conf_re2o(re2o_config, header): + print(re2o_config.keys()) + raise RuntimeError() + config_dict = { + "header": header + } + return conf_from_dict(config_dict) +def connect_as_self(base_url, username=None): + """Get a re2o token for the current unix user """ + if username is None: + username = getpass.getuser() + r = requests.post( + base_url + "token-auth", + data={ + "username": username, + "password": getpass.getpass("Login to {} as {} with password :".format(base_url, username)) + } + ) + if r.status_code != 200: + logger.critical("Wrong login/password") + exit(1) + token = r.json().get("token") + return token +def get_switch_from_results(results, switch_name): + for s in results: + if s.get("short_name"): + return s + return None + +def get_switch_from_re2o(re2o_instance, switch_name, re2o_user): + base_url = "{}/api/".format(re2o_instance) + token = connect_as_self(base_url, re2o_user) + headers = {"Authorization": "Token " + token} + r = requests.get(base_url + "switchs/ports-config", headers=headers) + sw_config = get_switch_from_results(r.json()["results"], switch_name) + while r.json().get("next") and s is None: + r = requests.get(r.json().get("next"), headers=headers) + sw_config = get_switch_from_results(r.json()["results"], switch_name) + return sw_config if __name__ == "__main__": format_string = "%(asctime)s - %(levelname)s - %(message)s" @@ -186,26 +233,52 @@ if __name__ == "__main__": parser.add_argument("-w", "--whole", action="store_true", help="Affiche la configuration en entier au lieu du diff") parser.add_argument("switch_name", type=str, help="Génère le template de ce switch") parser.add_argument("-H", "--host", type=str, required=False, help="Host sur lequel de déployer la configuration au lieu de l'adresse dans le template") + parser.add_argument("-r", "--re2o", type=str, required=False, help="Si renseigné, la configuration sera récupérée depuis l'instance de re2o indiquée au lieu d'utiliser les fichiers yaml") + parser.add_argument("--re2o-user", type=str, required=False, help="Permet de choisir l'user pour se connecter à l'api re2o, par défaut on prend l'user unix courant") + parser.add_argument("-4", "--force-ipv4", action="store_true", help="Force le provisionning en ipv4") + args = parser.parse_args() - logger.debug("Loading master config") - master_config = yaml.load(open("configs/master.yml", "r"), yaml.Loader) - logger.debug("Loading config for {}".format(args.switch_name)) - switch_config = yaml.load(open("configs/switches/{}.yml".format(args.switch_name), "r"), yaml.Loader) + if args.re2o: + logger.debug("Loading master config") + master_config = yaml.load(open("configs/master.yml", "r"), yaml.Loader) + logger.debug("Loading config from re2o") + re2o_config = get_switch_from_re2o(args.re2o, args.switch_name, args.re2o_user) + else: + logger.debug("Loading master config") + master_config = yaml.load(open("configs/master.yml", "r"), yaml.Loader) + logger.debug("Loading config for {}".format(args.switch_name)) + switch_config = yaml.load(open("configs/switches/{}.yml".format(args.switch_name), "r"), yaml.Loader) if args.host: switch_address = args.host + elif args.re2o: + if args.force_ipv4: + switch_address = re2o_config.get("ipv4") + else: + switch_address = re2o_config.get("ipv6") or re2o_config.get("ipv4") else: - switch_address = switch_config.get("ipv6-addr") or switch_config.get("ipv4-addr") + if args.force_ipv4: + switch_address = switch_config.get("ipv4-addr") + else: + switch_address = switch_config.get("ipv6-addr") or switch_config.get("ipv4-addr") + if switch_address is None: - switch_address = "{}.switches.crans.org".format(args.switch_name) + switch_address = "{}.switches.crans.org".format(args.switch_name) #TODO: crans logger.info("Connecting to {} with address {}".format(args.switch_name, switch_address)) - session = connect_to_switch(switch_address, user="root", key=master_config.get("ssh_private_key")) + session = connect_to_switch(switch_address, user="root", key=master_config.get("ssh_private_key")) #TODO: spécifier chemin clef old_config = sftp_read_file(session, "cfg/running-config").decode("utf-8") + header = get_header(old_config) + # génération de la conf logging.info("Generating configuration for {}".format(args.switch_name)) - configuration = gen_conf(master_config, switch_config, old_config) + if args.re2o: + configuration = gen_conf_re2o(re2o_config, header) + else: + configuration = gen_conf(master_config, switch_config, header) + + # génération du diff for line in difflib.unified_diff(old_config.split("\n"), configuration.split("\n"), fromfile='origin', tofile='new', lineterm=""): if line.startswith("-"): termcolor.cprint(line, "red") From 0aa15d15c61ca16c86a7fb4efed5bd4db2a3d86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Tue, 29 Sep 2020 23:54:05 +0200 Subject: [PATCH 2/6] radius logoff --- configs/master.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/configs/master.yml b/configs/master.yml index 6a1f0d7..5596a28 100644 --- a/configs/master.yml +++ b/configs/master.yml @@ -20,3 +20,4 @@ radius_servers: secret: ploptotoswitch1 snmp_user: re2o unauth_redirect: http://intranet.crans.org/users/initial_register +radius_logoff: 3600 From 135e9da414cd2665e10380888cb146bfef4b1efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Tue, 29 Sep 2020 23:54:40 +0200 Subject: [PATCH 3/6] patch typo + make it look like actual config --- configs/config.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/configs/config.j2 b/configs/config.j2 index ea645be..61d9d94 100644 --- a/configs/config.j2 +++ b/configs/config.j2 @@ -43,13 +43,16 @@ ip dns server-address priority {{ loop.index }} {{ d }} {%- endfor %} ip ssh filetransfer {%- for i6 in ipv6_managers.values() %} -ipv6 authorized-managers {{ i6.ip }} {{ i6.subnet }} access manager +ipv6 authorized-managers {{ i6.ip }} {{ i6.subnet }} access manager {%- endfor %} {%- if ra_guard_ports %} ipv6 ra-guard ports {{ ra_guard_ports }} {%- endif %} {%- for iface in interfaces %} interface {{ iface.number }} +{%- if iface.flowcontrol %} + flow-control +{%- endif %} name "{{ iface.name }}" {%- if iface.dhcp_trust %} dhcp-snooping trust @@ -57,9 +60,6 @@ interface {{ iface.number }} {%- if iface.dhcpv6_trust %} dhcpv6-snooping trust {%- endif %} -{%- if iface.flowcontrol %} - flow-control -{% endif %} {%- if iface.arp_trust %} arp-protect trust {%- endif %} From eb6a54d70b9d64a1c25f740072518955bfb3a15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Tue, 29 Sep 2020 23:55:14 +0200 Subject: [PATCH 4/6] now it fully works with re2o --- generate-conf.py | 131 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/generate-conf.py b/generate-conf.py index f47752f..4717208 100644 --- a/generate-conf.py +++ b/generate-conf.py @@ -178,11 +178,119 @@ def gen_conf(master_config, switch_config, header): return conf_from_dict(config_dict) def gen_conf_re2o(re2o_config, header): - print(re2o_config.keys()) - raise RuntimeError() + mgmt_utils = re2o_config.get("switchs_management_utils") + ipv4_managers = dict() + for m in mgmt_utils.get("subnet"): + print(m) + ipv4_managers[m.get("network")] = { "ip": m.get("network"), "subnet": m.get("netmask")} + ipv6_managers = dict() + # FUCK YOU ! subnet6 c'est pas une liste de subnets mais un seul subnet + m = mgmt_utils.get("subnet6") + ipv6_managers[m.get("network")] = { "ip": m.get("network"), "subnet": m.get("netmask")} + vlans = dict() + dhcp_snooping_vlans = list() + dhcpv6_snooping_vlans = list() + arp_protect = { "vlans": list() } + for vlan in re2o_config.get("vlans"): + vlan_id = vlan["vlan_id"] + vlans[vlan_id] = vlan + range_tagged = list() + range_untagged = list() + # on récupère les ports tag et untagged + for port in re2o_config.get("ports"): + port_profile = port["get_port_profile"] + for v in port_profile["vlan_tagged"]: + print(v) + if v["vlan_id"] == vlan_id: + range_tagged.append(port["port"]) + # i n'y a qu'un seul vlan untagged + v = port_profile["vlan_untagged"] + if v is not None and v["vlan_id"] == vlan_id: + range_untagged.append(port["port"]) + vlans[vlan_id]["tagged"] = syntax_from_range(range_tagged) + vlans[vlan_id]["untagged"] = syntax_from_range(range_untagged) + # on rajoute les ips sur les vlans où il y en a + for address, iface in re2o_config.get("interfaces_subnet", dict()).items(): + # ouais y'a une autre liste là, don't ask + for i in iface: + if i["vlan_id"] == vlan_id: + if vlans[vlan_id].get("ip") is None: + vlans[vlan_id]["ip"] = dict() + vlans[vlan_id]["ip"]["addr"] = address + vlans[vlan_id]["ip"]["subnet"] = i["netmask"] + for address, iface in re2o_config.get("interfaces6_subnet", dict()).items(): + if iface["vlan_id"] == vlan_id: + if vlans[vlan_id].get("ip") is None: + vlans[vlan_id]["ip"] = dict() + vlans[vlan_id]["ip"]["addr6"] = address + vlans[vlan_id]["ip"]["subnet6"] = iface["netmask_cidr"] + # on ajoute les vlans qui ont besoin du dhcp snooping + if vlan["dhcp_snooping"]: + dhcp_snooping_vlans.append(vlan_id) + if vlan["dhcpv6_snooping"]: + dhcpv6_snooping_vlans.append(vlan_id) + if vlan["arp_protect"]: + arp_protect["vlans"].append(vlan_id) + vlans[vlan_id]["ipv6_mld"] = vlan["mld"] + # on récupère les informations intéressantes sur les ports + mac_based_ports = list() + ra_guard_ports = list() + interfaces = list() + loop_protect = { "ports": list()} + for port in re2o_config.get("ports"): + port_profile = port["get_port_profile"] + if port_profile["ra_guard"]: + ra_guard_ports.append(port["port"]) + if port_profile["loop_protect"]: + loop_protect["ports"].append(port["port"]) + iface = { + "name": port["pretty_name"], + "number": port["port"], + "dhcp_trust": not port_profile["dhcp_snooping"], + "dhcpv6_trust": not port_profile["dhcpv6_snooping"], + "flowcontrol": port_profile["flow_control"], + "arp_trust": not port_profile["arp_protect"], + } + if port_profile["radius_type"] == "MAC-radius": + mac_based_ports.append(port["port"]) + iface["mac_based"] = True + iface["addr_limit"] = port_profile["mac_limit"] + iface["logoff"] = master_config.get("radius_logoff") + interfaces.append(iface) + loop_protect["ports"] = syntax_from_range(loop_protect["ports"]) + arp_protect["vlans"] = syntax_from_range(arp_protect["vlans"], vlan_syntax=True) + interfaces.sort(key=lambda x: x["number"]) + radius_key = re2o_config.get("get_radius_key_value") + radius_servers = [ {"ip": i, "secret": radius_key } for i in mgmt_utils["radius_servers"]["ipv4"] + mgmt_utils["radius_servers"]["ipv6"]] config_dict = { - "header": header + "header": header, + "location": re2o_config.get("switchbay").get("name"), + "hostname": re2o_config.get("short_name"), + "dhcp_servers": mgmt_utils.get("dhcp_servers").get("ipv4"), + "dhcpv6_servers": mgmt_utils.get("dhcp_servers").get("ipv6"), + "ipv4_managers": ipv4_managers, + "ipv6_managers": ipv6_managers, + "vlans": vlans, + "dhcp_snooping_vlans": syntax_from_range(dhcp_snooping_vlans, vlan_syntax=True), + "dhcpv6_snooping_vlans": syntax_from_range(dhcpv6_snooping_vlans, vlan_syntax=True), + "logging": mgmt_utils["log_servers"]["ipv4"] + mgmt_utils["log_servers"]["ipv6"], + "sntp": mgmt_utils["ntp_servers"]["ipv4"] + mgmt_utils["ntp_servers"]["ipv6"], + "dns": mgmt_utils["dns_recursive_servers"]["ipv4"] + mgmt_utils["dns_recursive_servers"]["ipv6"], + "radius_servers": radius_servers, + "snmp_user": master_config.get("snmp_user"), + "unauth_redirect": master_config.get("unauth_redirect"), + "mac_based_ports": syntax_from_range(mac_based_ports), + "ra_guard_ports": syntax_from_range(ra_guard_ports), + "loop_protect": loop_protect, + "arp_protect": arp_protect, + "interfaces": interfaces, + "FUCK": ['interfaces_subnet', 'ports', 'ipv6', 'ipv4', 'get_radius_key_value', 'interfaces6_subnet', 'list_modules'] } + pprint(re2o_config) +# pprint(re2o_config.get("interfaces_subnet")) +# pprint(re2o_config.get("list_modules")) +# raise RuntimeError() + return conf_from_dict(config_dict) def connect_as_self(base_url, username=None): @@ -204,7 +312,7 @@ def connect_as_self(base_url, username=None): def get_switch_from_results(results, switch_name): for s in results: - if s.get("short_name"): + if s.get("short_name") == switch_name: return s return None @@ -212,11 +320,19 @@ def get_switch_from_re2o(re2o_instance, switch_name, re2o_user): base_url = "{}/api/".format(re2o_instance) token = connect_as_self(base_url, re2o_user) headers = {"Authorization": "Token " + token} + # On récupère la config du bon switch r = requests.get(base_url + "switchs/ports-config", headers=headers) sw_config = get_switch_from_results(r.json()["results"], switch_name) while r.json().get("next") and s is None: r = requests.get(r.json().get("next"), headers=headers) sw_config = get_switch_from_results(r.json()["results"], switch_name) + # on récupère les infos globales pour dhcp, managers etc. + r = requests.get(base_url + "preferences/optionaltopologie", headers=headers) + sw_config.update(r.json()) + # on récupère la liste des vlans + r = requests.get(base_url + "machines/vlan", headers=headers) + pprint(r.json().get("results")) + sw_config.update({"vlans": r.json().get("results")}) return sw_config if __name__ == "__main__": @@ -298,3 +414,10 @@ if __name__ == "__main__": session = connect_to_switch(switch_address, user="crans", key=master_config.get("ssh_private_key")) sftp_write_file(session, "/tmp/conf_tmp", "cfg/startup-config") logging.info("Deployement done for {}".format(args.switch_name)) + + +# on retire ces deux lignes qui sont hardcodées dans les templates crans +# c'est deux adresses de multicast au pif, wtf ? +# https://gitlab.crans.org/nounous/scripts/-/commit/af2d158ff1a99dee2f032a81bf14de4144d2b82f +#-filter multicast 01005e-0000fb drop 1-52 +#-filter multicast 333300-0000fb drop 1-52 From 9464de9c5ba61d34199c6eaa0caa3fc54cbf767d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Tue, 29 Sep 2020 23:57:43 +0200 Subject: [PATCH 5/6] update requirements --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requirements.txt b/requirements.txt index 617e9d8..f0bd194 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,12 @@ +certifi==2020.6.20 +chardet==3.0.4 colorlog==4.2.1 +idna==2.10 Jinja2==2.11.2 MarkupSafe==1.1.1 pkg-resources==0.0.0 PyYAML==5.3.1 +requests==2.24.0 ssh2-python==0.18.0.post1 termcolor==1.1.0 +urllib3==1.25.10 From 058451c41d699317bd8a9d7d043622a413e443c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Paulon?= Date: Wed, 30 Sep 2020 00:01:52 +0200 Subject: [PATCH 6/6] clean --- generate-conf.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/generate-conf.py b/generate-conf.py index 4717208..b006e37 100644 --- a/generate-conf.py +++ b/generate-conf.py @@ -3,8 +3,6 @@ import difflib import getpass import logging -from pprint import pprint - import colorlog import requests import termcolor @@ -181,7 +179,6 @@ def gen_conf_re2o(re2o_config, header): mgmt_utils = re2o_config.get("switchs_management_utils") ipv4_managers = dict() for m in mgmt_utils.get("subnet"): - print(m) ipv4_managers[m.get("network")] = { "ip": m.get("network"), "subnet": m.get("netmask")} ipv6_managers = dict() # FUCK YOU ! subnet6 c'est pas une liste de subnets mais un seul subnet @@ -200,7 +197,6 @@ def gen_conf_re2o(re2o_config, header): for port in re2o_config.get("ports"): port_profile = port["get_port_profile"] for v in port_profile["vlan_tagged"]: - print(v) if v["vlan_id"] == vlan_id: range_tagged.append(port["port"]) # i n'y a qu'un seul vlan untagged @@ -284,12 +280,7 @@ def gen_conf_re2o(re2o_config, header): "loop_protect": loop_protect, "arp_protect": arp_protect, "interfaces": interfaces, - "FUCK": ['interfaces_subnet', 'ports', 'ipv6', 'ipv4', 'get_radius_key_value', 'interfaces6_subnet', 'list_modules'] } - pprint(re2o_config) -# pprint(re2o_config.get("interfaces_subnet")) -# pprint(re2o_config.get("list_modules")) -# raise RuntimeError() return conf_from_dict(config_dict) @@ -331,7 +322,6 @@ def get_switch_from_re2o(re2o_instance, switch_name, re2o_user): sw_config.update(r.json()) # on récupère la liste des vlans r = requests.get(base_url + "machines/vlan", headers=headers) - pprint(r.json().get("results")) sw_config.update({"vlans": r.json().get("results")}) return sw_config