はじめに
この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2024 (一枚目) の 17日目として投稿しています。
2017年版: https://qiita.com/advent-calendar/2017/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco
2019年版: https://qiita.com/advent-calendar/2019/cisco
2020年版: https://qiita.com/advent-calendar/2020/cisco
2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2
2021年版: https://qiita.com/advent-calendar/2021/cisco
2021年版(2枚目): https://qiita.com/advent-calendar/2021/cisco2
2022年版(1,2): https://qiita.com/advent-calendar/2022/cisco
2023年版: https://qiita.com/advent-calendar/2023/cisco
2024年版: https://qiita.com/advent-calendar/2024/cisco
スイッチポートにつながっているもの
データセンター内にサーバを設置する場合、最終的にはイーサネットでスイッチのポートへ接続します。どのポートに何が繋がっているのか、もちろん接続時にインベントリー情報として、何らかの方法で記録しておく事が望ましいのですが、運用上完璧にそれが出来ていないといった話を、(こそっと)聞くことがあります。
その可能性が完全に拭えない場合には、「このスイッチには何が繋がっているんだ!?」という問いに答えるには、ケーブルを追いかけるしかありません。しかしながら、本番環境のケーブルになど触りたくないのが本心で、また、技術的、政治的、担当者の成績的(笑)にもまずい状況ということになると思います。
ルータとスイッチの情報から追いかける
今日、イーサネット上で IP を使用して通信される場合がほとんどですので、一般的にもルータの ARP 情報とスイッチの Mac の情報を突き合わせることで、繋がっている機器の IP アドレスを特定することが可能です。
サーバがルータを経由して通信する際、ルータ上に IP-Mac アドレスのひも付き情報が ARP テーブルに記録され、通信中はそれが使用されます。また、スイッチ上の CAM MAC テーブルには、接続されているポートと Mac アドレスのひも付きが保存されています。それらの情報が今回の作業に必要なものとなります。
トポロジー
この記事を作成するにあたって、以下のようなトポロジーを CML 上で作成してみました。
昨今では Spine-Leaf な形が増えており、Cisco としてもぜひ ACI などを使用して頂きたいですが、コア-ディストリビューション-アクセスな、3階層モデルもまだまだ使用されており、それを想定したものです。
アクセススイッチは2つのルータへ接続されており、どちらかのリンクがダウンしても接続性を維持します。サーバ (alpine-0 等) が使用するデフォルトゲートウェイは、HSRP や VRRP などを使用して冗長化され、アップリンク方向へは r1 又は r2 が active/standby として使用されます。ダウンリンク方向では、上位ルータのルーティングに依存しますが、両方が active/active で使われる可能性があります。
Radkit Service の設置
管理ネットワーク上に Radkit Service のサーバを設置します。ルータ・スイッチは同管理ネットワークへ接続されており、radkit-service からは接続が可能となっています。
また、4つのネットワーク機器について接続情報が登録されていますので、Radkit-Service 経由でそれぞれの機器を操作することが出来るようになりました。
Radkit Client を使ってクラウドへ接続・操作する
Radkit Client をインストールすると、Python の radkit_client パッケージがインストールされます。それを使用しながら、基本的には以下の流れのコードを作成します。
- Cisco SSO に対して、ユーザ認証
- 指定された Service ID への接続
- Inventory 機器の操作
ユーザ認証についてはいくつか方法がありますが、本記事では token 認証を使用します。ユーザ認証を行った後、使用可能時間が有限な Token が配布されますので、以後はそれを使用することで、毎回認証を行う必要がなくなります。
以下の例では、接続後にオープンされる Websocket endpoint へ接続し、認証完了の通知を待っています。
from radkit_client.sync import Client
with Client.create() as client:
connect_data = client.oauth_connect_only(CLIENT_ID)
ws = create_connection(str(connect_data.token_url))
webbrowser.open(str(connect_data.sso_url))
token = json.loads(ws.recv())['access_token']
その後、得られた Token を使って Radkit Cloud へログインし、Service へと接続します。
token_client = client.access_token_login(token)
service = token_client.service(SERVICE_ID).wait()
以下の記事も参考にしてみてください。こちらでは、Certificate を使用したログイン方法について、紹介しております。
ルータから arp 情報を取得する
ルータ r1 と r2 より、show ip arp
の結果を取得します。実機上で取得される結果は、以下のようなものです。
例えば、IP アドレス 192.168.1.10
は、MAC アドレス 5254.000c.136e
と紐づいています。
r1#show ip arp
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.1.2.1 - 5254.0018.d825 ARPA GigabitEthernet2
Internet 10.10.0.1 - 5254.0017.caf5 ARPA GigabitEthernet1
Internet 10.10.0.3 103 aabb.cc00.0520 ARPA GigabitEthernet1
Internet 10.10.0.10 80 5254.0011.9600 ARPA GigabitEthernet1
Internet 192.168.1.1 - 5254.001c.0925 ARPA GigabitEthernet3
Internet 192.168.1.2 134 5254.000f.766d ARPA GigabitEthernet3
Internet 192.168.1.10 0 5254.000c.136e ARPA GigabitEthernet3
Internet 192.168.1.20 0 5254.0008.7123 ARPA GigabitEthernet3
Internet 192.168.1.254 - 0000.0c07.ac01 ARPA GigabitEthernet3
Internet 192.168.2.1 - 5254.000e.0b06 ARPA GigabitEthernet4
Internet 192.168.2.10 77 5254.0006.f4a8 ARPA GigabitEthernet4
r1#
これはテキストデータとなっており、パースして読み出す必要がありますが、PyAts の Genie が使用可能ですので、コードは簡易なもので完了します。
devs = service.inventory['r1'].singleton()
devs.add(service.inventory['r2'].singleton())
res = devs.exec("show ip arp").wait()
result = radkit_genie.parse(res, os='iosxe')
r1_arp =result['r1']['show ip arp'].data
r2_arp =result['r2']['show ip arp'].data
このように、パースした情報を Dictionary に格納してくれますので、読み出しは簡単です。
- r1_arp
{'_exclude': ['age'],
'interfaces': {
'GigabitEthernet1': {'ipv4': {'neighbors': {
'10.10.0.1': {'age': '-',
'ip': '10.10.0.1',
'link_layer_address': '5254.0017.caf5',
'origin': 'static',
'protocol': 'Internet',
'type': 'ARPA'},
'10.10.0.10': {'age': '0',
'ip': '10.10.0.10',
'link_layer_address': '5254.0011.9600',
'origin': 'dynamic',
'protocol': 'Internet',
'type': 'ARPA'},
'10.10.0.3': {'age': '106',
'ip': '10.10.0.3',
'link_layer_address': 'aabb.cc00.0520',
'origin': 'dynamic',
'protocol': 'Internet',
'type': 'ARPA'}}}},
...
Genie で対応しているコマンドについては、以下をご確認ください。
スイッチから mac テーブル情報を取得する
使用するコマンドは show mac address-table
です。
実機から取得する内容は以下です。
sw1#show mac address-table
Mac Address Table
-------------------------------------------
Vlan Mac Address Type Ports
---- ----------- -------- -----
10 0000.0c07.ac01 DYNAMIC Et0/0
10 5254.0008.7123 DYNAMIC Et1/0
10 5254.000c.136e DYNAMIC Et0/3
10 5254.000f.766d DYNAMIC Et0/1
10 5254.001c.0925 DYNAMIC Et0/0
Total Mac Addresses for this criterion: 5
sw1#
接続先デバイスやコマンドが違いますが、コードは上のルータで使用したものと同様です。
devs = service.inventory['sw1'].singleton()
devs.add(service.inventory['sw2'].singleton())
res = devs.exec("show mac address-table").wait()
result = radkit_genie.parse(res, os='iosxe')
sw1_mac_table = result['sw1']['show mac address-table'].data
sw2_mac_table = result['sw2']['show mac address-table'].data
以下のように Directory として取得できます。
{'mac_table':
{'vlans':
{'10': {'vlan': 10, 'mac_addresses': {
'0000.0c07.ac01': {
'mac_address': '0000.0c07.ac01',
'interfaces': {'Ethernet0/0': {
'interface': 'Ethernet0/0', 'entry_type': 'dynamic'}}},
'5254.0008.7123': {
'mac_address': '5254.0008.7123',
'interfaces': {'Ethernet1/0': {
'interface': 'Ethernet1/0', 'entry_type': 'dynamic'}}},
'5254.000c.136e': {
'mac_address': '5254.000c.136e',
'interfaces': {'Ethernet0/3': {
'interface': 'Ethernet0/3', 'entry_type': 'dynamic'}}},
'5254.000f.766d': {
'mac_address': '5254.000f.766d',
'interfaces': {'Ethernet0/1': {
'interface': 'Ethernet0/1', 'entry_type': 'dynamic'}}},
'5254.001c.0925': {
'mac_address': '5254.001c.0925',
'interfaces': {'Ethernet0/0': {
'interface': 'Ethernet0/0', 'entry_type': 'dynamic'}}}
...
ポートから見える mac アドレスに紐づく IP アドレスを探す
sw1
の Eth0/0
に繋がっているホストの IP アドレスは何でしょうか。
show mac address-table
の出力から、その mac アドレスは 5254.0008.7123
であることが確認できます。
その mac アドレスと紐づく IP アドレスは、show ip arp
コマンドの出力から、192.168.1.20
であることがわかります。
自動化
マッピングを探すこの作業、ポート数が少ない間は手作業でも可能ですが、それが多い場合には大変時間もかかります。48ポートラインカードが10枚くらい刺さってたりすると、、、😱
せっかく Directory にデータとして扱えるようになりましたので、このまま続けてマッピングのコードも追加します。Python の Dictionary を使っての作業のみとなるため、詳細な説明は致しませんが、これらのデータがあれば、目的が達成可能であることをご理解頂けたらと思います。
結果、以下が得られました。
('sw1', '0000.0c07.ac01', 'Ethernet0/0', '192.168.1.254')
('sw1', '5254.0008.7123', 'Ethernet1/0', '192.168.1.20')
('sw1', '5254.000c.136e', 'Ethernet0/3', '192.168.1.10')
('sw1', '5254.000f.766d', 'Ethernet0/1', '192.168.1.2')
('sw1', '5254.001c.0925', 'Ethernet0/0', '192.168.1.1')
('sw2', '0000.0c07.ac02', 'Ethernet0/1', '192.168.2.254')
('sw2', '5254.0006.f4a8', 'Ethernet0/3', '192.168.2.10')
('sw2', '5254.000e.0b06', 'Ethernet0/0', '192.168.2.1')
('sw2', '5254.0014.f150', 'Ethernet1/0', '192.168.2.20')
('sw2', '5254.0016.f3a7', 'Ethernet0/1', '192.168.2.2')
おわりに
ここまで書いておいて何ですが、この手法では完全なデータが作成出来るわけではありません。ルータの ARP 情報はデフォルトで 4時間、スイッチの Macテーブルはデフォルトで 300秒経つと消えてしまいます。常に通信が発生している環境であれば良いのですが、そうではない場合、事前に broadcast ping や、個々に ping をするなど、ルータ・スイッチに認識させるといったことが必要になるかも知れません。
今回作成したコード
参考情報としてご確認頂ければと思います。この方法が最善であるわけではありません。
$ cat test.py
import webbrowser
from radkit_client.sync import Client, DeviceDict
import radkit_genie
from websocket import create_connection
import time
import json
import os
from datetime import datetime, timedelta
CLIENT_ID = "<CCO ID>"
SERVICE_ID = "xxxx-xxxx-xxxx"
def is_token_valid(file_path: str, max_age_seconds: int = 3600) -> bool:
if not os.path.exists(file_path):
print(f"File '{file_path}' does not exist.")
return False
current_time = time.time()
file_mod_time = os.path.getmtime(file_path)
file_age = current_time - file_mod_time
if file_age < max_age_seconds:
age_str = str(timedelta(seconds=int(file_age)))
print(f"File '{file_path}' exists and is {age_str} old.")
return True
else:
age_str = str(timedelta(seconds=int(file_age)))
print(f"File '{file_path}' is too old ({age_str}). It must be less than {timedelta(seconds=max_age_seconds)} old.")
return False
with Client.create() as client:
if is_token_valid('token'):
with open('token', 'r') as f:
token = f.read()
else:
connect_data = client.oauth_connect_only(CLIENT_ID)
ws = create_connection(str(connect_data.token_url))
webbrowser.open(str(connect_data.sso_url))
token = json.loads(ws.recv())['access_token']
# print('Token:', token)
with open('token', 'w') as f:
f.write(token)
token_client = client.access_token_login(token)
service = token_client.service(SERVICE_ID).wait()
print(service.status)
print(service.inventory)
# ARP テーブル情報の取得
devs = service.inventory['r1'].singleton()
devs.add(service.inventory['r2'].singleton())
res = devs.exec("show ip arp").wait()
result = radkit_genie.parse(res, os='iosxe')
r1_arp =result['r1']['show ip arp'].data
r2_arp =result['r2']['show ip arp'].data
# 2つのルータ上の情報を合わせる
vlan10_arp = r1_arp['interfaces']['GigabitEthernet3']['ipv4']['neighbors']
vlan10_arp.update(r2_arp['interfaces']['GigabitEthernet3']['ipv4']['neighbors'])
vlan20_arp = r1_arp['interfaces']['GigabitEthernet4']['ipv4']['neighbors']
vlan20_arp.update(r2_arp['interfaces']['GigabitEthernet4']['ipv4']['neighbors'])
vlan10_mac_ip = [(x['link_layer_address'],x['ip']) for x in vlan10_arp.values()]
vlan20_mac_ip = [(x['link_layer_address'],x['ip']) for x in vlan20_arp.values()]
print(vlan10_mac_ip)
print(vlan20_mac_ip)
# MAC テーブル情報の取得
devs = service.inventory['sw1'].singleton()
devs.add(service.inventory['sw2'].singleton())
res = devs.exec("show mac address-table").wait()
result = radkit_genie.parse(res, os='iosxe')
sw1_mac_table = result['sw1']['show mac address-table'].data
sw2_mac_table = result['sw2']['show mac address-table'].data
print(sw1_mac_table)
print(sw2_mac_table)
vlan10 = [(x['mac_address'], [intf['interface'] for intf in x['interfaces'].values()]) for x in sw1_mac_table['mac_table']['vlans']['10']['mac_addresses'].values()]
print(vlan10)
vlan20 = [(x['mac_address'], [intf['interface'] for intf in x['interfaces'].values()]) for x in sw2_mac_table['mac_table']['vlans']['20']['mac_addresses'].values()]
print(vlan20)
# MAC アドレスをキーとして、2つの情報を結合する
sw1 = []
for mac in vlan10:
for arp in vlan10_mac_ip:
if mac[0] == arp[0]:
sw_mac_ip = ('sw1', mac[0], ",".join(mac[1]), arp[1])
sw1.append(sw_mac_ip)
sw2 = []
for mac in vlan20:
for arp in vlan20_mac_ip:
if mac[0] == arp[0]:
sw_mac_ip = ('sw2', mac[0], ",".join(mac[1]), arp[1])
sw2.append(sw_mac_ip)
# 表示
for x in sw1+sw2:
print(x)
免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、私の所属する組織の意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、私の所属する組織や他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Webサイトの利用に関するあらゆる責任から私の所属する組織を免責することに同意したものとします。