[python]路由表脚本生成程序


老实说这是老东西了,自己之前用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

完整代码在这里