#!/usr/bin/env python # Get latest IPv4 transfer stats from specified RIR, convert the JSON structure to # simple .csv format. As not all RIRs publish the same info (ripencc vs. does # not list source/recipient country, Lacnic tag RESOURCE_TRANSFER as Polic/Market) # this script's output sticks to the common demoninator from __future__ import print_function import argparse import codecs import json import sys import re try: # For Python 3.0 and later from urllib.request import urlopen except ImportError: # Fall back to Python 2's urllib2 from urllib2 import urlopen def int2ip(ip): """Convert an IPv4 address in integer to text representation """ ip1 = (ip & 0xFF000000) >> 24 ip2 = (ip & 0x00FF0000) >> 16 ip3 = (ip & 0x0000FF00) >> 8 ip4 = (ip & 0x000000FF) ipnum = "%d.%d.%d.%d" % (ip1, ip2, ip3, ip4) return(ipnum) def ip2int(text): """Convert an IPv4 address in text form to integer @param text: The IPv4 address @type text: string @returns: string """ parts = text.split('.') if len(parts) != 4: raise SyntaxError("%s: more than 4 bytes???" % text) for part in parts: if not part.isdigit(): raise SyntaxError("%s: not an IPv4 address" % text) #if len(part) > 1 and part[0] == '0': # No leading zeros # Why did I consider leading space syntax error? # seems to work just fine (and is present in ARIN data) #raise SyntaxError("%s: no leading zeroes" % text) ip = 0 for part in parts: ip = 256 * ip + int(part) return ip def blockSize(b): # compute the size of IPv4 block b # b is either a prefix # or a full IPv4 range m=re.match("[\d\.]+\/(\d+)",b) if m: # a prefix size = 2 ** (32 - int(m.group(1))) else: m=re.match("(^[\d\.]+)-([\d\.]+)$",b) if m: ip1 = ip2int(m.group(1)) ip2 = ip2int(m.group(2)) size = ip2 - ip1 + 1 if size < 0: raise RunTimeError("%s: first IP larger than second?" % b) else: raise RuntimeError("%s: cannot determine size; not an IPv4 block?" % b) return(size) def block2range(b): # convert block to dict # input: cidr prefix or range # output: tuple (startIP, size) m=re.match("^([\d\.]+)\/(\d+)",b) if m: # a prefix ip1 = ip2int(m.group(1)) size = 2 ** (32 - int(m.group(2))) else: m=re.match("(^[\d\.]+)-([\d\.]+)$",b) if m: ip1 = ip2int(m.group(1)) ip2 = ip2int(m.group(2)) size = ip2 - ip1 + 1 if size < 0: raise RunTimeError("%s: first IP larger than second?" % b) else: raise RuntimeError("%s: cannot determine size; not an IPv4 block?" % b) return((ip1,size)) def process_transaction(transaction,startDate,endDate): # Process one set of transfers # Write details of each transferred resource to stdout # Example input (ripencc): # # { # "transfer_date": "2015-11-23T19:00:00Z", # "asns": [], # "ip4nets": [ # { # "original_set": [ # null # ], # "transfer_set": [ # { # "start": "207.189.192.0", # "end": "207.189.207.255" # } # ] # } # ], # "ip6nets": [], # "type": "RESOURCE_TRANSFER", # "source_organization": { # "name": null # }, # "recipient_organization": { # "name": "Junet AB" # }, # "source_rir": "ARIN", # "recipient_rir": "RIPE NCC" # }, if not 'ip4nets' in transaction: # no IPv4 transferred here return if isinstance(transaction['ip4nets'], list): # RIPENCC has ip4nets as a list with either 0 or 1 elements # other RIRs have it as a dict which may or may not exist length = len(transaction['ip4nets']) if length == 0: # no IPv4 transferred here return elif length == 1: transferred_set = transaction['ip4nets'][0]['transfer_set'] else: print("ABORT: multiple sets of transfer_sets in 'ipv4nets'?",transaction['ipv4nets'],file=sys.stderr) sys.exit() else: transferred_set = transaction['ip4nets']['transfer_set'] ntransfers = len(transferred_set) if 'completion_date' in transaction: # LACNIC, at least until 20191214 date = transaction['completion_date'][0:10] else: date = transaction['transfer_date'][0:10] yyyymmdd = date[0:4] + date[5:7] + date[8:10] if (int(yyyymmdd) > endDate) or (int(yyyymmdd) < startDate): return # 20191214 - LACNIC still has a dict instead of string for 'type' #"type": { # "description": "Policy 2.3.2.18 Intra-RIR Transfer", # "signifier": "Policy/Market" #}, if 'signifier' in transaction['type']: # LACNIC ttype = transaction['type']['signifier'] else: ttype = transaction['type'] if ttype == 'Policy/Market': #LACNIC ttype = 'RESOURCE_TRANSFER' elif ttype == 'TRANSFER': #AFRINIC ttype = 'RESOURCE_TRANSFER' from_rir = transaction['source_rir'] to_rir = transaction['recipient_rir'] if 'source_organization' in transaction: sourceOrigName = transaction['source_organization']['name'] else: sourceCountry = '' sourceOrigName = '' if 'recipient_organization' in transaction: recipientOrigName = transaction['recipient_organization']['name'] else: recipientOrigName = '' blocks = {} totalsize = 0; for transfer in transferred_set: try: #LACNIC, RIPENCC startIP = transfer['start'] lastIP = transfer['end'] except: #ARIN, APNIC, AFRINIC startIP = transfer['start_address'] lastIP = transfer['end_address'] ip1 = ip2int(startIP) ip2 = ip2int(lastIP) startIP = int2ip(ip1) # ditch the leading zeroes size = ip2 - ip1 + 1 if size < 0: raise RunTimeError("%s: first IP larger than second?" % b) if sys.version_info.major == 2: # python2 needs explicit utf8 encoding to prevent it falling back # to ascii (and failing with error) when output is redirected # to a pipe or file print(("%s|%s|%s|%d|%s|%s|%s|%s|%s" % (date, startIP,lastIP,size,from_rir, to_rir, sourceOrigName,recipientOrigName, ttype)).encode('utf-8')) else: # python3 works as expected, no diff between output to terminal or # redirect to file. Adding explicit .encode('utf-8') results in backslah # encoded strings, which we do not want print(("%s|%s|%s|%d|%s|%s|%s|%s|%s" % (date, startIP,lastIP,size,from_rir, to_rir, sourceOrigName,recipientOrigName, ttype))) def main(): parser = argparse.ArgumentParser() parser.add_argument('-r','--rir',default="ripencc",help="RIR to fetch statistics from") parser.add_argument('-e','--enddate',default="20300000",help="end date, last day to consider, YYYYMMDD") parser.add_argument('-s','--startdate',default="20000000",help="start date, first day to includein stats, YYYYMMDD") args = parser.parse_args() endDate = int(args.enddate) startDate = int(args.startdate) rir=args.rir if rir=='ripencc': url="https://ftp.ripe.net/pub/stats/ripencc/transfers/transfers_latest.json" else: url="https://ftp." + rir + ".net/pub/stats/" + rir + "/transfers/transfers_latest.json" print("opening URL ",url,file=sys.stderr) transfers = urlopen(url) transfersT = transfers.read() transfersJ = json.loads(transfersT) #print json.dumps(transfersJ, indent=1) print("processing transfers", file=sys.stderr) print("%s|%s|%s|%s|%s|%s|%s|%s|%s" % ('date', 'start','end','size','source_rir', 'recipient_rir', 'source)origanisation','recipient_organisation', 'type')) for transaction in transfersJ['transfers']: process_transaction(transaction,startDate,endDate) if __name__ == "__main__": main()