import json import os import base64 import logging import random import threading import requests class NodeCommunicator: def __init__(self, name: str, config): self.name = name self.__config = config self.__api_addr = config["api-con"] def update(self, action: str, peering: dict): try: if action == "add": req = requests.post(f"{self.__api_addr}peerings", json=peering, timeout=10) elif action == "update": req = requests.put(f"{self.__api_addr}peerings", json=peering, timeout=10) elif action == "delete": req = requests.delete(f"{self.__api_addr}peerings", json=peering, timeout=10) else: return 400 # should only get here if a request was "successful" (not necessarily http code 200) return req.status_code except requests.exceptions.ReadTimeout: # if it took more than timeout for the client to respond return 504 class PeeringManager: def __init__(self, config): self.__config = config self.__peering_file = config["peerings"] self.__load_peerings() self._nodes = {} for node in self.__config["nodes"]: self._nodes[node] = NodeCommunicator( name=node, config=self.__config["nodes"][node]) self._amounts = None self._threads = [] def __load_peerings(self): if not os.path.exists(self.__peering_file): with open(self.__peering_file, "x") as p: json.dump({"mntner": {}, "asn": {}}, p) try: with open(self.__peering_file, "r") as p: self.peerings = json.load(p) # migration due to typo in "mnter" key: if "mnter" in self.peerings: print("WARN: migrating peering data (Typo mnter)") self.peerings = {"mntner": self.peerings["mnter"], "asn": self.peerings["asn"]} except json.decoder.JSONDecodeError as e: print("CRITICAL: potentially corrupted peering database, aborting:") print(e.args) exit(1) # with open(self.__peering_file, "w") as p: # json.dump({"mntner": {}, "asn": {}}, p) # with open(self.__peering_file, "r") as p: # self.peerings = json.load(p) # self.peerings = {} # missing_peerings = False # for peering in self._peerings: # if os.path.exists(f"{self._peering_dir}/{peering}.json"): # with open(f"{self._peering_dir}/{peering}.json") as peer_cfg: # self.peerings[peering] = json.load(peer_cfg) # else: # logging.warning(f"peering with id {peering} doesn't exist. removing reference in `{self._peering_dir}/peerings.json`") # self._peerings.remove(peering) # missing_peerings = True # if missing_peerings: # with open(f"{self._peering_dir}/peerings.json","w") as p: # json.dump(self._peerings, p, indent=4) def _save_peerings(self): with open(self.__peering_file, "w") as p: json.dump(self.peerings, p, indent=4) self._amounts = None def _update_nodes(self, action: str, peering, new_peering=None): """mode: "add","update","delete peering: peering to send to node (included in peering) new_peering: if mode=="update" the new peering to update to """ if peering["node"] in self._nodes: return_code = self._nodes[peering["node"]].update(action=action, peering = peering if not new_peering else new_peering) if return_code in [200, 201]: return True, return_code else: return False, return_code else: return False, 404 def _update_amounts(self): __new = {} for asn in self.peerings["asn"]: for peering in self.peerings["asn"][asn]: if peering["node"] not in __new: __new[peering["node"]] = 0 __new[peering["node"]] += 1 self._amounts = __new def amount_by_node(self, node_name: str): if self._amounts is None: self._update_amounts() try: return self._amounts[node_name] except KeyError: return 0 def exists(self, asn, node, mnt=None, wg_key=None, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=None, bgp_enh=None): """checks if a peerings with specific data already exists""" # check if mnt is specified, already exists in the database and if that mnt has the specified ASn -> if not: return False if mnt and not (mnt in self.peerings["mntner"] and asn in self.peerings["mntner"][mnt]): return False selected_peerings = self.peerings["asn"][asn] # check if the ASn even has peerings if len(selected_peerings) == 0: return False for p in selected_peerings: if p["node"] == node: if (not wg_key or p["wg_key"] == wg_key) and (not endpoint or p["endpoint"] == endpoint) \ and (not ipv6ll or p["ipv6ll"] == ipv6ll) and (not ipv4 or p["ipv4"] == ipv4) and (not ipv6 or p["ipv6"] == ipv6)\ and (not bgp_mp or p["bgp_mp"] == bgp_mp) and (not bgp_enh or p["bgp_enh"] == bgp_enh): return True return False def get_peerings_by_mnt(self, mnt): # print(self.peerings) try: out = [] # if admin is logged in: return all if mnt == self.__config["MNT"]: for mntner in self.peerings["mntner"]: for asn in self.peerings["mntner"][mntner]: try: for peering in self.peerings["asn"][asn]: out.append(peering) except KeyError: pass else: for asn in self.peerings["mntner"][mnt]: try: for peering in self.peerings["asn"][asn]: out.append(peering) except KeyError: pass return out except KeyError: return {} def add_peering(self, asn, node, mnt, wg_key, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=True, bgp_enh=True)-> tuple[bool,int]: # check if this MNT already has a/this asn try: if asn not in self.peerings["mntner"][mnt]: # ... and add it if it hasn't self.peerings[mnt].append(asn) except KeyError: # ... and cerate it if it doesn't have any yet self.peerings["mntner"][mnt] = [asn] try: if asn not in self.peerings["asn"]: self.peerings["asn"][asn] = [] except KeyError: self.peerings["asn"][asn] = [] # deny more than one peering per ASN to one node for peering in self.peerings["asn"][asn]: if peering["node"] == node: return False, 409 peering = {"MNT": mnt, "ASN": asn, "node": node, "wg_key": wg_key, "endpoint": endpoint, "ipv6ll": ipv6ll, "ipv4": ipv4, "ipv6": ipv6, "bgp_mp": bgp_mp, "bgp_enh": bgp_enh} success, code = self._update_nodes("add", peering=peering) if not success and code != 409: # if something failed notify user and don't add it to the list return False, code elif code == 409: # apparently this peering already exists in a similar way, so we'll try updating it success, code = self._update_nodes("update", peering=peering) if not success: return False, code self.peerings["asn"][asn].append(peering) self._save_peerings() return True, code def update_peering(self, asn, node, mnt, wg_key, endpoint=None, ipv6ll=None, ipv4=None, ipv6=None, bgp_mp=True, bgp_enh=True): # check if this MNT already has a/this asn try: if asn not in self.peerings["mntner"][mnt]: # ... and add it if it hasn't self.peerings[mnt].append(asn) except KeyError: # ... and create it if it doesn't have any yet self.peerings["mntner"][mnt] = [asn] try: # there are no peerings for this asn -> can't edit nothing... if asn not in self.peerings["asn"]: return False, 404 except KeyError: # this should only happen if "asn" not in self.peerings return False, 404 found = False for pNr in range(len(self.peerings["asn"][asn])): if self.peerings["asn"][asn][pNr]["node"] == node: old_peering = self.peerings["asn"][asn][pNr] new_peering = self.peerings["asn"][asn][pNr] = {"MNT": mnt, "ASN": asn, "node": node, "wg_key": wg_key, "endpoint": endpoint, "ipv6ll": ipv6ll, "ipv4": ipv4, "ipv6": ipv6, "bgp_mp": bgp_mp, "bgp_enh": bgp_enh} found = True peering_number = pNr if not found: return False, 404 # notify the node success, code = self._update_nodes("update", old_peering, new_peering=new_peering) if not success and code != 404: # revert updating peering self.peerings["asn"][asn][peering_number] = old_peering return False, code elif code == 404: # for some reason the node doesn't yet know about this peering, we'l try adding it success, code = self._update_nodes("add", old_peering, new_peering=new_peering) if not success: # revert updating peering self.peerings["asn"][asn][peering_number] = old_peering return False, code self._save_peerings() return True, code def delete_peering(self, asn, node, mnt, wg_key=None): if not self.exists(asn, node, mnt=mnt, wg_key=wg_key): return False, 404 for p in self.peerings["asn"][asn]: if p["node"] == node: self.peerings["asn"][asn].remove(p) self._save_peerings() success, code = self._update_nodes("delete", peering=p) print(f"DELETE: {asn} on {node}: {success}, {code}") return success, code # if nothing got found (should have been catched by self.exists) return False, 404