始めに
これはCisco有志によるアドベントカレンダー記事の1つです。
https://qiita.com/advent-calendar/2025/cisco
2025年12月8日追記
デバイス登録を一括して行う際にserial.viptelaファイルを
アップロードする機能を追加しておきました。
🔧 必要なもの
- Python 3.9+
-
requestsライブラリpip install requests - vManage GUI で発行できる API Token(JWT)
🔐 API Token の取り方
vManage(CDCS Cloud Controller)にログインして:
右上のユーザアイコン → My Profile → API token → Generate
JSON が表示されるので、その中の token だけを sdwan_token.txt に保存します。
例:
eyJ0eXAiOiJqd3QiLCJhbGciOiJSUzI1NiJ9...
※ 1行そのまま貼り付け。
📁 ディレクトリ構成
MT-CDCS/
├── sdwan_device_inventory.py ← Pythonスクリプト
└── sdwan_token.txt ← API token (JWT)
🧪 仕組みの流れ(重要)
-
sdwan_token.txtの JWT を読み込む - SSP API へ POST
ここで gateway_url を取得する
https://ssp.sdwan.cisco.com/ssp/api/v6/apigw/info/ - API Gateway 経由で
を叩く
GET https://<gateway_url>/dataservice/device - デバイス一覧 JSON が返る
📝 フル Python スクリプト
sdwan_device_inventory.py
#!/usr/bin/env python3
"""
Cloud-delivered Cisco Catalyst SD-WAN (CDCS) 向け
- 引数なし: デバイス一覧表示
- 引数あり: WAN Edge リスト (serialFile.viptela / sdwan-edge-list.viptela) をアップロード
使用方法:
python sdwan_device_inventory.py
python sdwan_device_inventory.py sdwan-edge-list.viptela
"""
import json
import os
import sys
import requests
# ---------------------------
# 設定
# ---------------------------
TOKEN_FILE = "sdwan_token.txt"
TENANT_ORG = "CDCS-AP-3A-va-xxxxxxxx - xxxxxx"
SSP_INFO_URL = "https://ssp.sdwan.cisco.com/ssp/api/v6/apigw/info/"
# 必要に応じて verify=False も可(検証環境など)
REQUESTS_VERIFY = True
# ---------------------------
# トークン読み込み
# ---------------------------
def read_token_from_file(path: str = TOKEN_FILE) -> str:
"""
sdwan_token.txt に保存された JWT (1 行) を読み込む
"""
try:
with open(path, "r") as f:
token = f.read().strip()
if not token:
raise RuntimeError(f"ERROR: token file is empty: {path}")
return token
except FileNotFoundError:
raise RuntimeError(f"ERROR: token file not found: {path}")
# ---------------------------
# API Gateway URL の取得 (/ssp/api/v6/apigw/info/)
# ---------------------------
def get_gateway_url(org: str, api_token: str) -> str:
payload = {
"org": org,
"apikey": api_token,
"is_redirect": False,
}
print(f"[DEBUG] POST {SSP_INFO_URL}")
resp = requests.post(SSP_INFO_URL, json=payload, verify=REQUESTS_VERIFY)
print(f"[DEBUG] SSP status = {resp.status_code}")
if resp.status_code != 200:
print("[DEBUG] SSP response text:")
print(resp.text)
resp.raise_for_status()
data = resp.json()
gw = data.get("gateway_url")
if not gw:
raise RuntimeError(f"ERROR: gateway_url not found in response: {data}")
return gw
# ---------------------------
# CSRF トークン取得 (/dataservice/client/token)
# ---------------------------
def get_csrf_token(gateway_url: str, api_token: str) -> str:
"""
Cloud ガイドの Step 6 にあるとおり、client/token から CSRF を取得して
後続の API 呼び出しで X-XSRF-Token に載せる。
"""
url = f"https://{gateway_url}/dataservice/client/token?json=true"
headers = {
"Authorization": f"Bearer {api_token}",
}
print(f"[DEBUG] GET {url}")
resp = requests.get(url, headers=headers, verify=REQUESTS_VERIFY)
print(f"[DEBUG] client/token status = {resp.status_code}")
if resp.status_code != 200:
print("[DEBUG] client/token response text:")
print(resp.text)
resp.raise_for_status()
data = resp.json()
token = data.get("token")
if not token:
raise RuntimeError(f"ERROR: CSRF token not found in response: {data}")
return token
# ---------------------------
# 共通ヘッダ作成 (JWT + CSRF)
# ---------------------------
def build_headers(api_token: str, csrf_token: str) -> dict:
"""
すべての API 呼び出しで使う共通ヘッダ
- Authorization: Bearer <APIトークン>
- X-XSRF-Token: <client/token で取得した CSRF>
"""
return {
"Authorization": f"Bearer {api_token}",
"X-XSRF-Token": csrf_token,
}
# ---------------------------
# デバイス一覧取得 (/dataservice/device)
# ---------------------------
def get_devices(gateway_url: str, headers: dict) -> dict:
url = f"https://{gateway_url}/dataservice/device"
print(f"[DEBUG] GET {url}")
resp = requests.get(url, headers=headers, verify=REQUESTS_VERIFY)
print(f"[DEBUG] /device status = {resp.status_code}")
if resp.status_code != 200:
print("[DEBUG] /device response text:")
print(resp.text)
resp.raise_for_status()
return resp.json()
def print_device_summary(devices: dict) -> None:
print("\n[INFO] Device Summary:")
data_list = devices.get("data", [])
if not data_list:
print(" (no devices)")
return
for d in data_list:
host = d.get("host-name") or "-"
system_ip = d.get("system-ip") or "-"
model = d.get("device-model") or "-"
status = d.get("reachability") or d.get("status") or "-"
print(f"{host:30} {system_ip:15} {model:20} {status}")
# ---------------------------
# WAN Edge リスト ファイルアップロード
# /dataservice/system/device/fileupload
# ---------------------------
def upload_wan_edge_file(gateway_url: str, headers: dict, file_path: str) -> None:
if not os.path.exists(file_path):
raise RuntimeError(f"ERROR: File not found: {file_path}")
url = f"https://{gateway_url}/dataservice/system/device/fileupload"
print(f"[INFO] Uploading WAN Edge file: {file_path}")
print(f"[DEBUG] POST {url}")
# multipart/form-data で送る
# ansible-viptela の実装では
# files={'file': open(file, 'rb')}
# data={'validity':'valid', 'upload':'true'}
# を指定している
with open(file_path, "rb") as f:
files = {
"file": (os.path.basename(file_path), f, "application/octet-stream"),
}
data = {
"upload": "true",
"validity": "valid",
}
resp = requests.post(
url,
headers=headers,
files=files,
data=data,
verify=REQUESTS_VERIFY,
)
print(f"[DEBUG] fileupload status = {resp.status_code}")
if resp.status_code != 200:
print("[DEBUG] Response text:")
print(resp.text)
resp.raise_for_status()
# 成功時は JSON で返ってくることが多いので一応表示
try:
print("[INFO] fileupload response JSON:")
print(json.dumps(resp.json(), indent=2))
except ValueError:
print("[INFO] fileupload response text:")
print(resp.text)
# ---------------------------
# メイン処理
# ---------------------------
def main():
# 引数: 0 個 → デバイス一覧, 1 個 → WAN Edge リストをアップロード
upload_file = None
if len(sys.argv) == 2:
upload_file = sys.argv[1]
print("[INFO] Loading API token...")
api_token = read_token_from_file()
print("[INFO] Getting gateway_url from SSP...")
gateway_url = get_gateway_url(TENANT_ORG, api_token)
print(f"[INFO] gateway_url = {gateway_url}")
print("[INFO] Getting CSRF token from client/token...")
csrf_token = get_csrf_token(gateway_url, api_token)
print(f"[DEBUG] CSRF token (truncated) = {csrf_token[:16]}...")
headers = build_headers(api_token, csrf_token)
if upload_file:
# WAN Edge リスト アップロードモード
upload_wan_edge_file(gateway_url, headers, upload_file)
else:
# デバイス一覧取得モード
print("[INFO] Fetching devices...")
devices = get_devices(gateway_url, headers)
print_device_summary(devices)
if __name__ == "__main__":
main()
▶️ 実行
python3 sdwan_device_inventory.py
📌 出力例
python3 sdwan_device_inventory.py
[INFO] Loading API token...
[INFO] Getting gateway_url from SSP...
[DEBUG] POST https://ssp.sdwan.cisco.com/ssp/api/v6/apigw/info/
[DEBUG] SSP status = 200
[INFO] gateway_url = clouduswest234-apigw.sdwan.cisco.com
[INFO] Getting CSRF token from client/token...
[DEBUG] GET https://clouduswest234-apigw.sdwan.cisco.com/dataservice/client/token?json=true
[DEBUG] client/token status = 200
[DEBUG] CSRF token (truncated) = 5E209F68F9CE088F...
[INFO] Fetching devices...
[DEBUG] GET https://clouduswest234-apigw.sdwan.cisco.com/dataservice/device
[DEBUG] /device status = 200
[INFO] Device Summary:
DC-101 169.254.10.131 vedge-C8200L-1N-4T reachable
DC-102 169.254.10.132 vedge-C8200L-1N-4T reachable
DC-201 169.254.10.130 vedge-C8200L-1N-4T reachable
Branch001 169.254.10.135 vedge-C1121-4P reachable
Branch002 169.254.10.134 vedge-C1121-4P unreachable
🎉 まとめ
-
CDCS の API は SSP → gateway_url 取得 → /dataservice の流れで使う
-
ローカルの JWT を使うだけで簡単に操作できる
-
これを応用すれば、config取得・テンプレート適用・vEdgeのみ抽出なども可能
(専用コントローラのようになんでもかんでもできるというわけではなさそう。) -
APIの連続実行はできません。時間を空けて実行するか、失敗したのちにもう一度スクリプトを実行し直すとうまく動きそうです。実行レートとか制限については↓の公式ドキュメントを参考
📚 参考URL
Cloud-delivered Cisco Catalyst SD-WAN – Getting Started Guide(API)
Cisco SD-WAN vManage REST APIs(全APIリスト)
Cisco SD-WAN API Authentication(JWT / API Key 説明)
質問や追加の改善点があればコメントしてください!