blob: 116158cbd26b95aede89f783890441e6627745e7 [file] [log] [blame]
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +02001"""
2Updates IRR objects in RIPE for the CCCamp IX.
3
4This, given a mntner password and a list of ASNs taking part in the IXP will
5ensure 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
13import difflib
14import string
15import sys
16import time
17
Serge Bazanski821fa5f2019-08-14 14:33:30 +020018import grpc
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +020019import requests
20
Serge Bazanski821fa5f2019-08-14 14:33:30 +020021from bgpwtf.cccampix.proto import ix_pb2 as ipb
22from bgpwtf.cccampix.proto import ix_pb2_grpc as ipb_grpc
23
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +020024
25class 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
77banner = [
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
96class 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
128class 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
155def 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
188def 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
196def 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
204if __name__ == '__main__':
205 if len(sys.argv) != 3:
Serge Bazanski821fa5f2019-08-14 14:33:30 +0200206 print("Usage: {} <password> <verifier addr>".format(sys.argv[0]))
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +0200207 sys.exit(1)
208
209 password = sys.argv[1]
Serge Bazanski821fa5f2019-08-14 14:33:30 +0200210 verifier = sys.argv[2]
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +0200211
Serge Bazanski821fa5f2019-08-14 14:33:30 +0200212 chan = grpc.insecure_channel(verifier)
213 stub = ipb_grpc.VerifierStub(chan)
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +0200214
Serge Bazanski821fa5f2019-08-14 14:33:30 +0200215 req = ipb.PeerSummaryRequest()
216 peers = stub.PeerSummary(req)
Sergiusz Bazanski57be3f72019-07-19 20:58:50 +0200217
Serge Bazanski821fa5f2019-08-14 14:33:30 +0200218 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 Bazanski57be3f72019-07-19 20:58:50 +0200225 sync_autnum(members, password)
226 sync_asset(members, password)