diff --git a/cosmo/graphqlclient.py b/cosmo/graphqlclient.py index bdbaea1..19ab574 100644 --- a/cosmo/graphqlclient.py +++ b/cosmo/graphqlclient.py @@ -1,10 +1,44 @@ import json from string import Template +from urllib.parse import urlencode import requests class GraphqlClient: + def __init__(self, url, token): + self.url = url + self.token = token + + v = self.query_version() + + if v == "3.x": + self.child_client = GraphqlClientV3(url, token) + elif v == "4.x": + self.child_client = GraphqlClientV4(url, token) + else: + raise Exception("Unknown Version") + + def query_version(self): + r = requests.get( + f"{self.url}/api/status/", + headers={ + "Authorization": f"Token {self.token}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + if r.status_code != 200: + raise Exception("Error querying api: " + r.text) + + json = r.json() + return "3.x" if str(json['netbox-version']).startswith("wc_3") else "4.x" + + def get_data(self, device_config): + return self.child_client.get_data(device_config) + + +class GraphqlBase: def __init__(self, url, token): self.url = url self.token = token @@ -24,6 +58,35 @@ def query(self, query): return r.json() + def query_rest(self, path, queries): + q = urlencode(queries, doseq=True) + url = f"{self.url}/{path}?{q}" + + return_array = list() + + while url is not None: + r = requests.get( + url, + headers={ + "Authorization": f"Token {self.token}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + + if r.status_code != 200: + raise Exception("Error querying api: " + r.text) + + data = r.json() + + url = data['next'] + return_array.extend(data['results']) + + return return_array + + +class GraphqlClientV3(GraphqlBase): + def get_data(self, device_config): query_template = Template( """ @@ -173,3 +236,149 @@ def get_data(self, device_config): r = self.query(query) return r['data'] + + +class GraphqlClientV4(GraphqlBase): + + def get_data(self, device_config): + query_template = Template( + """ + query { + device_list(filters: { + name: { in_list: $device_array }, + }) { + id + name + serial + + device_type { + slug + } + platform { + manufacturer { + slug + } + slug + } + primary_ip4 { + address + } + + interfaces { + id + name + enabled + type + mode + mtu + mac_address + description + vrf { + id + } + lag { + id + } + ip_addresses { + address + } + untagged_vlan { + id + name + vid + } + tagged_vlans { + id + name + vid + } + tags { + name + slug + } + parent { + id + } + connected_endpoints { + ... on InterfaceType { + name + device { + primary_ip4 { + address + } + interfaces { + ip_addresses { + address + } + } + } + } + } + custom_fields + } + + } + vrf_list { + id + name + description + rd + export_targets { + name + } + import_targets { + name + } + } + l2vpn_list (filters: {name: {starts_with: "WAN: "}}) { + id + name + type + identifier + terminations { + id + assigned_object { + __typename + ... on VLANType { + id + } + ... on InterfaceType { + id + device { + name + interfaces (filters:{type: {exact: "virtual"}}) { + ip_addresses { + address + } + parent { + name + type + } + vrf { + id + } + } + } + } + } + } + } + }""" + ) + + device_list = device_config['router'] + device_config['switch'] + query = query_template.substitute( + device_array=json.dumps(device_list) + ) + + r = self.query(query) + graphql_data = r['data'] + + static_routes = self.query_rest("/api/plugins/routing/staticroutes/", {"device": device_list}) + + for d in r['data']['device_list']: + + device_static_routes = list(filter(lambda sr: str(sr['device']['id']) == d['id'], static_routes)) + d['staticroute_set'] = device_static_routes + + return graphql_data diff --git a/cosmo/serializer.py b/cosmo/serializer.py index 23713db..868881c 100644 --- a/cosmo/serializer.py +++ b/cosmo/serializer.py @@ -102,7 +102,7 @@ def process_l2vpn_terminations(l2vpn_list): for l2vpn in l2vpn_list: if not l2vpn["name"].startswith("WAN: "): continue - if l2vpn['type'] == "VPWS" and len(l2vpn['terminations']) != 2: + if l2vpn['type'].lower() == "vpws" and len(l2vpn['terminations']) != 2: warnings.warn( f"VPWS circuits are only allowed to have two terminations. {l2vpn['name']} has {len(l2vpn['terminations'])} terminations, ignoring...") continue @@ -111,7 +111,7 @@ def process_l2vpn_terminations(l2vpn_list): "VLANType", "InterfaceType"]: warnings.warn(f"Found unsupported L2VPN termination in {l2vpn['name']}, ignoring...") continue - if l2vpn['type'] == "VPWS" and termination['assigned_object']['__typename'] != "InterfaceType": + if l2vpn['type'].lower() == "vpws" and termination['assigned_object']['__typename'] != "InterfaceType": warnings.warn( f"Found non-interface termination in L2VPN {l2vpn['name']}, ignoring... VPWS only supports interace terminations.") continue @@ -172,7 +172,8 @@ def _get_unit(self, iface): for ip in iface["ip_addresses"]: ipa = ipaddress.ip_interface(ip["address"]) - is_secondary = ip.get("role", None) == "SECONDARY" + role = ip.get("role", None) + is_secondary = role and role.lower() == "secondary" # abort if a private IP is used on a unit without a VRF # we use !is_global instead of is_private since the latter ignores 100.64/10 @@ -250,7 +251,7 @@ def _get_unit(self, iface): unit_stub["mtu"] = iface["mtu"] interface_vlan_id = None - if iface["mode"] == "ACCESS": + if iface["mode"] and iface["mode"].lower() == "access": if not iface.get("untagged_vlan"): warnings.warn( f"Interface {iface['name']} on device {self.device['name']} is mode ACCESS but has no untagged vlan, skipping" @@ -268,9 +269,9 @@ def _get_unit(self, iface): l2vpn = l2vpn_vlan_attached or l2vpn_interface_attached if l2vpn: - if l2vpn['type'] in ["VPWS", "EVPL"] and unit_stub.get('vlan'): + if l2vpn['type'].lower() in ["vpws", "evpl"] and unit_stub.get('vlan'): unit_stub["encapsulation"] = "vlan-ccc" - elif l2vpn['type'] in ["MPLS_EVPN", "VXLAN_EVPN"]: + elif l2vpn['type'].lower() in ["mpls_evpn", "vxlan_evpn"]: unit_stub["encapsulation"] = "vlan-bridge" # We need to collect the used L2VPNs for rendering those afterwards in other places within the configuration. @@ -423,15 +424,15 @@ def serialize(self): case _: warnings.warn(f"FEC mode {fec} on interface {interface['name']} is not known, ignoring") - if interface.get("type") == "LAG": + if interface.get("type", '').lower() == "lag": interface_stub["type"] = "lag" - elif interface.get("type") == "LOOPBACK": + elif interface.get("type", '').lower() == "loopback": interface_stub["type"] = "loopback" - elif interface.get("type") == "VIRTUAL": + elif interface.get("type", '').lower() == "virtual": interface_stub["type"] = "virtual" elif tags.has_key("access"): interface_stub["type"] = "access" - elif "BASE" in interface.get("type", ""): + elif "base" in interface.get("type", "").lower(): interface_stub["type"] = "physical" if tags.has_key("access"): @@ -467,7 +468,7 @@ def serialize(self): continue l2vpn = self.l2vpn_interface_terminations.get(si["id"]) - if len(sub_interfaces) == 1 and l2vpn and l2vpn['type'] in ["VPWS", "EPL", "EVPL"] \ + if len(sub_interfaces) == 1 and l2vpn and l2vpn['type'].lower() in ["vpws", "epl", "evpl"] \ and not si.get('vlan') and not si.get('custom_fields', {}).get("outer_tag", None): interface_stub["encapsulation"] = "ethernet-ccc" @@ -520,7 +521,7 @@ def serialize(self): router_id = next(iter(interfaces[self.lo_interface]["units"][0]["families"]["inet"]["address"].keys())).split("/")[0] for _, l2vpn in self.l2vpns.items(): - if l2vpn['type'] == "VXLAN_EVPN": + if l2vpn['type'].lower() == "vxlan_evpn": self.routing_instances[l2vpn["name"].replace("WAN: ", "")] = { "bridge_domains": [ { @@ -545,7 +546,7 @@ def serialize(self): "route_distinguisher": "9136:" + str(l2vpn["identifier"]), "vrf_target": "target:1:" + str(l2vpn["identifier"]), } - elif l2vpn['type'] == "MPLS_EVPN": + elif l2vpn['type'].lower() == "mpls_evpn": self.routing_instances[l2vpn["name"].replace("WAN: ", "")] = { "interfaces": [ i["name"] for i in l2vpn["interfaces"] @@ -558,8 +559,8 @@ def serialize(self): "route_distinguisher": "9136:" + str(l2vpn["identifier"]), "vrf_target": "target:1:" + str(l2vpn["identifier"]), } - elif l2vpn['type'] == "VPWS": - l2vpn_interfaces = {}; + elif l2vpn['type'].lower() == "vpws": + l2vpn_interfaces = {} for i in l2vpn["interfaces"]: id_local = int(i['id']) @@ -589,8 +590,8 @@ def serialize(self): "route_distinguisher": "9136:" + str(l2vpn["identifier"]), "vrf_target": "target:1:" + str(l2vpn["identifier"]), } - elif l2vpn['type'] == "EPL" or l2vpn['type'] == "EVPL": - l2vpn_interfaces = {}; + elif l2vpn['type'].lower() == "epl" or l2vpn['type'].lower() == "evpl": + l2vpn_interfaces = {} for i in l2vpn["interfaces"]: for termination in l2vpn["terminations"]: if termination["assigned_object"]["id"] == i["id"]: @@ -613,7 +614,9 @@ def serialize(self): # We pick a `virtual` interface which is not in a VRF and which parent is a `loopback` # Then we pick the first IPv4 address. for a in remote_interfaces: - if a["vrf"] == None and a["parent"] and a["parent"]["type"] == "LOOPBACK" and a["parent"]["name"].startswith("lo"): + if a["vrf"] is None and a["parent"] and \ + a["parent"]["type"] and a["parent"]["type"].lower() == "loopback" and \ + a["parent"]["name"].startswith("lo"): for ip in a['ip_addresses']: ipa = ipaddress.ip_interface(ip["address"]) if ipa.version == 4: @@ -628,7 +631,7 @@ def serialize(self): l2circuits[l2vpn["name"].replace("WAN: ", "")] = { "interfaces": l2vpn_interfaces, - "description": f"{l2vpn['type']}: " + l2vpn["name"].replace("WAN: ", "") + " via " + remote_device, + "description": f"{l2vpn['type'].upper()}: " + l2vpn["name"].replace("WAN: ", "") + " via " + remote_device, } for _, l3vpn in self.l3vpns.items(): @@ -689,7 +692,7 @@ def serialize(self): interface_stub["mtu"] = interface["mtu"] if interface["mtu"] else 10000 - if interface["type"] == "LAG": + if interface["type"] and interface['type'].lower() == "lag": interface_stub["bond_mode"] = "802.3ad" interface_stub["bond_slaves"] = sorted([i["name"] for i in self.device["interfaces"] if i["lag"] and i["lag"]["id"] == interface["id"]])