21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cisco SD-WAN Cloud(CDCS)コントローラとAPI

21
Last updated at Posted at 2025-12-03

始めに

これは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)

🧪 仕組みの流れ(重要)

  1. sdwan_token.txt の JWT を読み込む
  2. SSP API へ POST
    https://ssp.sdwan.cisco.com/ssp/api/v6/apigw/info/
    
    ここで gateway_url を取得する
  3. API Gateway 経由で
    GET https://<gateway_url>/dataservice/device
    
    を叩く
  4. デバイス一覧 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 説明)

質問や追加の改善点があればコメントしてください!

21
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?