老实说这是老东西了,自己之前用scala写过,用ruby写过,这次是用python第二次实现,github上这种生成脚本也挺多的,自己全当是练手。
这个脚本其实就是把apnic提供的数据过滤出指定数据并解析然后生成路由表更新脚本的程序。用途相信各位也清楚,以下是自己重复造轮子的过程:
程序的主逻辑是读取apnic数据,用正则表达式过滤和解析,用解析出来的数据生成路由表修改脚本和恢复脚本。
首先是读取apnic数据。个人的方式是查找当前目录,然后脚本所在目录,如果再没有自动下载文件到脚本所在目录并返回。这里没有read on fly,即边下载边读,也没有比对并确保最新的逻辑。之后可以考虑加上。
APNIC_URL = 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) def locate_apnic_path(filename = 'delegated-apnic-latest'): if os.path.exists(filename): return filename apnic_path = os.path.join(SCRIPT_DIR, filename) if not os.path.exists(apnic_path): print 'Download apnic file from %s to %s' % (APNIC_URL, apnic_path) urllib.urlretrieve(APNIC_URL, apnic_path) return apnic_path
使用正则表达式过滤并解析的代码如下。解析结果是个CIDR格式,即网关的IP和对应子网掩码的路由前缀数。原始数据中IP后面的数据是子网主机数。
NET_PATTERN = re.compile( r'^apnic\|CN\|ipv4\|([0-9.]+)\|(\d+)\|\d+\|a.*$', re.IGNORECASE) def parse_cn_net(apnic_path): with open(apnic_path) as lines: for line in lines: # example: apnic|CN|ipv4|1.0.32.0|8192|20110412|allocated m = NET_PATTERN.match(line) if not m: continue # (IP, routing prefix), e.g (1.2.3.4, 24) means 1.2.3.4/24 yield (m.group(1), 32 - int(math.log(int(m.group(2)), 2)))
理论上接下就直接用解析出来的CIDR生成脚本了,但是考虑到部分网站或者内网也要纳入这个脚本,所以个人额外加了个excluding_net的配置,解析代码如下:
excluding_net中支持#开始的注释,忽略空行,允许直接host或者CIDR格式的单行配置。
def load_excluding_net(filename = 'excluding_net'): filepath = os.path.join(SCRIPT_DIR, filename) if os.path.exists(filepath): with open(filepath) as lines: for line in lines: if line.startswith('#'): continue # skip comment host_or_cidr = line.rstrip() # remove line break if not host_or_cidr: continue # skip empty line i = host_or_cidr.find('/') if i < 0: # host yield (host_or_cidr, 32) else: yield (host_or_cidr[:i], int(host_or_cidr[(i + 1):]))
excluding_net配置样例
# xnnyygn.in, xnnyygn.in 116.251.214.29/32 # internal net 192.168.1.0/24 192.168.3.0/24
接下来才是用上面解析出来的CIDR数据生成脚本。在撰写如何生成脚本的代码中个人考虑了好久,主要是因为不同平台生成的脚本文件名,命令模式不同。
首先我可以确定一点,默认网关IP是可以通过命令得到,而不应该在生成脚本时决定。否则笔记本一会儿有线一会儿无线网关地址变化了就麻烦了。
第二是如果默认网关地址变化,那么恢复默认网关时即恢复脚本时,需要通过类似/tmp/prev_gw来交互。
再加上脚本头,一些容错处理的话,程序中嵌入脚本文本会很多,自然而然会想到模板。不过我并没有使用通用的模板,直接分离为脚本头和route设置命令,同时增加输出脚本名配置用于定位脚本头模板和输出的脚本的名字。配置如下:
PLATFORM_META = { 'linux': ( 'linux-ip-pre-up', 'route add -net %s/%d gw ${PREV_GW} metric 5\n', 'linux-ip-down', 'route delete -net %s/%d gw ${PREV_GW} metric 5\n' ), 'mac': ( 'mac-ip-up', 'route add %s/%d ${PREV_GW}\n', 'mac-ip-down', 'route delete %s/%d ${PREV_GW}\n' ) }
这里字典的key是转换过的平台名,value的四个值分别是启动脚本名,启动脚本命令模板,恢复脚本名,恢复脚本命令模板。每个命令模板接受CIDR格式的tuple。
为了方便输出,个人用了个类来集合输出操作,这个类在初始化时就会把头模板填充进来。
HEADER_DIR = 'header' class RouteScript: def __init__(self, name, cmd_pattern): self.cmd_pattern = cmd_pattern self.script = open(name, 'w') header_path = os.path.join(SCRIPT_DIR, HEADER_DIR, name) if os.path.exists(header_path): with open(header_path) as f: self.script.write(f.read()) def append(self, cidr): self.script.write(self.cmd_pattern % cidr) def close(self): self.script.close()
基于上面类的输出代码,比没有类的时候要短小很多:
def generate(platform): config = PLATFORM_META[platform] up_script = RouteScript(config[0], config[1]) down_script = RouteScript(config[2], config[3]) for cidr in itertools.chain( load_excluding_net(), parse_cn_net(locate_apnic_path())): up_script.append(cidr) down_script.append(cidr) up_script.close() down_script.close()
最后脚本还做了一点平台支持的逻辑,即根据sys.platform决定用那个平台,比如linux2 -> linux, darwin -> mac。上面代码的platform是转换过的平台关键字。另外支持命令行第一个参数指定平台。
PLATFORM_MAPPING = { 'linux2': 'linux', 'darwin': 'mac' } def determine_platform(): if len(sys.argv) > 1: return sys.argv[1] key = sys.platform return PLATFORM_MAPPING.get(key) or key if __name__ == '__main__': platform = determine_platform() if platform in PLATFORM_META: generate(platform) else: print >>sys.stderr, 'unsupported platform [%s], or you can specify one' % platform
完整代码在这里。