Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 1 | """ |
| 2 | Updates IRR objects in RIPE for the CCCamp IX. |
| 3 | |
| 4 | This, given a mntner password and a list of ASNs taking part in the IXP will |
| 5 | ensure the right IRR objects are present: |
| 6 | |
| 7 | - an aut-num containing import/export RPSL rules |
| 8 | - an as-set containing all member ASs. |
| 9 | |
| 10 | |
| 11 | """ |
| 12 | |
| 13 | import difflib |
| 14 | import string |
| 15 | import sys |
| 16 | import time |
| 17 | |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 18 | import grpc |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 19 | import requests |
| 20 | |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 21 | from bgpwtf.cccampix.proto import ix_pb2 as ipb |
| 22 | from bgpwtf.cccampix.proto import ix_pb2_grpc as ipb_grpc |
| 23 | |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 24 | |
| 25 | class IRRObject: |
| 26 | """An IRR object from RIPE.""" |
| 27 | TYPE = None |
| 28 | def __init__(self, fields=None): |
| 29 | self.fields = fields or [] |
| 30 | |
| 31 | def add(self, k, v): |
| 32 | self.fields.append((k, v)) |
| 33 | |
| 34 | def render(self): |
| 35 | """Render to IRR format.""" |
| 36 | return '\n'.join('{:16s}{}'.format(k+":", v) for k, v in self.fields) + "\n" |
| 37 | |
| 38 | @classmethod |
| 39 | def from_ripe(cls, v): |
| 40 | """Download this object from the RIPE REST API.""" |
| 41 | if cls.TYPE is None: |
| 42 | raise Exception('cannot fetch untyped IRRObject') |
| 43 | |
| 44 | r = requests.get('https://rest.db.ripe.net/ripe/{}/{}.json'.format(cls.TYPE, v)) |
| 45 | d = r.json() |
| 46 | |
| 47 | obj = d['objects']['object'][0] |
| 48 | assert obj['type'] == cls.TYPE |
| 49 | assert obj['primary-key']['attribute'][0]['value'] == v |
| 50 | |
| 51 | attrs = obj['attributes']['attribute'] |
| 52 | |
| 53 | fields = [] |
| 54 | for attr in attrs: |
| 55 | # Skip metadata. |
| 56 | if attr['name'] in ('created', 'last-modified'): |
| 57 | continue |
| 58 | fields.append((attr['name'], attr['value'])) |
| 59 | |
| 60 | return cls(fields) |
| 61 | |
| 62 | def send_to_ripe(self, password): |
| 63 | """Update this object (or create new) in RIPE using the SyncUpdates API.""" |
| 64 | res = self.render() |
| 65 | res += 'password: {}\n'.format(password) |
| 66 | |
| 67 | data = { |
| 68 | 'DATA': res, |
| 69 | } |
| 70 | r = requests.post('http://syncupdates.db.ripe.net/', files=data) |
| 71 | res = r.text |
| 72 | if 'Modify SUCCEEDED' not in res: |
| 73 | print(res) |
| 74 | raise Exception("Unexpected result from RIPE syncupdates") |
| 75 | |
| 76 | |
| 77 | banner = [ |
| 78 | ('remarks', r".--------------------------------------."), |
| 79 | ('remarks', r"| _ _ __ |"), |
| 80 | ('remarks', r"| | |__ __ _ _ ____ _| |_ / _| |"), |
| 81 | ('remarks', r"| | '_ \ / _` | '_ \ \ /\ / / __| |_ |"), |
| 82 | ('remarks', r"| | |_) | (_| | |_) \ V V /| |_| _| |"), |
| 83 | ('remarks', r"| |_.__/ \__, | .__(_)_/\_/ \__|_| |"), |
| 84 | ('remarks', r"| |___/|_| |"), |
| 85 | ('remarks', r"|--------------------------------------|"), |
| 86 | ('remarks', r"| CCCamp2019 Internet Exchange Point |"), |
| 87 | ('remarks', r"'--------------------------------------'"), |
| 88 | ('remarks', r''), |
| 89 | ('remarks', r'// 21. - 25. August 2019'), |
| 90 | ('remarks', r'// Ziegeleipark Mildenberg, Zehdenick, Germany, Earth, Milky Way'), |
| 91 | ('remarks', r'// Join us: https://bgp.wtf/cccamp19'), |
| 92 | ('remarks', r''), |
| 93 | ] |
| 94 | |
| 95 | |
| 96 | class IXPAutNum(IRRObject): |
| 97 | """An aut-num (AS) object.""" |
| 98 | TYPE = 'aut-num' |
| 99 | @classmethod |
| 100 | def make_for_members(cls, members): |
| 101 | fields = [ |
| 102 | ('aut-num', 'AS208521'), |
| 103 | ('as-name', 'BGPWTF-CCCAMP19-IX'), |
| 104 | ] + banner + [ |
| 105 | ('remarks', '// Current members:'), |
| 106 | ] |
| 107 | |
| 108 | for member in sorted(list(members)): |
| 109 | fields.append(('import', 'from {} accept {}'.format(member, member))) |
| 110 | fields.append(('export', 'to {} announce AS-CCCAMP19-IX'.format(member))) |
| 111 | |
| 112 | fields += [ |
| 113 | ('remarks', ''), |
| 114 | ('remarks', '// Abuse: noc@hackerspace.pl'), |
| 115 | ('org', 'ORG-SH103-RIPE'), |
| 116 | ('admin-c', 'HACK2-RIPE'), |
| 117 | ('tech-c', 'HACK2-RIPE'), |
| 118 | ('status', 'ASSIGNED'), |
| 119 | ('mnt-by', 'RIPE-NCC-END-MNT'), |
| 120 | ('mnt-by', 'BGPWTF-AUTOMATION'), |
| 121 | ('mnt-by', 'pl-hs-1-mnt'), |
| 122 | ('source', 'RIPE'), |
| 123 | ] |
| 124 | |
| 125 | return cls(fields) |
| 126 | |
| 127 | |
| 128 | class ASSet(IRRObject): |
| 129 | """An as-set object.""" |
| 130 | TYPE = 'as-set' |
| 131 | @classmethod |
| 132 | def make_for_members(cls, members): |
| 133 | fields = [ |
| 134 | ('as-set', 'AS-CCCAMP19-IX'), |
| 135 | ('admin-c', 'HACK2-RIPE'), |
| 136 | ] + banner + [ |
| 137 | ('remarks', '// Current members:'), |
| 138 | ] |
| 139 | |
| 140 | for member in sorted(list(members)): |
| 141 | fields.append(('members', member)) |
| 142 | |
| 143 | fields += [ |
| 144 | ('remarks', ''), |
| 145 | ('remarks', '// Abuse: noc@hackerspace.pl'), |
| 146 | ('tech-c', 'HACK2-RIPE'), |
| 147 | ('mnt-by', 'BGPWTF-AUTOMATION'), |
| 148 | ('mnt-by', 'pl-hs-1-mnt'), |
| 149 | ('source', 'RIPE'), |
| 150 | ] |
| 151 | |
| 152 | return cls(fields) |
| 153 | |
| 154 | |
| 155 | def sync(want, got, password, force): |
| 156 | """Sync an object if there is a diff to its current state.""" |
| 157 | wantr = want.render().split('\n') |
| 158 | gotr = got.render().split('\n') |
| 159 | |
| 160 | d = list(difflib.unified_diff(gotr, wantr, fromfile='got', tofile='want')) |
| 161 | fields_diff = set() |
| 162 | for dd in d: |
| 163 | if dd.startswith('---'): |
| 164 | continue |
| 165 | if dd.startswith('+++'): |
| 166 | continue |
| 167 | if not dd.startswith('+') and not dd.startswith('-'): |
| 168 | continue |
| 169 | |
| 170 | field = dd[1:].split(':')[0] |
| 171 | fields_diff.add(field) |
| 172 | |
| 173 | # We ignore remarks field changes, because the RIPE API returns us them always |
| 174 | # mangled (with spaces missing in the ASCII art). |
| 175 | if list(fields_diff) == ['remarks'] and not force: |
| 176 | print('No changes.') |
| 177 | return |
| 178 | |
| 179 | if force: |
| 180 | print('Forcing update') |
| 181 | else: |
| 182 | print('Diff:') |
| 183 | print('\n'.join(d)) |
| 184 | want.send_to_ripe(password) |
| 185 | print('Updated.') |
| 186 | |
| 187 | |
| 188 | def sync_autnum(members, password, force=False): |
| 189 | print('Syncing aut-num...') |
| 190 | want = IXPAutNum.make_for_members(members) |
| 191 | got = IXPAutNum.from_ripe('AS208521') |
| 192 | sync(want, got, password, force) |
| 193 | |
| 194 | |
| 195 | |
| 196 | def sync_asset(members, password, force=False): |
| 197 | print('Syncing as-set...') |
| 198 | want = ASSet.make_for_members(members) |
| 199 | got = ASSet.from_ripe('AS-CCCAMP19-IX') |
| 200 | sync(want, got, password, force) |
| 201 | |
| 202 | |
| 203 | |
| 204 | if __name__ == '__main__': |
| 205 | if len(sys.argv) != 3: |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 206 | print("Usage: {} <password> <verifier addr>".format(sys.argv[0])) |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 207 | sys.exit(1) |
| 208 | |
| 209 | password = sys.argv[1] |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 210 | verifier = sys.argv[2] |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 211 | |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 212 | chan = grpc.insecure_channel(verifier) |
| 213 | stub = ipb_grpc.VerifierStub(chan) |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 214 | |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 215 | req = ipb.PeerSummaryRequest() |
| 216 | peers = stub.PeerSummary(req) |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 217 | |
Serge Bazanski | 821fa5f | 2019-08-14 14:33:30 +0200 | [diff] [blame] | 218 | members = [] |
| 219 | for peer in peers: |
| 220 | if peer.check_status != peer.STATUS_OK: |
| 221 | continue |
| 222 | members.append('AS'+str(peer.peeringdb_info.asn)) |
| 223 | |
| 224 | print("Members:", members) |
Sergiusz Bazanski | 57be3f7 | 2019-07-19 20:58:50 +0200 | [diff] [blame] | 225 | sync_autnum(members, password) |
| 226 | sync_asset(members, password) |