forked from hswaw/hscloud
bgpwtf/cccampix: init
Add sync script for camp IX. This will likely be triggered externally from some sort of long-running service. Change-Id: I4ead566e4308d24fdb64e789a7ca0e3dbf0214fbmaster
parent
ebee511732
commit
57be3f7c40
|
@ -0,0 +1,9 @@
|
|||
py_binary(
|
||||
name = "sync",
|
||||
srcs = [
|
||||
"sync.py",
|
||||
],
|
||||
deps = [
|
||||
"@pip36//requests",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,216 @@
|
|||
"""
|
||||
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 requests
|
||||
|
||||
|
||||
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 AS1,AS2,AS3,...".format(sys.argv[0]))
|
||||
sys.exit(1)
|
||||
|
||||
password = sys.argv[1]
|
||||
members = [m.strip().upper() for m in sys.argv[2].split(',')]
|
||||
|
||||
for member in members:
|
||||
if not member.startswith('AS'):
|
||||
raise Exception('{} is not a valid ASN'.format(member))
|
||||
|
||||
if not all(c in string.digits for c in member[2:]):
|
||||
raise Exception('{} is not a valid ASN'.format(member))
|
||||
|
||||
sync_autnum(members, password)
|
||||
sync_asset(members, password)
|
Loading…
Reference in New Issue