]> git.ipfire.org Git - location/libloc.git/blame - src/python/location-importer.in
importer: Correctly sort the results
[location/libloc.git] / src / python / location-importer.in
CommitLineData
78ff0cf2
MT
1#!/usr/bin/python3
2###############################################################################
3# #
4# libloc - A library to determine the location of someone on the Internet #
5# #
6# Copyright (C) 2020 IPFire Development Team <info@ipfire.org> #
7# #
8# This library is free software; you can redistribute it and/or #
9# modify it under the terms of the GNU Lesser General Public #
10# License as published by the Free Software Foundation; either #
11# version 2.1 of the License, or (at your option) any later version. #
12# #
13# This library is distributed in the hope that it will be useful, #
14# but WITHOUT ANY WARRANTY; without even the implied warranty of #
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
16# Lesser General Public License for more details. #
17# #
18###############################################################################
19
20import argparse
6ffd06b5 21import ipaddress
78ff0cf2 22import logging
6ffd06b5
MT
23import math
24import re
78ff0cf2 25import sys
83d61c46 26import telnetlib
78ff0cf2
MT
27
28# Load our location module
29import location
29c6fa22 30import location.database
3192b66c 31import location.importer
78ff0cf2
MT
32from location.i18n import _
33
34# Initialise logging
35log = logging.getLogger("location.importer")
36log.propagate = 1
37
38class CLI(object):
39 def parse_cli(self):
40 parser = argparse.ArgumentParser(
41 description=_("Location Importer Command Line Interface"),
42 )
6ffd06b5 43 subparsers = parser.add_subparsers()
78ff0cf2
MT
44
45 # Global configuration flags
46 parser.add_argument("--debug", action="store_true",
47 help=_("Enable debug output"))
bc1f5f53
MT
48 parser.add_argument("--quiet", action="store_true",
49 help=_("Enable quiet mode"))
78ff0cf2
MT
50
51 # version
52 parser.add_argument("--version", action="version",
53 version="%(prog)s @VERSION@")
54
29c6fa22
MT
55 # Database
56 parser.add_argument("--database-host", required=True,
57 help=_("Database Hostname"), metavar=_("HOST"))
58 parser.add_argument("--database-name", required=True,
59 help=_("Database Name"), metavar=_("NAME"))
60 parser.add_argument("--database-username", required=True,
61 help=_("Database Username"), metavar=_("USERNAME"))
62 parser.add_argument("--database-password", required=True,
63 help=_("Database Password"), metavar=_("PASSWORD"))
64
0983f3dd
MT
65 # Write Database
66 write = subparsers.add_parser("write", help=_("Write database to file"))
67 write.set_defaults(func=self.handle_write)
68 write.add_argument("file", nargs=1, help=_("Database File"))
69 write.add_argument("--signing-key", nargs="?", type=open, help=_("Signing Key"))
1164d876 70 write.add_argument("--backup-signing-key", nargs="?", type=open, help=_("Backup Signing Key"))
0983f3dd
MT
71 write.add_argument("--vendor", nargs="?", help=_("Sets the vendor"))
72 write.add_argument("--description", nargs="?", help=_("Sets a description"))
73 write.add_argument("--license", nargs="?", help=_("Sets the license"))
b904896a 74 write.add_argument("--version", type=int, help=_("Database Format Version"))
0983f3dd 75
6ffd06b5
MT
76 # Update WHOIS
77 update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
78 update_whois.set_defaults(func=self.handle_update_whois)
79
83d61c46
MT
80 # Update announcements
81 update_announcements = subparsers.add_parser("update-announcements",
82 help=_("Update BGP Annoucements"))
83 update_announcements.set_defaults(func=self.handle_update_announcements)
84 update_announcements.add_argument("server", nargs=1,
85 help=_("Route Server to connect to"), metavar=_("SERVER"))
86
d7fc3057
MT
87 # Update overrides
88 update_overrides = subparsers.add_parser("update-overrides",
89 help=_("Update overrides"),
90 )
91 update_overrides.add_argument(
92 "files", nargs="+", help=_("Files to import"),
93 )
94 update_overrides.set_defaults(func=self.handle_update_overrides)
95
8084b33a
MT
96 # Import countries
97 import_countries = subparsers.add_parser("import-countries",
98 help=_("Import countries"),
99 )
100 import_countries.add_argument("file", nargs=1, type=argparse.FileType("r"),
101 help=_("File to import"))
102 import_countries.set_defaults(func=self.handle_import_countries)
103
78ff0cf2
MT
104 args = parser.parse_args()
105
bc1f5f53 106 # Configure logging
78ff0cf2 107 if args.debug:
f9de5e61 108 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
109 elif args.quiet:
110 location.logger.set_level(logging.WARNING)
78ff0cf2 111
6ffd06b5
MT
112 # Print usage if no action was given
113 if not "func" in args:
114 parser.print_usage()
115 sys.exit(2)
116
78ff0cf2
MT
117 return args
118
119 def run(self):
120 # Parse command line arguments
121 args = self.parse_cli()
122
29c6fa22 123 # Initialise database
6ffd06b5 124 self.db = self._setup_database(args)
29c6fa22 125
78ff0cf2 126 # Call function
6ffd06b5 127 ret = args.func(args)
78ff0cf2
MT
128
129 # Return with exit code
130 if ret:
131 sys.exit(ret)
132
133 # Otherwise just exit
134 sys.exit(0)
135
29c6fa22
MT
136 def _setup_database(self, ns):
137 """
138 Initialise the database
139 """
140 # Connect to database
141 db = location.database.Connection(
142 host=ns.database_host, database=ns.database_name,
143 user=ns.database_username, password=ns.database_password,
144 )
145
146 with db.transaction():
147 db.execute("""
83d61c46
MT
148 -- announcements
149 CREATE TABLE IF NOT EXISTS announcements(network inet, autnum bigint,
150 first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
151 last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP);
152 CREATE UNIQUE INDEX IF NOT EXISTS announcements_networks ON announcements(network);
153 CREATE INDEX IF NOT EXISTS announcements_family ON announcements(family(network));
154
6ffd06b5 155 -- autnums
0983f3dd 156 CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
6ffd06b5
MT
157 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
158
8084b33a
MT
159 -- countries
160 CREATE TABLE IF NOT EXISTS countries(
161 country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
162 CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
163
429a43d1 164 -- networks
83d61c46 165 CREATE TABLE IF NOT EXISTS networks(network inet, country text);
429a43d1 166 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
83d61c46 167 CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
d7fc3057
MT
168
169 -- overrides
170 CREATE TABLE IF NOT EXISTS autnum_overrides(
171 number bigint NOT NULL,
172 name text,
bd1aa6a1 173 country text,
b8e25b71
MT
174 is_anonymous_proxy boolean,
175 is_satellite_provider boolean,
176 is_anycast boolean
d7fc3057
MT
177 );
178 CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
179 ON autnum_overrides(number);
180
181 CREATE TABLE IF NOT EXISTS network_overrides(
182 network inet NOT NULL,
183 country text,
b8e25b71
MT
184 is_anonymous_proxy boolean,
185 is_satellite_provider boolean,
186 is_anycast boolean
d7fc3057
MT
187 );
188 CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
189 ON network_overrides(network);
29c6fa22
MT
190 """)
191
192 return db
193
0983f3dd
MT
194 def handle_write(self, ns):
195 """
196 Compiles a database in libloc format out of what is in the database
197 """
0983f3dd 198 # Allocate a writer
1164d876 199 writer = location.Writer(ns.signing_key, ns.backup_signing_key)
0983f3dd
MT
200
201 # Set all metadata
202 if ns.vendor:
203 writer.vendor = ns.vendor
204
205 if ns.description:
206 writer.description = ns.description
207
208 if ns.license:
209 writer.license = ns.license
210
211 # Add all Autonomous Systems
212 log.info("Writing Autonomous Systems...")
213
214 # Select all ASes with a name
6e97c44b
MT
215 rows = self.db.query("""
216 SELECT
217 autnums.number AS number,
218 COALESCE(
219 (SELECT overrides.name FROM autnum_overrides overrides
220 WHERE overrides.number = autnums.number),
221 autnums.name
222 ) AS name
223 FROM autnums
224 WHERE name <> %s ORDER BY number
225 """, "")
0983f3dd
MT
226
227 for row in rows:
228 a = writer.add_as(row.number)
229 a.name = row.name
230
231 # Add all networks
232 log.info("Writing networks...")
233
234 # Select all known networks
235 rows = self.db.query("""
5372d9c7
MT
236 -- Get a (sorted) list of all known networks
237 WITH known_networks AS (
238 SELECT network FROM announcements
239 UNION
240 SELECT network FROM networks
241 ORDER BY network
242 )
243
244 -- Return a list of those networks enriched with all
245 -- other information that we store in the database
0983f3dd 246 SELECT
5372d9c7
MT
247 DISTINCT ON (known_networks.network)
248 known_networks.network AS network,
0983f3dd 249 announcements.autnum AS autnum,
bd1aa6a1
MT
250
251 -- Country
252 COALESCE(
253 (
254 SELECT country FROM network_overrides overrides
255 WHERE announcements.network <<= overrides.network
256 ORDER BY masklen(overrides.network) DESC
257 LIMIT 1
258 ),
259 (
260 SELECT country FROM autnum_overrides overrides
261 WHERE announcements.autnum = overrides.number
262 ),
263 networks.country
264 ) AS country,
8e8555bb 265
0983f3dd 266 -- Flags
1422b5d4
MT
267 COALESCE(
268 (
269 SELECT is_anonymous_proxy FROM network_overrides overrides
270 WHERE announcements.network <<= overrides.network
271 ORDER BY masklen(overrides.network) DESC
272 LIMIT 1
273 ),
274 (
275 SELECT is_anonymous_proxy FROM autnum_overrides overrides
276 WHERE announcements.autnum = overrides.number
b8e25b71
MT
277 ),
278 FALSE
1422b5d4
MT
279 ) AS is_anonymous_proxy,
280 COALESCE(
281 (
282 SELECT is_satellite_provider FROM network_overrides overrides
283 WHERE announcements.network <<= overrides.network
284 ORDER BY masklen(overrides.network) DESC
285 LIMIT 1
286 ),
287 (
288 SELECT is_satellite_provider FROM autnum_overrides overrides
289 WHERE announcements.autnum = overrides.number
b8e25b71
MT
290 ),
291 FALSE
1422b5d4
MT
292 ) AS is_satellite_provider,
293 COALESCE(
294 (
295 SELECT is_anycast FROM network_overrides overrides
296 WHERE announcements.network <<= overrides.network
297 ORDER BY masklen(overrides.network) DESC
298 LIMIT 1
299 ),
300 (
301 SELECT is_anycast FROM autnum_overrides overrides
302 WHERE announcements.autnum = overrides.number
b8e25b71
MT
303 ),
304 FALSE
5372d9c7
MT
305 ) AS is_anycast,
306
307 -- Must be part of returned values for ORDER BY clause
17d49c4f
MT
308 masklen(announcements.network) AS sort_a,
309 masklen(networks.network) AS sort_b
5372d9c7
MT
310 FROM known_networks
311 LEFT JOIN announcements ON known_networks.network <<= announcements.network
312 LEFT JOIN networks ON known_networks.network <<= networks.network
17d49c4f 313 ORDER BY known_networks.network, sort_a DESC, sort_b DESC
0983f3dd
MT
314 """)
315
316 for row in rows:
317 network = writer.add_network(row.network)
318
5372d9c7
MT
319 # Save country
320 if row.country:
321 network.country_code = row.country
322
323 # Save ASN
324 if row.autnum:
325 network.asn = row.autnum
0983f3dd
MT
326
327 # Set flags
328 if row.is_anonymous_proxy:
329 network.set_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY)
330
331 if row.is_satellite_provider:
332 network.set_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER)
333
334 if row.is_anycast:
335 network.set_flag(location.NETWORK_FLAG_ANYCAST)
336
8084b33a
MT
337 # Add all countries
338 log.info("Writing countries...")
339 rows = self.db.query("SELECT * FROM countries ORDER BY country_code")
340
341 for row in rows:
342 c = writer.add_country(row.country_code)
343 c.continent_code = row.continent_code
344 c.name = row.name
345
0983f3dd
MT
346 # Write everything to file
347 log.info("Writing database to file...")
348 for file in ns.file:
349 writer.write(file)
350
6ffd06b5
MT
351 def handle_update_whois(self, ns):
352 downloader = location.importer.Downloader()
353
354 # Download all sources
0365119d
MT
355 with self.db.transaction():
356 # Create some temporary tables to store parsed data
357 self.db.execute("""
358 CREATE TEMPORARY TABLE _autnums(number integer, organization text)
359 ON COMMIT DROP;
360 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
361
362 CREATE TEMPORARY TABLE _organizations(handle text, name text)
363 ON COMMIT DROP;
364 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
365 """)
366
367 for source in location.importer.WHOIS_SOURCES:
6ffd06b5
MT
368 with downloader.request(source, return_blocks=True) as f:
369 for block in f:
370 self._parse_block(block)
371
0365119d
MT
372 self.db.execute("""
373 INSERT INTO autnums(number, name)
374 SELECT _autnums.number, _organizations.name FROM _autnums
375 LEFT JOIN _organizations ON _autnums.organization = _organizations.handle
376 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
377 """)
378
429a43d1
MT
379 # Download all extended sources
380 for source in location.importer.EXTENDED_SOURCES:
381 with self.db.transaction():
429a43d1
MT
382 # Download data
383 with downloader.request(source) as f:
384 for line in f:
385 self._parse_line(line)
386
6ffd06b5
MT
387 def _parse_block(self, block):
388 # Get first line to find out what type of block this is
389 line = block[0]
390
6ffd06b5 391 # aut-num
429a43d1 392 if line.startswith("aut-num:"):
6ffd06b5
MT
393 return self._parse_autnum_block(block)
394
395 # organisation
396 elif line.startswith("organisation:"):
397 return self._parse_org_block(block)
398
6ffd06b5 399 def _parse_autnum_block(self, block):
6ffd06b5
MT
400 autnum = {}
401 for line in block:
402 # Split line
403 key, val = split_line(line)
404
405 if key == "aut-num":
406 m = re.match(r"^(AS|as)(\d+)", val)
407 if m:
408 autnum["asn"] = m.group(2)
409
0365119d 410 elif key == "org":
6ffd06b5
MT
411 autnum[key] = val
412
413 # Skip empty objects
414 if not autnum:
415 return
416
417 # Insert into database
0365119d
MT
418 self.db.execute("INSERT INTO _autnums(number, organization) \
419 VALUES(%s, %s) ON CONFLICT (number) DO UPDATE SET \
420 organization = excluded.organization",
421 autnum.get("asn"), autnum.get("org"),
6ffd06b5
MT
422 )
423
6ffd06b5
MT
424 def _parse_org_block(self, block):
425 org = {}
426 for line in block:
427 # Split line
428 key, val = split_line(line)
429
0365119d 430 if key in ("organisation", "org-name"):
6ffd06b5
MT
431 org[key] = val
432
433 # Skip empty objects
434 if not org:
435 return
436
0365119d
MT
437 self.db.execute("INSERT INTO _organizations(handle, name) \
438 VALUES(%s, %s) ON CONFLICT (handle) DO \
439 UPDATE SET name = excluded.name",
440 org.get("organisation"), org.get("org-name"),
6ffd06b5
MT
441 )
442
429a43d1
MT
443 def _parse_line(self, line):
444 # Skip version line
445 if line.startswith("2"):
446 return
6ffd06b5 447
429a43d1
MT
448 # Skip comments
449 if line.startswith("#"):
450 return
6ffd06b5 451
429a43d1
MT
452 try:
453 registry, country_code, type, line = line.split("|", 3)
454 except:
455 log.warning("Could not parse line: %s" % line)
456 return
6ffd06b5 457
429a43d1
MT
458 # Skip any lines that are for stats only
459 if country_code == "*":
6ffd06b5
MT
460 return
461
429a43d1
MT
462 if type in ("ipv6", "ipv4"):
463 return self._parse_ip_line(country_code, type, line)
464
429a43d1
MT
465 def _parse_ip_line(self, country, type, line):
466 try:
467 address, prefix, date, status, organization = line.split("|")
468 except ValueError:
469 organization = None
470
471 # Try parsing the line without organization
472 try:
473 address, prefix, date, status = line.split("|")
474 except ValueError:
475 log.warning("Unhandled line format: %s" % line)
476 return
477
478 # Skip anything that isn't properly assigned
479 if not status in ("assigned", "allocated"):
480 return
481
482 # Cast prefix into an integer
483 try:
484 prefix = int(prefix)
485 except:
486 log.warning("Invalid prefix: %s" % prefix)
7177031f 487 return
429a43d1
MT
488
489 # Fix prefix length for IPv4
490 if type == "ipv4":
491 prefix = 32 - int(math.log(prefix, 2))
492
493 # Try to parse the address
494 try:
495 network = ipaddress.ip_network("%s/%s" % (address, prefix), strict=False)
496 except ValueError:
497 log.warning("Invalid IP address: %s" % address)
498 return
499
87b3e102
MT
500 self.db.execute("INSERT INTO networks(network, country) \
501 VALUES(%s, %s) ON CONFLICT (network) DO \
502 UPDATE SET country = excluded.country",
503 "%s" % network, country,
6ffd06b5
MT
504 )
505
83d61c46
MT
506 def handle_update_announcements(self, ns):
507 server = ns.server[0]
508
509 # Pre-compile regular expression for routes
83d61c46
MT
510 route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
511
512 with telnetlib.Telnet(server) as t:
513 # Enable debug mode
514 #if ns.debug:
515 # t.set_debuglevel(10)
516
517 # Wait for console greeting
fcd5b8b2
MT
518 greeting = t.read_until(b"> ", timeout=30)
519 if not greeting:
520 log.error("Could not get a console prompt")
521 return 1
83d61c46
MT
522
523 # Disable pagination
524 t.write(b"terminal length 0\n")
525
526 # Wait for the prompt to return
527 t.read_until(b"> ")
528
529 # Fetch the routing tables
530 with self.db.transaction():
531 for protocol in ("ipv6", "ipv4"):
532 log.info("Requesting %s routing table" % protocol)
533
534 # Request the full unicast routing table
535 t.write(b"show bgp %s unicast\n" % protocol.encode())
536
537 # Read entire header which ends with "Path"
538 t.read_until(b"Path\r\n")
539
540 while True:
541 # Try reading a full entry
542 # Those might be broken across multiple lines but ends with i
543 line = t.read_until(b"i\r\n", timeout=5)
544 if not line:
545 break
546
547 # Show line for debugging
548 #log.debug(repr(line))
549
550 # Try finding a route in here
551 m = route.match(line)
552 if m:
553 network, autnum = m.groups()
554
555 # Convert network to string
556 network = network.decode()
557
d773c1bc
MT
558 # Append /24 for IPv4 addresses
559 if not "/" in network and not ":" in network:
560 network = "%s/24" % network
561
83d61c46
MT
562 # Convert AS number to integer
563 autnum = int(autnum)
564
565 log.info("Found announcement for %s by %s" % (network, autnum))
566
567 self.db.execute("INSERT INTO announcements(network, autnum) \
568 VALUES(%s, %s) ON CONFLICT (network) DO \
569 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
570 network, autnum,
571 )
572
573 log.info("Finished reading the %s routing table" % protocol)
574
575 # Purge anything we never want here
576 self.db.execute("""
577 -- Delete default routes
578 DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
579
580 -- Delete anything that is not global unicast address space
581 DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
582
1d4e4e8f
PM
583 -- DELETE "current network" address space
584 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
585
cedee656
PM
586 -- DELETE local loopback address space
587 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
588
1d4e4e8f 589 -- DELETE RFC 1918 address space
83d61c46
MT
590 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
591 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
592 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
593
209c04b6
PM
594 -- DELETE test, benchmark and documentation address space
595 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
596 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
597 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
598 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
599 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
600
601 -- DELETE CGNAT address space (RFC 6598)
602 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
603
604 -- DELETE link local address space
605 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
606
607 -- DELETE IPv6 to IPv4 (6to4) address space
608 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
609
b89cee80
PM
610 -- DELETE multicast and reserved address space
611 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
612 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
613
83d61c46
MT
614 -- Delete networks that are too small to be in the global routing table
615 DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
616 DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
617
618 -- Delete any non-public or reserved ASNs
619 DELETE FROM announcements WHERE NOT (
620 (autnum >= 1 AND autnum <= 23455)
621 OR
622 (autnum >= 23457 AND autnum <= 64495)
623 OR
624 (autnum >= 131072 AND autnum <= 4199999999)
625 );
626
627 -- Delete everything that we have not seen for 14 days
628 DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
629 """)
630
d7fc3057
MT
631 def handle_update_overrides(self, ns):
632 with self.db.transaction():
633 # Drop all data that we have
634 self.db.execute("""
635 TRUNCATE TABLE autnum_overrides;
636 TRUNCATE TABLE network_overrides;
637 """)
638
639 for file in ns.files:
640 log.info("Reading %s..." % file)
641
642 with open(file, "rb") as f:
643 for type, block in location.importer.read_blocks(f):
644 if type == "net":
645 network = block.get("net")
646 # Try to parse and normalise the network
647 try:
648 network = ipaddress.ip_network(network, strict=False)
649 except ValueError as e:
650 log.warning("Invalid IP network: %s: %s" % (network, e))
651 continue
652
94dfab8c
MT
653 # Prevent that we overwrite all networks
654 if network.prefixlen == 0:
655 log.warning("Skipping %s: You cannot overwrite default" % network)
656 continue
657
d7fc3057
MT
658 self.db.execute("""
659 INSERT INTO network_overrides(
660 network,
661 country,
662 is_anonymous_proxy,
663 is_satellite_provider,
664 is_anycast
56f6587a 665 ) VALUES (%s, %s, %s, %s, %s)
d7fc3057
MT
666 ON CONFLICT (network) DO NOTHING""",
667 "%s" % network,
668 block.get("country"),
28d29b7c
MT
669 self._parse_bool(block, "is-anonymous-proxy"),
670 self._parse_bool(block, "is-satellite-provider"),
671 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
672 )
673
f476cdfd
MT
674 elif type == "aut-num":
675 autnum = block.get("aut-num")
d7fc3057
MT
676
677 # Check if AS number begins with "AS"
678 if not autnum.startswith("AS"):
679 log.warning("Invalid AS number: %s" % autnum)
680 continue
681
682 # Strip "AS"
683 autnum = autnum[2:]
684
685 self.db.execute("""
686 INSERT INTO autnum_overrides(
687 number,
688 name,
bd1aa6a1 689 country,
d7fc3057
MT
690 is_anonymous_proxy,
691 is_satellite_provider,
692 is_anycast
bd1aa6a1 693 ) VALUES(%s, %s, %s, %s, %s, %s)
d7fc3057 694 ON CONFLICT DO NOTHING""",
bd1aa6a1
MT
695 autnum,
696 block.get("name"),
697 block.get("country"),
28d29b7c
MT
698 self._parse_bool(block, "is-anonymous-proxy"),
699 self._parse_bool(block, "is-satellite-provider"),
700 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
701 )
702
703 else:
704 log.warning("Unsupport type: %s" % type)
705
28d29b7c
MT
706 @staticmethod
707 def _parse_bool(block, key):
708 val = block.get(key)
709
710 # There is no point to proceed when we got None
711 if val is None:
712 return
713
714 # Convert to lowercase
715 val = val.lower()
716
717 # True
718 if val in ("yes", "1"):
719 return True
720
721 # False
722 if val in ("no", "0"):
723 return False
724
725 # Default to None
726 return None
727
8084b33a
MT
728 def handle_import_countries(self, ns):
729 with self.db.transaction():
730 # Drop all data that we have
731 self.db.execute("TRUNCATE TABLE countries")
732
733 for file in ns.file:
734 for line in file:
735 line = line.rstrip()
736
737 # Ignore any comments
738 if line.startswith("#"):
739 continue
740
741 try:
742 country_code, continent_code, name = line.split(maxsplit=2)
743 except:
744 log.warning("Could not parse line: %s" % line)
745 continue
746
747 self.db.execute("INSERT INTO countries(country_code, name, continent_code) \
748 VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code, name, continent_code)
749
6ffd06b5
MT
750
751def split_line(line):
752 key, colon, val = line.partition(":")
753
754 # Strip any excess space
755 key = key.strip()
756 val = val.strip()
78ff0cf2 757
6ffd06b5 758 return key, val
78ff0cf2
MT
759
760def main():
761 # Run the command line interface
762 c = CLI()
763 c.run()
764
765main()