async def communities(participant: Participant, ) -> None: """Generate Participant-specific BGP Community Lists.""" log.info("Generating Communities for {}", participant.pretty) create_file_structure() participant_comms = template_env.get_template("participant-communities.j2") for rs in params.route_servers: result = await participant_comms.render_async( p=participant, now=datetime.utcnow().isoformat()) output_file = POLICIES_DIR / rs.name / str( participant.asn) / "communities.ios" if not output_file.exists(): output_file.touch() log.debug("Communities for {}\n{}", participant.pretty, result) with output_file.open("w+") as of: of.write(result) if verify_complete(output_file): log.success( "Generated Communities for {} at {}", participant.pretty, str(output_file), )
async def route_map(participant: Participant) -> None: """Generate Participant-specific Route Maps.""" log.info("Generating Route Maps for {}", participant.pretty) create_file_structure() participant_route_map = template_env.get_template( "participant-route-map.j2") for rs in params.route_servers: result = await participant_route_map.render_async( p=participant, rs=rs.id, loc=rs.loc_id, metro=rs.metro_id, now=datetime.utcnow().isoformat(), ) output_file = POLICIES_DIR / rs.name / str( participant.asn) / "route-map.ios" if not output_file.exists(): output_file.touch() log.debug("Route Maps for {}\n{}", participant.pretty, result) with output_file.open("w+") as of: of.write(result) if verify_complete(output_file): log.success( "Generated Route Maps for {} at {}", participant.pretty, str(output_file), )
async def bgp(participant: Participant) -> None: """Generate Participant-specific BGP Configs.""" log.info("Generating BGP Config for {}", participant.pretty) create_file_structure() max4, max6 = await max_prefixes(participant.asn) for rs in params.route_servers: output_file = POLICIES_DIR / rs.name / str(participant.asn) / "bgp.ios" if not output_file.exists(): output_file.touch() template = template_env.get_template("participant-bgp.j2") result = await template.render_async(p=participant, max4=max4, max6=max6, now=datetime.utcnow().isoformat()) log.debug("BGP Config for {}\n{}", participant.pretty, result) with output_file.open("w+") as of: of.write(result) if verify_complete(output_file): log.success( "Generated BGP Config for {} at {}", participant.pretty, str(output_file), )
async def run_whois(query: str) -> AsyncGenerator[str, None]: """Open raw socket to IRRd host and execute query.""" # Open the socket to IRRd log.debug("Opening connection to IRRd server") reader, writer = await asyncio.open_connection("rr.ntt.net", port=43) # Send the query writer.write(query.encode()) await writer.drain() # Read the response response = b"" while True: data = await reader.read(128) if data: response += data else: log.debug("Closing connection to IRRd server") writer.close() break if writer.can_write_eof(): writer.write_eof() items = ",".join(response.decode().split()[1:-1]).split(",") for item in (i for i in items if i): yield item
async def send_policy(route_server: RouteServer, wait: int) -> str: """Read a generated policy & send it to a route server.""" result = "" # Optionally block pause of execution. if wait: log.info("Waiting {} seconds before proceeding with update", wait) time.sleep(wait) try: connection: Connection = rpyc.connect(route_server.name, params.agent.port, ipv6=True) except ConnectionRefusedError: result = "Connection refused to {}:{}".format(route_server.name, params.agent.port) except gaierror: result = "Route Server {}:{} is unreachable".format( route_server.name, params.agent.port) # If an error has occurred, stop here & return a valuable error # message. if result: log.critical(result) await send_webhook(route_server.name, "Route Policy Error", result) return result config_file = POLICIES_DIR / route_server.name / "_all.ios" # Read the combined configuration file & generate a SHA256 digest of # its content. Then, encrypt the content, and generate another # SHA256 digest of the encrypted contents. with config_file.open("r") as f: payload, predigest, postdigest = create_payload(f.read()) log.debug("Pre Encryption Digest: {}", predigest) log.debug("Post Encryption Digest: {}", postdigest) while not connection.closed: log.info("Sending Policy to Route Server {} ({})", route_server.name, predigest) # Send the preflight payload digests. connection.root.set_digest(postdigest, predigest) # Send the encrypted payload. response = connection.root.push_policy(payload) result = "{}: {}".format(route_server.name, response) log.success(result) break # Gracefully close the connection. connection.close() await send_webhook(route_server.name, "Route Policy Update", result) return result
async def send_webhook(title: str, summary: str, message: str) -> None: """Send a Slack webhook.""" generated = generate_policy_block(title, summary, message) log.debug("Generated Slack hook:\n{}", generated) try: async with AsyncClient(base_url="https://hooks.slack.com") as client: await client.post( str(params.slack.webhook.path), json=generated, ) except Exception as err: log.error(str(err))
async def get_as_set(asn: str) -> Sequence[str]: """Search PeeringDB for an entry matching an ASN and return its IRR AS_Set.""" result = [] async with AsyncClient( http2=True, verify=True, base_url="https://peeringdb.com", headers={"Accept": "application/json"}, ) as client: log.debug("Getting max prefixes for AS{}", asn) res = await client.get("/api/net", params={"asn__contains": asn}) res.raise_for_status() for data in res.json()["data"]: if "asn" in data and str(data["asn"]) == asn: log.debug("Matched AS{} to {}", str(asn), data["name"]) log.debug( "AS{} PeeringDB Org ID {}, last updated {}", str(asn), str(data["org_id"]), data["updated"], ) as_set = data.get("irr_as_set", "") if as_set != "": result = as_set.split(" ") log.debug("Found AS-Set(s) {} for {}", result, data["name"]) break return result
async def max_prefixes(asn: int) -> Tuple[int, int]: """Search PeeringDB for an entry matching an ASN and return its max prefixes.""" prefixes = (200, 20) async with AsyncClient( http2=True, verify=True, base_url="https://peeringdb.com", headers={"Accept": "application/json"}, ) as client: log.debug("Getting max prefixes for AS{}", str(asn)) res = await client.get("/api/net", params={"asn__contains": asn}) res.raise_for_status() for data in res.json()["data"]: if "asn" in data and data["asn"] == asn: log.debug("Matched AS{} to {}", str(asn), data["name"]) log.debug( "AS{} PeeringDB Org ID {}, last updated {}", str(asn), str(data["org_id"]), data["updated"], ) prefixes = ( data.get("info_prefixes4", 200), data.get("info_prefixes6", 20), ) return prefixes
async def prefixes(participant: Participant) -> None: """Generate Participant-specific Prefix Lists.""" log.info("Generating Prefix Lists for {}", participant.pretty) create_file_structure() for rs in params.route_servers: async for family, render in render_prefixes( participant, max_ipv4=params.max_length.ipv4, max_ipv6=params.max_length.ipv6, template_env=template_env, ): output_file = (POLICIES_DIR / rs.name / str(participant.asn) / f"prefix-list-ipv{family}.ios") if not output_file.exists(): output_file.touch() rendered = await render log.debug( "IPv{} Prefix List for {}\n{}", family, participant.pretty, rendered, ) with output_file.open("w") as of: of.write(rendered) if verify_complete(output_file): log.success( "Generated IPv{} Prefix Lists for {} at {}", family, participant.pretty, str(output_file), )
def create_file_structure() -> bool: """Gracefully create output policy file structure.""" if not POLICIES_DIR.exists(): log.debug("Creating {}", str(POLICIES_DIR)) POLICIES_DIR.mkdir() for rs in params.route_servers: rs_dir = POLICIES_DIR / rs.name if not rs_dir.exists(): log.debug("Creating {}", str(rs_dir)) rs_dir.mkdir() for participant in params.participants: participant_dir = rs_dir / str(participant.asn) if not participant_dir.exists(): log.debug("Creating {}", str(participant_dir)) participant_dir.mkdir() return True
async def send_acls( # noqa: C901 your mom's too complex switches: List[Switch], participants: List[Participant]) -> str: """Send ACLs to IX switches, only if updates are required.""" messages: Tuple[str, ...] = () # Determine if an ACL push is required. async for participant in requires_update(switches, participants): for switch in switches: async with AsyncClient( auth=((switch.username, switch.password.get_secret_value())), verify=False, ) as client: p_messages: Tuple[str, ...] = () p_errors: Tuple[str, ...] = () acl4 = await generate_acl4(participant) acl6 = await generate_acl6(participant) push_id = ( f"{participant.pretty} ACL Push {datetime.utcnow().isoformat()}" ) data = { "id": push_id, "jsonrpc": "2.0", "method": "runCmds", "params": { "version": 1, "format": "json", "timestamps": False, "autoComplete": False, "expandAliases": False, "cmds": [ "configure", *acl4, *acl6, ], }, } log.info("Sending {} ACLs to {}", participant.pretty, switch.name) log.debug(json.dumps(data, indent=2)) res = await client.post( f"https://{str(switch.address)}/command-api", json=data) res_json = res.json() # Handle errors returned from the switch. if "error" in res_json: res_error: Tuple[str, ...] = () error_data = [ d for d in res_json["error"].get("data", []) if "errors" in d ] for d in error_data: for e in d.get("errors", []): res_error += (e, ) if "message" in res_json["error"]: res_error += (res_json["error"]["message"], ) p_errors += (", ".join(res_error), ) # Handle non-error messages returned from the switch. elif "result" in res_json: for d in [d for d in res_json["result"] if d]: p_messages += (d, ) if len(p_errors) == 0: messages += ("{} ACLs updated on {}".format( participant.pretty, str(switch.address)), ) for err in p_errors: msg = "Error while sending {} ACLs to {}:\n{}".format( participant.pretty, str(switch.address), err) log.error(msg) messages += (msg, ) for m in p_messages: msg = "Response while sending {} ACLs to {}:\n{}".format( participant.pretty, str(switch.address), m, ) log.info(msg) messages += (msg, ) if len(messages) == 0: message = "No particiapant ACLs require updates." else: message = ", ".join(messages) await send_webhook("Detail", "Switch ACL Updates", "\n".join(messages) or message) return message