kooshinlab / コーシンラボ

現役ネットワークエンジニアが、ネットワーク運用で必要になった技術の記事を書くブログです。

ネットワークエンジニアのためのIPアドレス表現 for Python

Pythonipaddressライブラリで、 ネットワークエンジニアがよく使うIPアドレスを表現する方法を例示します。

from ipaddress import ip_interface

>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

はじめに

PythonIPアドレスを取り扱う方法を調査してみました。 過去に、Rubyで調査したので、ほぼ同じことをできるか、Pythonで調査しました。

Python3.3から標準モジュールとしてipaddressライブラリが追加となっています。 より詳細な解説は、ライブラリのドキュメントを参照してください。

ここでは、Python3.7.4の環境で調査しました。

IPアドレスの表現方法

ipaddressライブラリには、IPアドレスを表現するために、3つの関数(ファクトリ関数)が定義されています。

ファクトリ関数 生成されるクラス 用途
ip_address() IPv4Address, IPv6Address IPv4/IPv6のホストアドレスを表現する。マスク情報は付加できない。
ip_network() IPv4Network, IPv6Network IPv4/IPv6のネットワークアドレスを表現する。IPプレフィックスとしてよく使う表現。ホストアドレスは表現できない。
ip_interface() IPv4Interface, IPv6Interface IPv4/IPv4のインタフェースアドレスを表現する。ルータのIPアドレスを扱うのならこっち。

ここでは、取り扱いやすいip_interface()関数を主で利用します。必要に応じて、他の関数を利用してください。 ip_interface()関数から生成されるIPv4Interfaceクラスから、IPv4AddressクラスとIPv4Networkクラスが生成できます。

from ipaddress import (
    ip_network,
    ip_address,
    ip_interface
)

# ホストアドレス(IPv4Address)
>>> ip_address("192.168.1.124")
IPv4Address('192.168.1.124')

# ネットワークアドレス(IPv4Network)
>>> ip_network("192.168.1.0/24")
IPv4Network('192.168.1.0/24')

# インタフェースアドレス(IPv4Interface)
>>> ip_interface("192.168.1.124/24")
IPv4Interface('192.168.1.124/24')


# ip_network()はネットワークアドレスを指定しないといけない
# ホストアドレスを指定すると、デフォルトはエラーとなる
>>> ip_network("192.168.1.124/24")
ValueError: 192.168.1.124/24 has host bits set

# strict=Falseを指定すると、ネットワークアドレスに変換する
>>> ip_network("192.168.1.124/24", strict=False)
IPv4Network('192.168.1.0/24')


# IPv4Interfaceクラスから、IPv4Addressクラスを生成
#  ip_interface() => ip_address()に変換
>>> ip = ip_interface("192.168.1.124/24")
>>> ip.ip
IPv4Address('192.168.1.124')

# IPv4Interfaceクラスから、IPv4Networkクラスを生成
#  ip_interface() => ip_network()に変換
>>> ip = ip_interface("192.168.1.124/24")
>>> ip.network
IPv4Network('192.168.1.0/24')
色々なIPアドレスの読み込み

ip_interface()関数を前提として、IPアドレスを読み込みます。

from ipaddress import ip_interface


# プレフィックス長を指定したIPアドレス
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

# プレフィックス長を省略すると/32になる
>>> ip = ip_interface("192.168.1.124")
>>> ip
IPv4Interface('192.168.1.124/32')
>>> str(ip)
'192.168.1.124/32'

# サブネットマスクで指定も可能
>>> ip = ip_interface("192.168.1.124/255.255.255.0")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

# 10進数からIPアドレスに変換
>>> ip = ip_interface(0)
>>> ip
IPv4Interface('0.0.0.0/32')
>>> str(ip)
'0.0.0.0/32'

# 16進数からIPアドレスに変換
>>> ip = ip_interface(0xffffffff)
>>> ip
IPv4Interface('255.255.255.255/32')
>>> str(ip)
'255.255.255.255/32'
よく使うIPアドレスの表現
from ipaddress import ip_interface


# IPアドレス指定
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')


# プレフィックス表記(1)
>>> str(ip)
'192.168.1.124/24'

# プレフィックス表記(2)
>>> ip.with_prefixlen
'192.168.1.124/24'

# サブネットマスク表記
>>> ip.with_netmask
'192.168.1.124/255.255.255.0'

# ホストアドレス
>>> ip.ip
IPv4Address('192.168.1.124')
>>> str(ip.ip)
'192.168.1.124'

# サブネットマスク
>>> ip.netmask
IPv4Address('255.255.255.0')
>>> str(ip.netmask)
'255.255.255.0'

# プレフィックス長
>>> ip.network.prefixlen
24

# ネットワークアドレス
>>> ip.network
IPv4Network('192.168.1.0/24')
>>> str(ip.network)
'192.168.1.0/24'
>>> ip.network.network_address
IPv4Address('192.168.1.0')
>>> str(ip.network.network_address)
'192.168.1.0'

# ホストマスク/ワイルドカードマスク
>>> ip.hostmask
IPv4Address('0.0.0.255')
>>> str(ip.hostmask)
'0.0.0.255'

# ブロードキャストアドレス(1)
>>> ip.network.broadcast_address
IPv4Address('192.168.1.255')
>>> str(ip.network.broadcast_address)
'192.168.1.255'

# ブロードキャストアドレス(2)
>>> ip_interface("0.0.0.0/0").network.broadcast_address
IPv4Address('255.255.255.255')
>>> str(ip_interface("0.0.0.0/0").network.broadcast_address)
'255.255.255.255'
その他の表現
from ipaddress import ip_interface


# IPアドレス指定
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')


# 数値
>>> int(ip)
3232235900

# DNS逆引きレコード
>>> ip.reverse_pointer
'124/24.1.168.192.in-addr.arpa'

# ホストアドレスの配列(ネットワークアドレスとブロードキャストを除く)
>>> ip.network.hosts()
<generator object _BaseNetwork.hosts at 0x7f87841d2430>
>>> list(ip.network.hosts())
[IPv4Address('192.168.1.1'), IPv4Address('192.168.1.2'), ~略~, IPv4Address('192.168.1.254')]


# 192.168.1.254~192.168.2.2までを表現
>>> ip = ip_interface("192.168.1.254/24")
>>> ip + 1
IPv4Interface('192.168.1.255/32')
>>> ip + 2
IPv4Interface('192.168.2.0/32')

>>> for i in range(5):
...     ip + i
...
IPv4Interface('192.168.1.254/32')
IPv4Interface('192.168.1.255/32')
IPv4Interface('192.168.2.0/32')
IPv4Interface('192.168.2.1/32')
IPv4Interface('192.168.2.2/32')
プレフィックスの表記

プレフィックスの計算には、ip_network()関数が便利なため、 ここでは、ip_network()を利用します。

サマライズする関数はcollapse_addresses()関数です。 collapse_addressesに、IPv4Network形式のアドレスリストを渡すことで、 適時サマライズしてくれます。

プレフィックスの分割は、IPv4Network.subnets()関数です。 現在のプレフィックス長に値を足す場合はprefixlen_diff=パラメータを使います。 新しいプレフィックス長にする場合はnew_prefix=パラメータを使います。

from ipaddress import (
    ip_network, # ★ip_network()関数を利用
    collapse_addresses,
)

# 複数のプレフィックスをサマライズする
>>> prefixes = map(ip_network, [
...     "192.168.0.0/25",
...     "192.168.0.128/25",
...     "192.168.1.0/24",
...     "192.168.3.0/24",
...     "192.168.4.0/24",
...     "192.168.5.0/26",
...     "192.168.128.0/22",
...     "192.168.132.0/22",
...     "192.168.128.0/21",
... ])
>>> summarized_networks = collapse_addresses(prefixes)
>>> list(summarized_networks)
[IPv4Network('192.168.0.0/23'), 
IPv4Network('192.168.3.0/24'), 
IPv4Network('192.168.4.0/24'), 
IPv4Network('192.168.5.0/26'), 
IPv4Network('192.168.128.0/21')]



# プレフィックスを分割する(プレフィックス長を増やす)
>>> prefix = ip_network("192.168.0.0/24")
# プレフィックス長に+1して、プレフィックスを分割(/24 => /25)
>>> list(prefix.subnets(prefixlen_diff=1))
[IPv4Network('192.168.0.0/25'), IPv4Network('192.168.0.128/25')]
# プレフィックス長に+2して、プレフィックスを分割(/24 => /26)
>>> list(prefix.subnets(prefixlen_diff=2))
[IPv4Network('192.168.0.0/26'), IPv4Network('192.168.0.64/26'), IPv4Network('192.168.0.128/26'), IPv4Network('192.168.0.192/26')]
# 新しいプレフィックス長を27にして、プレフィックスを分割(/24 => /27)
>>> list(prefix.subnets(new_prefix=27))
[IPv4Network('192.168.0.0/27'), IPv4Network('192.168.0.32/27'), IPv4Network('192.168.0.64/27'), IPv4Network('192.168.0.96/27'), IPv4Network('192.168.0.128/27'), IPv4Network('192.168.0.160/27'), IPv4Network('192.168.0.192/27'), IPv4Network('192.168.0.224/27')]


# プレフィックスのスーパーネットを求める(プレフィックス長を減らす)
>>> prefix = ip_network("192.168.0.0/24")
# プレフィックス長を-1にする(/24 => /23)
>>> prefix.supernet(prefixlen_diff=1)
IPv4Network('192.168.0.0/23')
# プレフィックス長を-2にする(/24 => /22)
>>> prefix.supernet(prefixlen_diff=2)
IPv4Network('192.168.0.0/22')
# プレフィックス長を/21にする(/24 => /21)
>>> prefix.supernet(new_prefix=21)
IPv4Network('192.168.0.0/21')


# プレフィックスから、特定のプレフィックスを除外する
>>> prefix1 = ip_network("192.168.0.0/24")
>>> prefix2 = ip_network("192.168.0.32/27")
>>> prefix1.address_exclude(prefix2)
<generator object _BaseNetwork.address_exclude at 0x00000286C6AD4BC8>
>>> list(prefix1.address_exclude(prefix2))
[IPv4Network('192.168.0.128/25'), IPv4Network('192.168.0.64/26'), IPv4Network('192.168.0.0/27')]
IPアドレスの判定
from ipaddress import ip_network


# プレフィックスが、サブネットの範囲内か判定(右辺に左辺が含まれるか?)
>>> ip1 = ip_network("192.168.0.0/24")
>>> ip2 = ip_network("192.168.0.32/27")
>>> ip1.subnet_of(ip2)
False
>>> ip2.subnet_of(ip1)
True


# プレフィックスが、スパーネットの範囲内か判定(左辺に右辺が含まれるか?)
>>> ip1 = ip_network("192.168.0.0/24")
>>> ip2 = ip_network("192.168.0.32/27")
>>> ip1.supernet_of(ip2)
True
>>> ip2.supernet_of(ip1)
False

各クラスで使える関数一覧

各クラス(IPv4Interface、IPv4Network、IPv4Address)に定義されている変数と関数一覧です。

from ipaddress import (
    ip_network,
    ip_address,
    ip_interface
)


# IPv4Interface
>>> ip_interface("192.168.1.124/24")
IPv4Interface('192.168.1.124/24')
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> ip.
ip.compressed      ip.is_global       ip.is_private      ip.netmask         ip.version
ip.exploded        ip.is_link_local   ip.is_reserved     ip.network         ip.with_hostmask
ip.hostmask        ip.is_loopback     ip.is_unspecified  ip.packed          ip.with_netmask
ip.ip              ip.is_multicast    ip.max_prefixlen   ip.reverse_pointer ip.with_prefixlen


# IPv4Network
>>> ip = ip_network("192.168.1.124/24", strict=False)
>>> ip
IPv4Network('192.168.1.0/24')
>>> ip.
ip.address_exclude(  ip.hosts(            ip.is_reserved       ip.overlaps(         ip.supernet_of(
ip.broadcast_address ip.is_global         ip.is_unspecified    ip.prefixlen         ip.version
ip.compare_networks( ip.is_link_local     ip.max_prefixlen     ip.reverse_pointer   ip.with_hostmask
ip.compressed        ip.is_loopback       ip.netmask           ip.subnet_of(        ip.with_netmask
ip.exploded          ip.is_multicast      ip.network_address   ip.subnets(          ip.with_prefixlen
ip.hostmask          ip.is_private        ip.num_addresses     ip.supernet(


# IPv4Address
>>> ip = ip_address("192.168.1.124")
>>> ip
IPv4Address('192.168.1.124')
>>> ip.
ip.compressed      ip.is_link_local   ip.is_private      ip.max_prefixlen   ip.version
ip.exploded        ip.is_loopback     ip.is_reserved     ip.packed
ip.is_global       ip.is_multicast    ip.is_unspecified  ip.reverse_pointer

付録:シスコルータのコンフィグを生成する

CSVファイルからコンフィグのパラメータを読み取り、 Jinja2テンプレートで、コンフィグを生成します。 今回は、インタフェースの設定と、OSPFの設定用のシスコルータのコンフィグを生成します。

入力CSVファイル:ospf_interfaces.csv

CSVのカラムは、下記の表のとおりです。

カラム名
interface インタフェース名、GigabitEthernet0/0
ipaddress インタフェースに割り当てるIPアドレスプレフィックス長付き)、192.168.0.1/24
ospfarea インタフェースが所属するOSPFエリア番号、0
interface,ipaddress,ospfarea
Loopback0,192.168.0.1/32,0
GigabitEthernet0/0,192.168.101.1/24,0
GigabitEthernet0/1,192.168.102.1/24,0
GigabitEthernet1/0,192.168.201.1/24,100
GigabitEthernet1/1,192.168.202.1/24,100
プログラム:ospf_template.py
import csv
from ipaddress import ip_interface
from jinja2 import Template


# CSVからパラメータ読み込み
# Excelで作成したCSVの場合、BOM付きのUTF-8で出力されるため、encoding="utf_8_sig"にする
params = []
with open("ospf_interfaces.csv", "r", encoding="utf_8_sig", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        ip = ip_interface(row["ipaddress"])
        print(row)
        param = {
            "ip": ip,
            "interface": row["interface"],
            "ospfarea": row["ospfarea"]
        }
        params.append(param)

# コンフィグのテンプレート
CONFIG_TEMPLATE = """
{%- for p in params %}
interface {{ p["interface"] }}
 ip address {{ p["ip"].ip }} {{ p["ip"].netmask }}
{% endfor %}

router ospf 1
 passive-interface default
{%- for p in params %}
 no passive-interface {{ p["interface"] }}
{%- endfor %}
{%- for p in params %}
 network {{ p["ip"].network.network_address }} {{ p["ip"].hostmask }} area {{ p["ospfarea"] }}
{%- endfor %}
"""

# テンプレートにパラメータを渡して、コンフィグに変換する
template = Template(CONFIG_TEMPLATE)
output = template.render({"params": params})
print("=" * 60)
print(output)
print("=" * 60)
実行結果

CSVファイルから各パラメータを読み取り、コンフィグが生成されました。

>python ospf_template.py
OrderedDict([('interface', 'Loopback0'), ('ipaddress', '192.168.0.1/32'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet0/0'), ('ipaddress', '192.168.101.1/24'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet0/1'), ('ipaddress', '192.168.102.1/24'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet1/0'), ('ipaddress', '192.168.201.1/24'), ('ospfarea', '100')])
OrderedDict([('interface', 'GigabitEthernet1/1'), ('ipaddress', '192.168.202.1/24'), ('ospfarea', '100')])
============================================================

interface Loopback0
 ip address 192.168.0.1 255.255.255.255

interface GigabitEthernet0/0
 ip address 192.168.101.1 255.255.255.0

interface GigabitEthernet0/1
 ip address 192.168.102.1 255.255.255.0

interface GigabitEthernet1/0
 ip address 192.168.201.1 255.255.255.0

interface GigabitEthernet1/1
 ip address 192.168.202.1 255.255.255.0


router ospf 1
 passive-interface default
 no passive-interface Loopback0
 no passive-interface GigabitEthernet0/0
 no passive-interface GigabitEthernet0/1
 no passive-interface GigabitEthernet1/0
 no passive-interface GigabitEthernet1/1
 network 192.168.0.1 0.0.0.0 area 0
 network 192.168.101.0 0.0.0.255 area 0
 network 192.168.102.0 0.0.0.255 area 0
 network 192.168.201.0 0.0.0.255 area 100
 network 192.168.202.0 0.0.0.255 area 100
============================================================

おわりに

ipaddressライブラリは、Python3.3から標準ライブラリとなっているので、非常に使いやすいと思います。 下記の参考文献の通り、便利でした!

参考文献