python ipaddressモジュールを使ってみたら便利だった話

[NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding) 21日目の記事です．
すいません日程を勘違いしてました・・・・．


ネットワークエンジニアが何かスクリプトを書く際，スクリプト内でipアドレスの操作や計算することがあるかと思います．
通常の数値と同じように計算をする場合，文字列型のipアドレスをパースして桁毎に重みをつけたIntに変換し・・・・という処理をしてからでなければ比較をすることができません．
また，ipv6は桁数も多く，さらに短縮形の表記など，単純なパースでは処理できない場合もあるため，自分で実装すると少し面倒です．

今回は，その面倒なipアドレスの処理をとっても簡単にできるipaddressモジュールを紹介します！
知っている人にとっては当たり前のことですが，私は最近までこの便利さを知らなかったので，まだ知らない人のために共有します

# 1.ipaddressモジュールについて

ipaddressモジュールはpython 3.3から正式実装された標準モジュールです．([PEP3144](https://www.python.org/dev/peps/pep-3144/))
Googleが開発した[ipaddrモジュール](https://pypi.python.org/pypi/ipaddr)を元としています．
勿論ipaddrモジュールを用いてもほぼ同様の動作が出来ますが，今回は標準モジュールとして搭載されている，インストール不要のipaddressを使ってみましょう．

# 2.環境
 * Python 3.4.3
 * MacOS X Yosemite

# 3.ipaddressモジュール内の関数

ipaddressモジュールは，`IPv4Address()`や`IPv6Address()`等，v4/v6専用の関数が用意されていますが，これを包括した`ip_address()`のような関数が用意されており，引数がv4/v6どちらなのかを気にせず使うことが出来ます．
ipaddressモジュールでは下記の3種類の関数を利用します．

* ip_address(address)
* ip_network(address, strict=True)
* ip_interface(address)

関数名通り，ipは`ip_address()`に，ネットワークアドレスは`ip_network()`に，Interfaceアドレスは`ip_interface()`に対応しています．

この関数を用いてipv4/v6型のオブジェクトを生成します．
さらにオプションの関数を使うことで様々な変換や計算を実施することが可能です．
個人的に便利で良く使った関数は下記のものです．

|関数|機能|
|---|---|
|*.version|オブジェクトがv4/v6どちらの情報をもっているかを判定する．v4なら4，v6なら6|
|*.compressed|v4は同等,v6は短縮表記の文字列を返す|
|*.exploded|v4は同等,v6は0を省略しない表記の文字列を返す|
|*.is_link_local|RFC3927のリンクローカル予約アドレスである場合はTrue|
|.with_with_prefixlen|prefix長形式のアドレスを文字列型で返す(10.10.10.1/24等)|
|.with_with_netmask|ネットワークマスク形式のアドレスを文字列型で返す(10.10.10.1 255.255.255.0等)|

また，各種演算子も利用することが出来ます．
IPアドレスをインクリメントする際にはipアドレスモジュールに単純に`+`,`-`演算子を付与して値を記述すれば，桁の上がり下がりは勝手に処理してくれます．

以下が実行例サンプルです．

```sample_ipaddress.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import ipaddress

if __name__ == "__main__":

    ip = ipaddress.ip_address('10.2.3.4')
    nw = ipaddress.ip_network('10.0.0.0/8')

    if ip in nw: # ip in network の1行でネットワークアドレスに含まれるか判別が可能
        print ("%s is included %s ."%(ip, nw))
    else :
        print ("%s isn't included %s ."%(ip, nw))

    print ("="*15)

    ip = ipaddress.ip_address('10.2.3.255')
    ipv6 = ipaddress.ip_address('fd00:260:301:104::104:1')
    ex_ipv6 = ipaddress.ip_address('fd00:0260:0301:0104:0000:0000:0104:1')
    
    #ipv4/v6どちらのアドレスが格納されているか確認
    print ("%s  is IPv%d"%(ip,ip.version))
    print ("%s  is IPv%d"%(ipv6,ipv6.version))
    print ("="*15)
    #アドレスのインクリメント
    ip = ip + 1
    ipv6 = ipv6 -2
    print ("increment ipv4 : %s"%ip)
    print ("decrement ipv6 : %s"%ipv6)
    print ("="*15)

    #ipv6の短縮形，非短縮系の変換
    print ("exploded ipv6 : %s"%ipv6.exploded)
    print ("compressed ipv6 : %s"%ex_ipv6.compressed)
    print ("="*15)

    #リンクローカルアドレスかどうかの判定
    print ("%s is linklocal : %s"%(ipv6,ipv6.is_link_local))

    ipv6_ll = ipaddress.ip_address('fe80::104:1')
    print ("%s is linklocal : %s"%(ipv6_ll,ipv6_ll.is_link_local))
```

```結果
% python3 ipaddress_sample.py
10.2.3.4 is included 10.0.0.0/8 .
===============
10.2.3.255  is IPv4
fd00:260:301:104::104:1  is IPv6
===============
increment ipv4 : 10.2.4.0
decrement ipv6 : fd00:260:301:104::103:ffff
===============
exploded ipv6 : fd00:0260:0301:0104:0000:0000:0103:ffff
compressed ipv6 : fd00:260:301:104::104:1
===============
fd00:260:301:104::103:ffff is linklocal : False
fe80::104:1 is linklocal : True
```


上記を見れば分かるように，ip_addressとip_networkの関数でipv4/v6の処理を判別してくれています．
このモジュールを使えばipv4とipv6で処理の場合分けをする手間を大きく省くことができます ．

# 4.例えばこんな時に使える(自分の場合）

自分はこういう時にこのモジュールがとっても役立ちました

* ルータのコンフィグからInterfaceとBGP情報を抜き出して管理票を作りたい
  * BGPのNeighborアドレスがどのInterfaceのアドレスと紐付いているかも自動で抜き出したい
  * BGPのNeighborアドレスとInterfaceアドレスと同じネットワークに属しているかを判定させて，同じネットワークなら紐付けを行いたい

といったような場合です．
コンフィグをパースして情報を抜き出して，InterfaceとBGPの対応付けのフェーズでipaddressモジュールが活躍します
下のようなコンフィグをパースしたとします

```ios-xr.conf

interface TenGigE0/0/0/0
 ipv4 address 10.10.10.10 255.255.255.0
 ipv6 nd suppress-ra
 ipv6 address fe80::222:1 link-local
 ipv6 address fd00:260:301:222::222:1/64
!

....Snip....

router bgp 64540
 neighbor 10.10.10.200
  remote-as 64601
  description "TEST-BGP-CONFIGURE"
  address-family ipv4 unicast
   route-policy as64601-sample in
   route-policy as64601-sample out
   next-hop-self
   soft-reconfiguration inbound always
  !
 !
 neighbor fd00:260:301:222::333:123
  remote-as 64601
  description "TEST-IPV6-BGP-CONFIGURE"
  address-family ipv6 unicast
   route-policy as64601-sample in
   route-policy as64601-sample out
   next-hop-self
   soft-reconfiguration inbound always
  !
 !
```

今回はこのコンフィグをうまくパースする方法については本筋でないので割愛します．
まずInterfaceのコンフィグをパースし，Interfaceの情報を保持，
その次にNeighborの情報を抜き出し，NeighborアドレスがInterface情報の中に含まれればInterfaceと紐付けを行います．

対応付けの部分は，前述でも同じようなコードをサンプルとして提示しましたが，
たったこれだけの記述で判別が可能です．
（パース部分は割愛しているため，IPはコンフィグと同じアドレスを直打ちしています）

```
    neighbor = ipaddress.ip_address('fd00:260:301:222::333:123') # NeighborのIP
    interface = ipaddress.ip_interface('fd00:260:301:222::222:1/64') # InterfaceのIP

    # interface.networkでネットワークネットワークアドレスへ変換してから比較
    if neighbor in interface.network:
        print ("%s is included %s ."%(neighbor, interface))
    else :
        print ("%s isn't included %s ."%(neighbor, interface))
```

```実行結果.log
fd00:260:301:222::333:123 is included fd00:260:301:222::222:1/64 .
```

# 5.まとめ

こういった便利なモジュールは，探してみると標準で搭載されていたり，サードパーティ製のものがあったりすることが多いです．
苦労して実装したはいいものの，実はもっと便利な機能を使ってすぐに実装できたということが
往々にしてあります．
自分で全てを実装することが悪いことではありませんが，便利で用途に合ったものがあるならば，活用したほうが格段に早く実装が可能です．
実装に熱中しすぎず「調査」もしっかりとしておきましょう．

当たり前のことかもしれませんが，自戒としてここに記しておきます（笑）