]> git.ipfire.org Git - location/libloc.git/blobdiff - src/python/location-importer.in
importer: Purge any redundant entries
[location/libloc.git] / src / python / location-importer.in
index 645144acc2e627ba9703233e0de937714be9c784..1467923475d2b1609ddf10e28f9f117900e4aec9 100644 (file)
@@ -22,6 +22,7 @@ import ipaddress
 import logging
 import math
 import re
+import socket
 import sys
 import telnetlib
 
@@ -45,6 +46,8 @@ class CLI(object):
                # Global configuration flags
                parser.add_argument("--debug", action="store_true",
                        help=_("Enable debug output"))
+               parser.add_argument("--quiet", action="store_true",
+                       help=_("Enable quiet mode"))
 
                # version
                parser.add_argument("--version", action="version",
@@ -65,9 +68,11 @@ class CLI(object):
                write.set_defaults(func=self.handle_write)
                write.add_argument("file", nargs=1, help=_("Database File"))
                write.add_argument("--signing-key", nargs="?", type=open, help=_("Signing Key"))
+               write.add_argument("--backup-signing-key", nargs="?", type=open, help=_("Backup Signing Key"))
                write.add_argument("--vendor", nargs="?", help=_("Sets the vendor"))
                write.add_argument("--description", nargs="?", help=_("Sets a description"))
                write.add_argument("--license", nargs="?", help=_("Sets the license"))
+               write.add_argument("--version", type=int, help=_("Database Format Version"))
 
                # Update WHOIS
                update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
@@ -89,11 +94,21 @@ class CLI(object):
                )
                update_overrides.set_defaults(func=self.handle_update_overrides)
 
+               # Import countries
+               import_countries = subparsers.add_parser("import-countries",
+                       help=_("Import countries"),
+               )
+               import_countries.add_argument("file", nargs=1, type=argparse.FileType("r"),
+                       help=_("File to import"))
+               import_countries.set_defaults(func=self.handle_import_countries)
+
                args = parser.parse_args()
 
-               # Enable debug logging
+               # Configure logging
                if args.debug:
-                       log.setLevel(logging.DEBUG)
+                       location.logger.set_level(logging.DEBUG)
+               elif args.quiet:
+                       location.logger.set_level(logging.WARNING)
 
                # Print usage if no action was given
                if not "func" in args:
@@ -142,6 +157,11 @@ class CLI(object):
                                CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
                                CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
 
+                               -- countries
+                               CREATE TABLE IF NOT EXISTS countries(
+                                       country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
+                               CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
+
                                -- networks
                                CREATE TABLE IF NOT EXISTS networks(network inet, country text);
                                CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
@@ -151,9 +171,10 @@ class CLI(object):
                                CREATE TABLE IF NOT EXISTS autnum_overrides(
                                        number bigint NOT NULL,
                                        name text,
-                                       is_anonymous_proxy boolean DEFAULT FALSE,
-                                       is_satellite_provider boolean DEFAULT FALSE,
-                                       is_anycast boolean DEFAULT FALSE
+                                       country text,
+                                       is_anonymous_proxy boolean,
+                                       is_satellite_provider boolean,
+                                       is_anycast boolean
                                );
                                CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
                                        ON autnum_overrides(number);
@@ -161,9 +182,9 @@ class CLI(object):
                                CREATE TABLE IF NOT EXISTS network_overrides(
                                        network inet NOT NULL,
                                        country text,
-                                       is_anonymous_proxy boolean DEFAULT FALSE,
-                                       is_satellite_provider boolean DEFAULT FALSE,
-                                       is_anycast boolean DEFAULT FALSE
+                                       is_anonymous_proxy boolean,
+                                       is_satellite_provider boolean,
+                                       is_anycast boolean
                                );
                                CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
                                        ON network_overrides(network);
@@ -175,10 +196,8 @@ class CLI(object):
                """
                        Compiles a database in libloc format out of what is in the database
                """
-               print(ns)
-
                # Allocate a writer
-               writer = location.Writer(ns.signing_key)
+               writer = location.Writer(ns.signing_key, ns.backup_signing_key)
 
                # Set all metadata
                if ns.vendor:
@@ -215,28 +234,96 @@ class CLI(object):
 
                # Select all known networks
                rows = self.db.query("""
+                       -- Get a (sorted) list of all known networks
+                       WITH known_networks AS (
+                                       SELECT network FROM announcements
+                               UNION
+                                       SELECT network FROM networks
+                               ORDER BY network
+                       )
+
+                       -- Return a list of those networks enriched with all
+                       -- other information that we store in the database
                        SELECT
-                               announcements.network AS network,
+                               DISTINCT ON (known_networks.network)
+                               known_networks.network AS network,
                                announcements.autnum AS autnum,
-                               (
-                                       SELECT networks.country FROM networks
-                                               WHERE announcements.network <<= networks.network
-                                               ORDER BY masklen(networks.network) DESC
-                                               LIMIT 1
+
+                               -- Country
+                               COALESCE(
+                                       (
+                                               SELECT country FROM network_overrides overrides
+                                                       WHERE announcements.network <<= overrides.network
+                                                       ORDER BY masklen(overrides.network) DESC
+                                                       LIMIT 1
+                                       ),
+                                       (
+                                               SELECT country FROM autnum_overrides overrides
+                                                       WHERE announcements.autnum = overrides.number
+                                       ),
+                                       networks.country
                                ) AS country,
 
                                -- Flags
-                               FALSE AS is_anonymous_proxy,
-                               FALSE AS is_satellite_provider,
-                               FALSE AS is_anycast
-                       FROM announcements
+                               COALESCE(
+                                       (
+                                               SELECT is_anonymous_proxy FROM network_overrides overrides
+                                                       WHERE announcements.network <<= overrides.network
+                                                       ORDER BY masklen(overrides.network) DESC
+                                                       LIMIT 1
+                                       ),
+                                       (
+                                               SELECT is_anonymous_proxy FROM autnum_overrides overrides
+                                                       WHERE announcements.autnum = overrides.number
+                                       ),
+                                       FALSE
+                               ) AS is_anonymous_proxy,
+                               COALESCE(
+                                       (
+                                               SELECT is_satellite_provider FROM network_overrides overrides
+                                                       WHERE announcements.network <<= overrides.network
+                                                       ORDER BY masklen(overrides.network) DESC
+                                                       LIMIT 1
+                                       ),
+                                       (
+                                               SELECT is_satellite_provider FROM autnum_overrides overrides
+                                                       WHERE announcements.autnum = overrides.number
+                                       ),
+                                       FALSE
+                               ) AS is_satellite_provider,
+                               COALESCE(
+                                       (
+                                               SELECT is_anycast FROM network_overrides overrides
+                                                       WHERE announcements.network <<= overrides.network
+                                                       ORDER BY masklen(overrides.network) DESC
+                                                       LIMIT 1
+                                       ),
+                                       (
+                                               SELECT is_anycast FROM autnum_overrides overrides
+                                                       WHERE announcements.autnum = overrides.number
+                                       ),
+                                       FALSE
+                               ) AS is_anycast,
+
+                               -- Must be part of returned values for ORDER BY clause
+                               masklen(announcements.network) AS sort_a,
+                               masklen(networks.network) AS sort_b
+                       FROM known_networks
+                               LEFT JOIN announcements ON known_networks.network <<= announcements.network
+                               LEFT JOIN networks ON known_networks.network <<= networks.network
+                       ORDER BY known_networks.network, sort_a DESC, sort_b DESC
                """)
 
                for row in rows:
                        network = writer.add_network(row.network)
 
-                       # Save AS & country
-                       network.asn, network.country_code = row.autnum, row.country
+                       # Save country
+                       if row.country:
+                               network.country_code = row.country
+
+                       # Save ASN
+                       if row.autnum:
+                               network.asn = row.autnum
 
                        # Set flags
                        if row.is_anonymous_proxy:
@@ -248,6 +335,15 @@ class CLI(object):
                        if row.is_anycast:
                                network.set_flag(location.NETWORK_FLAG_ANYCAST)
 
+               # Add all countries
+               log.info("Writing countries...")
+               rows = self.db.query("SELECT * FROM countries ORDER BY country_code")
+
+               for row in rows:
+                       c = writer.add_country(row.country_code)
+                       c.continent_code = row.continent_code
+                       c.name = row.name
+
                # Write everything to file
                log.info("Writing database to file...")
                for file in ns.file:
@@ -264,7 +360,7 @@ class CLI(object):
                                        ON COMMIT DROP;
                                CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
 
-                               CREATE TEMPORARY TABLE _organizations(handle text, name text)
+                               CREATE TEMPORARY TABLE _organizations(handle text, name text NOT NULL)
                                        ON COMMIT DROP;
                                CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
                        """)
@@ -277,8 +373,28 @@ class CLI(object):
                        self.db.execute("""
                                INSERT INTO autnums(number, name)
                                        SELECT _autnums.number, _organizations.name FROM _autnums
-                                               LEFT JOIN _organizations ON _autnums.organization = _organizations.handle
-                               ON CONFLICT (number) DO UPDATE SET name = excluded.name;
+                                               JOIN _organizations ON _autnums.organization = _organizations.handle
+                               ON CONFLICT (number) DO UPDATE SET name = excluded.name
+                       """)
+
+                       self.db.execute("""
+                               --- Purge any redundant entries
+                               CREATE TEMPORARY TABLE _garbage ON COMMIT DROP
+                               AS
+                               SELECT network FROM networks candidates
+                               WHERE EXISTS (
+                                       SELECT FROM networks
+                                       WHERE
+                                               networks.network << candidates.network
+                                       AND
+                                               networks.country = candidates.country
+                               );
+
+                               CREATE UNIQUE INDEX _garbage_search ON _garbage USING BTREE(network);
+
+                               DELETE FROM networks WHERE EXISTS (
+                                       SELECT FROM _garbage WHERE networks.network = _garbage.network
+                               );
                        """)
 
                # Download all extended sources
@@ -297,6 +413,10 @@ class CLI(object):
                if line.startswith("aut-num:"):
                        return self._parse_autnum_block(block)
 
+               # inetnum
+               if line.startswith("inet6num:") or line.startswith("inetnum:"):
+                       return self._parse_inetnum_block(block)
+
                # organisation
                elif line.startswith("organisation:"):
                        return self._parse_org_block(block)
@@ -326,6 +446,65 @@ class CLI(object):
                        autnum.get("asn"), autnum.get("org"),
                )
 
+       def _parse_inetnum_block(self, block):
+               logging.debug("Parsing inetnum block:")
+
+               inetnum = {}
+               for line in block:
+                       logging.debug(line)
+
+                       # Split line
+                       key, val = split_line(line)
+
+                       if key == "inetnum":
+                               start_address, delim, end_address = val.partition("-")
+
+                               # Strip any excess space
+                               start_address, end_address = start_address.rstrip(), end_address.strip()
+
+                               # Convert to IP address
+                               try:
+                                       start_address = ipaddress.ip_address(start_address)
+                                       end_address   = ipaddress.ip_address(end_address)
+                               except ValueError:
+                                       logging.warning("Could not parse line: %s" % line)
+                                       return
+
+                               # Set prefix to default
+                               prefix = 32
+
+                               # Count number of addresses in this subnet
+                               num_addresses = int(end_address) - int(start_address)
+                               if num_addresses:
+                                       prefix -= math.log(num_addresses, 2)
+
+                               inetnum["inetnum"] = "%s/%.0f" % (start_address, prefix)
+
+                       elif key == "inet6num":
+                               inetnum[key] = val
+
+                       elif key == "country":
+                               if val == "UNITED STATES":
+                                       val = "US"
+
+                               inetnum[key] = val.upper()
+
+               # Skip empty objects
+               if not inetnum:
+                       return
+
+               network = ipaddress.ip_network(inetnum.get("inet6num") or inetnum.get("inetnum"), strict=False)
+
+               # Bail out in case we have processed a non-public IP network
+               if network.is_private:
+                       logging.warning("Skipping non-globally routable network: %s" % network)
+                       return
+
+               self.db.execute("INSERT INTO networks(network, country) \
+                       VALUES(%s, %s) ON CONFLICT (network) DO UPDATE SET country = excluded.country",
+                       "%s" % network, inetnum.get("country"),
+               )
+
        def _parse_org_block(self, block):
                org = {}
                for line in block:
@@ -389,6 +568,7 @@ class CLI(object):
                        prefix = int(prefix)
                except:
                        log.warning("Invalid prefix: %s" % prefix)
+                       return
 
                # Fix prefix length for IPv4
                if type == "ipv4":
@@ -410,8 +590,93 @@ class CLI(object):
        def handle_update_announcements(self, ns):
                server = ns.server[0]
 
+               with self.db.transaction():
+                       if server.startswith("/"):
+                               self._handle_update_announcements_from_bird(server)
+                       else:
+                               self._handle_update_announcements_from_telnet(server)
+
+                       # Purge anything we never want here
+                       self.db.execute("""
+                               -- Delete default routes
+                               DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
+
+                               -- Delete anything that is not global unicast address space
+                               DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
+
+                               -- DELETE "current network" address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
+
+                               -- DELETE local loopback address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
+
+                               -- DELETE RFC 1918 address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
+
+                               -- DELETE test, benchmark and documentation address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
+
+                               -- DELETE CGNAT address space (RFC 6598)
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
+
+                               -- DELETE link local address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
+
+                               -- DELETE IPv6 to IPv4 (6to4) address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
+
+                               -- DELETE multicast and reserved address space
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
+                               DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
+
+                               -- Delete networks that are too small to be in the global routing table
+                               DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
+                               DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
+
+                               -- Delete any non-public or reserved ASNs
+                               DELETE FROM announcements WHERE NOT (
+                                       (autnum >= 1 AND autnum <= 23455)
+                                       OR
+                                       (autnum >= 23457 AND autnum <= 64495)
+                                       OR
+                                       (autnum >= 131072 AND autnum <= 4199999999)
+                               );
+
+                               -- Delete everything that we have not seen for 14 days
+                               DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
+                       """)
+
+       def _handle_update_announcements_from_bird(self, server):
+               # Pre-compile the regular expression for faster searching
+               route = re.compile(b"^\s(.+?)\s+.+?\[AS(.*?).\]$")
+
+               log.info("Requesting routing table from Bird (%s)" % server)
+
+               # Send command to list all routes
+               for line in self._bird_cmd(server, "show route"):
+                       m = route.match(line)
+                       if not m:
+                               log.debug("Could not parse line: %s" % line.decode())
+                               continue
+
+                       # Fetch the extracted network and ASN
+                       network, autnum = m.groups()
+
+                       # Insert it into the database
+                       self.db.execute("INSERT INTO announcements(network, autnum) \
+                               VALUES(%s, %s) ON CONFLICT (network) DO \
+                               UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
+                               network.decode(), autnum.decode(),
+                       )
+
+       def _handle_update_announcements_from_telnet(self, server):
                # Pre-compile regular expression for routes
-               #route = re.compile(b"^\*>?\s[\si]?([^\s]+)[.\s]*?(\d+)\si$", re.MULTILINE)
                route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
 
                with telnetlib.Telnet(server) as t:
@@ -420,8 +685,10 @@ class CLI(object):
                        #       t.set_debuglevel(10)
 
                        # Wait for console greeting
-                       greeting = t.read_until(b"> ")
-                       log.debug(greeting.decode())
+                       greeting = t.read_until(b"> ", timeout=30)
+                       if not greeting:
+                               log.error("Could not get a console prompt")
+                               return 1
 
                        # Disable pagination
                        t.write(b"terminal length 0\n")
@@ -430,76 +697,85 @@ class CLI(object):
                        t.read_until(b"> ")
 
                        # Fetch the routing tables
-                       with self.db.transaction():
-                               for protocol in ("ipv6", "ipv4"):
-                                       log.info("Requesting %s routing table" % protocol)
+                       for protocol in ("ipv6", "ipv4"):
+                               log.info("Requesting %s routing table" % protocol)
 
-                                       # Request the full unicast routing table
-                                       t.write(b"show bgp %s unicast\n" % protocol.encode())
+                               # Request the full unicast routing table
+                               t.write(b"show bgp %s unicast\n" % protocol.encode())
 
-                                       # Read entire header which ends with "Path"
-                                       t.read_until(b"Path\r\n")
+                               # Read entire header which ends with "Path"
+                               t.read_until(b"Path\r\n")
 
-                                       while True:
-                                               # Try reading a full entry
-                                               # Those might be broken across multiple lines but ends with i
-                                               line = t.read_until(b"i\r\n", timeout=5)
-                                               if not line:
-                                                       break
+                               while True:
+                                       # Try reading a full entry
+                                       # Those might be broken across multiple lines but ends with i
+                                       line = t.read_until(b"i\r\n", timeout=5)
+                                       if not line:
+                                               break
 
-                                               # Show line for debugging
-                                               #log.debug(repr(line))
+                                       # Show line for debugging
+                                       #log.debug(repr(line))
 
-                                               # Try finding a route in here
-                                               m = route.match(line)
-                                               if m:
-                                                       network, autnum = m.groups()
+                                       # Try finding a route in here
+                                       m = route.match(line)
+                                       if m:
+                                               network, autnum = m.groups()
 
-                                                       # Convert network to string
-                                                       network = network.decode()
+                                               # Convert network to string
+                                               network = network.decode()
 
-                                                       # Convert AS number to integer
-                                                       autnum = int(autnum)
+                                               # Append /24 for IPv4 addresses
+                                               if not "/" in network and not ":" in network:
+                                                       network = "%s/24" % network
 
-                                                       log.info("Found announcement for %s by %s" % (network, autnum))
+                                               # Convert AS number to integer
+                                               autnum = int(autnum)
 
-                                                       self.db.execute("INSERT INTO announcements(network, autnum) \
-                                                               VALUES(%s, %s) ON CONFLICT (network) DO \
-                                                               UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
-                                                               network, autnum,
-                                                       )
+                                               log.info("Found announcement for %s by %s" % (network, autnum))
+
+                                               self.db.execute("INSERT INTO announcements(network, autnum) \
+                                                       VALUES(%s, %s) ON CONFLICT (network) DO \
+                                                       UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
+                                                       network, autnum,
+                                               )
+
+                               log.info("Finished reading the %s routing table" % protocol)
 
-                                       log.info("Finished reading the %s routing table" % protocol)
+       def _bird_cmd(self, socket_path, command):
+               # Connect to the socket
+               s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+               s.connect(socket_path)
 
-                               # Purge anything we never want here
-                               self.db.execute("""
-                                       -- Delete default routes
-                                       DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
+               # Allocate some buffer
+               buffer = b""
 
-                                       -- Delete anything that is not global unicast address space
-                                       DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
+               # Send the command
+               s.send(b"%s\n" % command.encode())
 
-                                       -- DELETE RFC1918 address space
-                                       DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
-                                       DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
-                                       DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
+               while True:
+                       # Fill up the buffer
+                       buffer += s.recv(4096)
 
-                                       -- Delete networks that are too small to be in the global routing table
-                                       DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
-                                       DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
+                       while True:
+                               # Search for the next newline
+                               pos = buffer.find(b"\n")
 
-                                       -- Delete any non-public or reserved ASNs
-                                       DELETE FROM announcements WHERE NOT (
-                                               (autnum >= 1 AND autnum <= 23455)
-                                               OR
-                                               (autnum >= 23457 AND autnum <= 64495)
-                                               OR
-                                               (autnum >= 131072 AND autnum <= 4199999999)
-                                       );
+                               # If we cannot find one, we go back and read more data
+                               if pos <= 0:
+                                       break
 
-                                       -- Delete everything that we have not seen for 14 days
-                                       DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
-                               """)
+                               # Cut after the newline character
+                               pos += 1
+
+                               # Split the line we want and keep the rest in buffer
+                               line, buffer = buffer[:pos], buffer[pos:]
+
+                               # Look for the end-of-output indicator
+                               if line == b"0000 \n":
+                                       return
+
+                               # Otherwise return the line
+                               yield line
 
        def handle_update_overrides(self, ns):
                with self.db.transaction():
@@ -523,6 +799,11 @@ class CLI(object):
                                                                log.warning("Invalid IP network: %s: %s" % (network, e))
                                                                continue
 
+                                                       # Prevent that we overwrite all networks
+                                                       if network.prefixlen == 0:
+                                                               log.warning("Skipping %s: You cannot overwrite default" % network)
+                                                               continue
+
                                                        self.db.execute("""
                                                                INSERT INTO network_overrides(
                                                                        network,
@@ -530,17 +811,17 @@ class CLI(object):
                                                                        is_anonymous_proxy,
                                                                        is_satellite_provider,
                                                                        is_anycast
-                                                               ) VALUES (%s, %s, %s, %s)
+                                                               ) VALUES (%s, %s, %s, %s, %s)
                                                                ON CONFLICT (network) DO NOTHING""",
                                                                "%s" % network,
                                                                block.get("country"),
-                                                               block.get("is-anonymous-proxy") == "yes",
-                                                               block.get("is-satellite-provider") == "yes",
-                                                               block.get("is-anycast") == "yes",
+                                                               self._parse_bool(block, "is-anonymous-proxy"),
+                                                               self._parse_bool(block, "is-satellite-provider"),
+                                                               self._parse_bool(block, "is-anycast"),
                                                        )
 
-                                               elif type == "autnum":
-                                                       autnum = block.get("autnum")
+                                               elif type == "aut-num":
+                                                       autnum = block.get("aut-num")
 
                                                        # Check if AS number begins with "AS"
                                                        if not autnum.startswith("AS"):
@@ -554,20 +835,67 @@ class CLI(object):
                                                                INSERT INTO autnum_overrides(
                                                                        number,
                                                                        name,
+                                                                       country,
                                                                        is_anonymous_proxy,
                                                                        is_satellite_provider,
                                                                        is_anycast
-                                                               ) VALUES(%s, %s, %s, %s, %s)
+                                                               ) VALUES(%s, %s, %s, %s, %s, %s)
                                                                ON CONFLICT DO NOTHING""",
-                                                               autnum, block.get("name"),
-                                                               block.get("is-anonymous-proxy") == "yes",
-                                                               block.get("is-satellite-provider") == "yes",
-                                                               block.get("is-anycast") == "yes",
+                                                               autnum,
+                                                               block.get("name"),
+                                                               block.get("country"),
+                                                               self._parse_bool(block, "is-anonymous-proxy"),
+                                                               self._parse_bool(block, "is-satellite-provider"),
+                                                               self._parse_bool(block, "is-anycast"),
                                                        )
 
                                                else:
                                                        log.warning("Unsupport type: %s" % type)
 
+       @staticmethod
+       def _parse_bool(block, key):
+               val = block.get(key)
+
+               # There is no point to proceed when we got None
+               if val is None:
+                       return
+
+               # Convert to lowercase
+               val = val.lower()
+
+               # True
+               if val in ("yes", "1"):
+                       return True
+
+               # False
+               if val in ("no", "0"):
+                       return False
+
+               # Default to None
+               return None
+
+       def handle_import_countries(self, ns):
+               with self.db.transaction():
+                       # Drop all data that we have
+                       self.db.execute("TRUNCATE TABLE countries")
+
+                       for file in ns.file:
+                               for line in file:
+                                       line = line.rstrip()
+
+                                       # Ignore any comments
+                                       if line.startswith("#"):
+                                               continue
+
+                                       try:
+                                               country_code, continent_code, name = line.split(maxsplit=2)
+                                       except:
+                                               log.warning("Could not parse line: %s" % line)
+                                               continue
+
+                                       self.db.execute("INSERT INTO countries(country_code, name, continent_code) \
+                                               VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code, name, continent_code)
+
 
 def split_line(line):
        key, colon, val = line.partition(":")