| """ |
| 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: {} <password> <verifier addr>".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) |