""" Updates IRR objects in RIPE for the CCCamp IX. This, given a mntner password and a list of ASNs taking part in the IXP will ensure the right IRR objects are present: - an aut-num containing import/export RPSL rules - an as-set containing all member ASs. """ import difflib import string import sys import time import grpc import requests from bgpwtf.cccampix.proto import ix_pb2 as ipb from bgpwtf.cccampix.proto import ix_pb2_grpc as ipb_grpc class IRRObject: """An IRR object from RIPE.""" TYPE = None def __init__(self, fields=None): self.fields = fields or [] def add(self, k, v): self.fields.append((k, v)) def render(self): """Render to IRR format.""" return '\n'.join('{:16s}{}'.format(k+":", v) for k, v in self.fields) + "\n" @classmethod def from_ripe(cls, v): """Download this object from the RIPE REST API.""" if cls.TYPE is None: raise Exception('cannot fetch untyped IRRObject') r = requests.get('https://rest.db.ripe.net/ripe/{}/{}.json'.format(cls.TYPE, v)) d = r.json() obj = d['objects']['object'][0] assert obj['type'] == cls.TYPE assert obj['primary-key']['attribute'][0]['value'] == v attrs = obj['attributes']['attribute'] fields = [] for attr in attrs: # Skip metadata. if attr['name'] in ('created', 'last-modified'): continue fields.append((attr['name'], attr['value'])) return cls(fields) def send_to_ripe(self, password): """Update this object (or create new) in RIPE using the SyncUpdates API.""" res = self.render() res += 'password: {}\n'.format(password) data = { 'DATA': res, } r = requests.post('http://syncupdates.db.ripe.net/', files=data) res = r.text if 'Modify SUCCEEDED' not in res: print(res) raise Exception("Unexpected result from RIPE syncupdates") banner = [ ('remarks', r".--------------------------------------."), ('remarks', r"| _ _ __ |"), ('remarks', r"| | |__ __ _ _ ____ _| |_ / _| |"), ('remarks', r"| | '_ \ / _` | '_ \ \ /\ / / __| |_ |"), ('remarks', r"| | |_) | (_| | |_) \ V V /| |_| _| |"), ('remarks', r"| |_.__/ \__, | .__(_)_/\_/ \__|_| |"), ('remarks', r"| |___/|_| |"), ('remarks', r"|--------------------------------------|"), ('remarks', r"| CCCamp2019 Internet Exchange Point |"), ('remarks', r"'--------------------------------------'"), ('remarks', r''), ('remarks', r'// 21. - 25. August 2019'), ('remarks', r'// Ziegeleipark Mildenberg, Zehdenick, Germany, Earth, Milky Way'), ('remarks', r'// Join us: https://bgp.wtf/cccamp19'), ('remarks', r''), ] class IXPAutNum(IRRObject): """An aut-num (AS) object.""" TYPE = 'aut-num' @classmethod def make_for_members(cls, members): fields = [ ('aut-num', 'AS208521'), ('as-name', 'BGPWTF-CCCAMP19-IX'), ] + banner + [ ('remarks', '// Current members:'), ] for member in sorted(list(members)): fields.append(('import', 'from {} accept {}'.format(member, member))) fields.append(('export', 'to {} announce AS-CCCAMP19-IX'.format(member))) fields += [ ('remarks', ''), ('remarks', '// Abuse: noc@hackerspace.pl'), ('org', 'ORG-SH103-RIPE'), ('admin-c', 'HACK2-RIPE'), ('tech-c', 'HACK2-RIPE'), ('status', 'ASSIGNED'), ('mnt-by', 'RIPE-NCC-END-MNT'), ('mnt-by', 'BGPWTF-AUTOMATION'), ('mnt-by', 'pl-hs-1-mnt'), ('source', 'RIPE'), ] return cls(fields) class ASSet(IRRObject): """An as-set object.""" TYPE = 'as-set' @classmethod def make_for_members(cls, members): fields = [ ('as-set', 'AS-CCCAMP19-IX'), ('admin-c', 'HACK2-RIPE'), ] + banner + [ ('remarks', '// Current members:'), ] for member in sorted(list(members)): fields.append(('members', member)) fields += [ ('remarks', ''), ('remarks', '// Abuse: noc@hackerspace.pl'), ('tech-c', 'HACK2-RIPE'), ('mnt-by', 'BGPWTF-AUTOMATION'), ('mnt-by', 'pl-hs-1-mnt'), ('source', 'RIPE'), ] return cls(fields) def sync(want, got, password, force): """Sync an object if there is a diff to its current state.""" wantr = want.render().split('\n') gotr = got.render().split('\n') d = list(difflib.unified_diff(gotr, wantr, fromfile='got', tofile='want')) fields_diff = set() for dd in d: if dd.startswith('---'): continue if dd.startswith('+++'): continue if not dd.startswith('+') and not dd.startswith('-'): continue field = dd[1:].split(':')[0] fields_diff.add(field) # We ignore remarks field changes, because the RIPE API returns us them always # mangled (with spaces missing in the ASCII art). if list(fields_diff) == ['remarks'] and not force: print('No changes.') return if force: print('Forcing update') else: print('Diff:') print('\n'.join(d)) want.send_to_ripe(password) print('Updated.') def sync_autnum(members, password, force=False): print('Syncing aut-num...') want = IXPAutNum.make_for_members(members) got = IXPAutNum.from_ripe('AS208521') sync(want, got, password, force) def sync_asset(members, password, force=False): print('Syncing as-set...') want = ASSet.make_for_members(members) got = ASSet.from_ripe('AS-CCCAMP19-IX') sync(want, got, password, force) if __name__ == '__main__': if len(sys.argv) != 3: print("Usage: {} ".format(sys.argv[0])) sys.exit(1) password = sys.argv[1] verifier = sys.argv[2] chan = grpc.insecure_channel(verifier) stub = ipb_grpc.VerifierStub(chan) req = ipb.PeerSummaryRequest() peers = stub.PeerSummary(req) members = [] for peer in peers: if peer.check_status != peer.STATUS_OK: continue members.append('AS'+str(peer.peeringdb_info.asn)) print("Members:", members) sync_autnum(members, password) sync_asset(members, password)