blob: 116158cbd26b95aede89f783890441e6627745e7 [file] [log] [blame]
"""
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)