forked from hswaw/hscloud
821fa5fcc4
Change-Id: I185c2702384941b6537a6a4048bdb2e1c4e183ba
226 lines
6.5 KiB
Python
226 lines
6.5 KiB
Python
"""
|
|
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)
|