19
0

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 WorkflowsでPythonを実行してみた -Meraki ライセンスのカウント-

19
Last updated at Posted at 2025-12-11

はじめに

こちらはCisco Systems 合同会社 社員有志 による Advent Calendar です。
ぜひ、他の投稿もご覧ください:christmas_tree:

免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。

Cisco Workflowsとは?

Cisco Workflowsは、Cisco Merakiダッシュボードに統合されたクラウドベースの自動化ツールで、複雑なネットワーク作業をドラッグ&ドロップで簡単に自動化します。
Workflowsを扱った以下の投稿もぜひご参照ください。

今回やったこと

MerakiのAPIを使って、ネットワーク毎に使っているサブスクリプションライセンスの種別と数をカウントしました。
利用したツールは以下です。

  • Pythonスクリプト生成:Geminin
  • Python実行環境:Workflows

つまり、Cisco Workflowsでポチッと1クリックするだけで、ネットワーク毎のライセンスをカウントできるようになりました。

もちろん、Merakiダッシュボード上でもネットワーク毎のライセンスは確認できますが、ネットワークを1つ1つクリックして確認しなくてはいけません。管理ネットワークが少なければ気になりませんが、100個も200個もあるような場合、それら全てをポチポチ見ていくのは骨が折れます。そんな時、APIは業務効率を上げてくれる強い味方です。

今回は、コード作成にはGeminiを活用し、コード実行環境はWorkflowsを利用することで、ローコードでの運用効率化を目指してみました。

前提条件

今回の環境では、サブスクリプションの契約は1つで、それを全ネットワークにバインドしていることを前提とします。

サブスクリプションのライセンスはEnterpriseとAdvantageがあります。
サブスクリプション毎にEnterpriseかAdvantageのいずれかになります。
また、サブスクリプションとネットワークは1対1対応です(1つのネットワークに複数のサブスクリプションをバインドすることはできません)。
Enterprise又はAdvantageのサブスクリプションが複数ありネットワーク毎にいずれかがバインドされている環境というのは複雑なため、今回はシンプルな環境を前提とします。

やってみる

Pythonスクリプトの作成

今回はGeminiを使ってスクリプトを作成します。
https://gemini.google.com/app

Pythonで実行したい内容をプロンプトに投入し、後は微調整をするだけです。

プロンプト例:

あなたは優秀なプログラマーです。
私はプログラミングの初心者です。
MerakiのAPIを使って、ネットワーク毎に消費されているライセンス数を数えるPythonプログラムを作成したいです。
対象のオーガナイゼーションはサブスクリプションの契約で、このライセンス用のAPIではNetworkIDとの紐づけが困難です。
そのため、以下の手順を追ってライセンス数をカウントするプログラムを作成してください。

APIは以下を利用してください。環境変数として取り込むのではなく、プログラムの中に書き込んでください。
APIキー:MERAKI_API_KEY

また、実行環境で利用できるモジュールは以下のガイドに記載があるPython Modulesです。
https://documentation.meraki.com/Platform_Management/Workflows/Workflows/Activities/Python_Activities#title

<Pythonプログラムの概要>
1.ネットワーク毎のデバイス種別を取得してください。
2.MXは冗長化構成ではPrimary側だけライセンスを消費します。Primaryの機器のみをカウントしてください。
3.CatalystシリーズのHW製品は、firmwareをIOS XEもしくはMerakiのいずれえも利用できます。デバイス種別がCatalystシリーズでfirmwareがIOS XE以外の機器をカウントしてください。firmwareにIOS XEを利用しているものはカウントから外してください。
4.サブスクリプションのライセンスは、デバイス種別毎に添付の表に書かれている対応になります。
 1.~3.の手順で取得できたデバイスを添付の表にしたがって、ネットワーク毎に必要なライセンス種別と数をカウントしてください。

5.4.でカウントした結果を、ネットワーク毎にライセンス種別と数を書いて出力してください。途中経過の表示は不要です。

添付ファイル:Meraki_Subsc_Licensing_SKUs.png

※Meraki_Subsc_Licensing_SKUs.pngは、Subscription Licensing SKUsの製品モデルとライセンス情報を元に別途対応表を作成し画像ファイルとして添付。

Geminiで生成されたPythonスクリプトです。

Meraki_Subsc_License_Count.py
import requests
import json
from collections import defaultdict

# --- 1. 定数とライセンスマッピングの定義 ---
MERAKI_API_KEY = "API Key"
BASE_URL = "https://api.meraki.com/api/v1"
HEADERS = {
    "X-Cisco-Meraki-API-Key": MERAKI_API_KEY,
    "Content-Type": "application/json"
}

# 添付のOCRテキストと画像に基づいて、デバイスモデルとライセンスSKUの対応を定義します。
# OCRの読み取りミスや画像の情報を元に手動で修正しています。
hardware_to_license_sku = {
    "C9350-24U": "LIC-CS-AC1-M", "C9350-24P": "LIC-CS-AC1-M", "C9350-24T": "LIC-CS-AC1-M",
    "C9350-48-HX": "LIC-CS-AC1-L", "C9350-48U": "LIC-CS-AC1-L", "C9350-48P": "LIC-CS-AC1-L", "C9350-48T": "LIC-CS-AC1-L",
    "C9610R": "LIC-CS-CO-MO",
    "CW9172I": "LIC-CW", "CW9172H": "LIC-CW", "CW9176D1": "LIC-CW", "CW9176I": "LIC-CW", "CW9178": "LIC-CW",
    "MS120-8": "LIC-MS-100-S", "MS120-8LP": "LIC-MS-100-S", "MS120-8FP": "LIC-MS-100-S",
    "MS130-8": "LIC-MS-100-S", "MS130-8P": "LIC-MS-100-S", "MS130-8X": "LIC-MS-100-S", "MS130R-8P": "LIC-MS-100-S", "MS130-12X": "LIC-MS-100-S",
    "MS220-8": "LIC-MS-100-M", "MS220-8P": "LIC-MS-100-M",
    "MS120-24": "LIC-MS-100-M", "MS120-24P": "LIC-MS-100-M", "MS125-24": "LIC-MS-100-M", "MS125-24P": "LIC-MS-100-M",
    "MS130-24": "LIC-MS-100-M", "MS130-24P": "LIC-MS-100-M", "MS130-24X": "LIC-MS-100-M", "MS150-24T-4G": "LIC-MS-100-M",
    "MS120-48": "LIC-MS-100-L", "MS120-48LP": "LIC-MS-100-L", "MS120-48FP": "LIC-MS-100-L",
    "MS125-48": "LIC-MS-100-L", "MS125-48LP": "LIC-MS-100-L", "MS125-48FP": "LIC-MS-100-L",
    "MS130-48": "LIC-MS-100-L", "MS130-48P": "LIC-MS-100-L", "MS130-48X": "LIC-MS-100-L", "MS150-48T-4G": "LIC-MS-100-L",
    "MS210-24": "LIC-MS-200-M", "MS210-24P": "LIC-MS-200-M", "MS225-24": "LIC-MS-200-M", "MS225-24P": "LIC-MS-200-M",
    "MS250-24": "LIC-MS-200-M", "MS250-24P": "LIC-MS-200-M", "MS320-24": "LIC-MS-200-M", "MS320-24P": "LIC-MS-200-M",
    "C9200L-24T-4G": "LIC-MS-200-M",
    "MS210-48": "LIC-MS-200-L", "MS210-48LP": "LIC-MS-200-L", "MS210-48FP": "LIC-MS-200-L",
    "MS225-48": "LIC-MS-200-L", "MS225-48LP": "LIC-MS-200-L", "MS225-48FP": "LIC-MS-200-L",
    "MS250-48": "LIC-MS-200-L", "MS250-48LP": "LIC-MS-200-L", "MS250-48FP": "LIC-MS-200-L",
    "MS320-48": "LIC-MS-200-L", "MS320-48LP": "LIC-MS-200-L", "MS320-48FP": "LIC-MS-200-L",
    "C9200L-48T-4G": "LIC-MS-200-L",
    "MS350-24": "LIC-MS-300-M", "MS350-24P": "LIC-MS-300-M", "MS350-24X": "LIC-MS-300-M",
    "MS355-24X": "LIC-MS-300-M", "MS355-24X2": "LIC-MS-300-M",
    "MS390-24": "LIC-MS-300-M", "MS390-24P": "LIC-MS-300-M", "MS390-24U": "LIC-MS-300-M", "MS390-24UX": "LIC-MS-300-M",
    "C9300-24T-M": "LIC-MS-300-M", "C9300-24P-M": "LIC-MS-300-M", "C9300-24U-M": "LIC-MS-300-M", "C9300-24UX-M": "LIC-MS-300-M",
    "C9300X-12Y-M": "LIC-MS-300-M", "C9300X-24Y-M": "LIC-MS-300-M", "C9300X-24HX-M": "LIC-MS-300-M", "C9300-24S-M": "LIC-MS-300-M",
    "C9300L-24T-4X-M": "LIC-MS-300-M", "C9300L-24P-4X-M": "LIC-MS-300-M", "C9300L-24UXG": "LIC-MS-300-M",
    "MS350-48": "LIC-MS-300-L", "MS350-48LP": "LIC-MS-300-L", "MS350-48FP": "LIC-MS-300-L",
    "MS355-48X": "LIC-MS-300-L", "MS355-48X2": "LIC-MS-300-L",
    "MS390-48": "LIC-MS-300-L", "MS390-48P": "LIC-MS-300-L", "MS390-48U": "LIC-MS-300-L", "MS390-48UX": "LIC-MS-300-L", "MS390-48UX2": "LIC-MS-300-L",
    "C9300-48T-M": "LIC-MS-300-L", "C9300-48P-M": "LIC-MS-300-L", "C9300-48U-M": "LIC-MS-300-L", "C9300-48S-M": "LIC-MS-300-L",
    "C9300-48UXM-M": "LIC-MS-300-L", "C9300-48UN-M": "LIC-MS-300-L",
    "C9300X-48TX-M": "LIC-MS-300-L", "C9300X-48HX-M": "LIC-MS-300-L", "C9300X-48HXN-M": "LIC-MS-300-L",
    "C9300L-48T-4X-M": "LIC-MS-300-L", "C9300L-48P-4X-M": "LIC-MS-300-L", "C9300L-48PF-4X-M": "LIC-MS-300-L", "C9300L-48UXG": "LIC-MS-300-L",
    "MS410-16": "LIC-MS-400-M", "MS420-24": "LIC-MS-400-M", "MS425-16": "LIC-MS-400-M",
    "MS410-32": "LIC-MS-400-L", "MS420-48": "LIC-MS-400-L", "MS425-32": "LIC-MS-400-L", "MS450-12": "LIC-MS-400-L",
    "MX50": "LIC-MX-S", "MX60": "LIC-MX-S", "MX60W": "LIC-MX-S", "MX64": "LIC-MX-S", "MX64W": "LIC-MX-S", "MX65": "LIC-MX-S", "MX65W": "LIC-MX-S",
    "MX67": "LIC-MX-S", "MX67C-NA": "LIC-MX-S", "MX67C-WW": "LIC-MX-S", "MX67W": "LIC-MX-S", "MX68": "LIC-MX-S", "MX68W": "LIC-MX-S",
    "MX68CW-NA": "LIC-MX-S", "MX68CW-WW": "LIC-MX-S", "VMX-S": "LIC-MX-S",
    "MX70": "LIC-MX-M", "MX75": "LIC-MX-M", "MX80": "LIC-MX-M", "MX84": "LIC-MX-M", "MX85": "LIC-MX-M", "VMX-M": "LIC-MX-M",
    "MX90": "LIC-MX-L", "MX95": "LIC-MX-L", "MX100": "LIC-MX-L", "MX105": "LIC-MX-L", "MX400": "LIC-MX-L", "VMX100": "LIC-MX-L", "VMX-L": "LIC-MX-L",
    "MX250": "LIC-MX-XL", "MX450": "LIC-MX-XL", "MX600": "LIC-MX-XL", "VMX-XL": "LIC-MX-XL",
    "C8455-G2-MX": "LIC-CSR-XL",
    "MR12": "LIC-MR", "MR14": "LIC-MR", "MR16": "LIC-MR", "MR18": "LIC-MR", "MR20": "LIC-MR", "MR24": "LIC-MR", "MR26": "LIC-MR",
    "MR28": "LIC-MR", "MR30H": "LIC-MR", "MR32": "LIC-MR", "MR33": "LIC-MR", "MR34": "LIC-MR", "MR36": "LIC-MR", "MR36H": "LIC-MR",
    "MR42": "LIC-MR", "MR42E": "LIC-MR", "MR44": "LIC-MR", "MR45": "LIC-MR", "MR46": "LIC-MR", "MR46E": "LIC-MR", "MR52": "LIC-MR",
    "MR53": "LIC-MR", "MR53E": "LIC-MR", "MR55": "LIC-MR", "MR56": "LIC-MR", "MR57": "LIC-MR", "MR58": "LIC-MR", "MR58-CAR": "LIC-MR",
    "MV12N": "LIC-MV", "MV12W": "LIC-MV", "MV12WE": "LIC-MV", "MV13": "LIC-MV", "MV13M": "LIC-MV", "MV2": "LIC-MV", "MV21": "LIC-MV",
    "MV22": "LIC-MV", "MV22X": "LIC-MV", "MV23M": "LIC-MV", "MV23X": "LIC-MV", "MV32": "LIC-MV", "MV33": "LIC-MV", "MV33M": "LIC-MV",
    "MV52": "LIC-MV", "MV63": "LIC-MV", "MV63M": "LIC-MV", "MV63X": "LIC-MV", "MV71": "LIC-MV", "MV72": "LIC-MV", "MV72X": "LIC-MV",
    "MV73M": "LIC-MV", "MV73X": "LIC-MV", "MV93": "LIC-MV", "MV93M": "LIC-MV", "MV93X": "LIC-MV",
    "MT10": "LIC-MT", "MT11": "LIC-MT", "MT12": "LIC-MT", "MT14": "LIC-MT", "MT15": "LIC-MT", "MT20": "LIC-MT", "MT30": "LIC-MT", "MT40": "LIC-MT",
    "MG21": "LIC-MG", "MG21E": "LIC-MG", "MG41": "LIC-MG", "MG41E": "LIC-MG", "MG51": "LIC-MG", "MG51E": "LIC-MG", "MG52": "LIC-MG", "MG52E": "LIC-MG",
    "Z1": "LUC-Z", "Z3": "LUC-Z", "Z3C-NA": "LUC-Z", "Z3C-WW": "LUC-Z", "Z3C-JP": "LUC-Z", "Z4": "LUC-Z", "Z4C": "LUC-Z"
}

# --- 2. API呼び出しヘルパー関数 ---
def meraki_get(path, params=None):
    """Meraki API GETリクエストを実行するヘルパー関数"""
    url = f"{BASE_URL}{path}"
    try:
        response = requests.get(url, headers=HEADERS, params=params)
        response.raise_for_status()  # HTTPエラーがあれば例外を発生させる
        return response.json()
    except requests.exceptions.RequestException as e:
        # Meraki Workflowsの場合、構造化されたエラーを返そうとしますが、
        # メイン関数がどのように表示するかを決定します。
        return {"error": f"APIリクエストが {url} で失敗しました: {e}"}

# --- 3. メイン処理 ---
def get_meraki_license_counts():
    all_network_license_counts = {}
    
    # 組織の取得
    organizations = meraki_get("/organizations")
    if not organizations or "error" in organizations:
        # 組織の取得でエラーが発生した場合、直接エラーを返します
        return organizations 

    for org in organizations:
        org_id = org['id']
        org_name = org['name']
        # print(f"--- 組織: {org_name} ({org_id}) のライセンスをカウント中 ---") # 中間出力は抑制

        # ネットワークの取得
        networks = meraki_get(f"/organizations/{org_id}/networks")
        if not networks or "error" in networks:
            # ネットワークの取得に失敗した場合、ログに記録し(必要であれば)、次の組織に進みます
            # print(f"組織 {org_name} にネットワークが見つからないか、取得に失敗しました。")
            continue

        for network in networks:
            network_id = network['id']
            network_name = network['name']
            current_network_licenses = defaultdict(int)

            # ネットワーク内のデバイスの取得
            devices = meraki_get(f"/networks/{network_id}/devices")
            if not devices or "error" in devices:
                continue

            # プライマリMXアプライアンスの特定
            primary_mx_serial = None
            mx_devices_in_network = [d for d in devices if d.get('productType') == 'appliance']
            if mx_devices_in_network:
                mx_uplink_statuses = meraki_get(f"/networks/{network_id}/appliance/uplinks/statuses")
                if mx_uplink_statuses and "error" not in mx_uplink_statuses:
                    for status in mx_uplink_statuses:
                        if status.get('status') == 'primary':
                            primary_mx_serial = status.get('serial')
                            break
                # プライマリが見つからない場合、最初のMXをプライマリとみなす(フォールバック)
                if not primary_mx_serial and mx_devices_in_network:
                    primary_mx_serial = mx_devices_in_network[0]['serial']

            processed_mx_serials = set()

            for device in devices:
                model = device.get('model')
                serial = device.get('serial')
                product_type = device.get('productType')
                firmware = device.get('firmware') # Catalystのファームウェアチェック用

                if not model:
                    continue

                # 2. MXは冗長化構成ではプライマリ側だけライセンスを消費します。プライマリの機器のみをカウントしてください。
                if product_type == 'appliance': # MXデバイス
                    if serial == primary_mx_serial and serial not in processed_mx_serials:
                        # プライマリMXのみをカウント
                        processed_mx_serials.add(serial)
                    else:
                        # セカンダリMX、またはプライマリが特定できない場合の複数MXはスキップ
                        continue

                # 3. CatalystシリーズのHW製品は、firmwareをIOS XEもしくはMerakiのいずれえも利用できます。
                #    デバイス種別がCatalystシリーズでfirmwareがIOS XE以外の機器をカウントしてください。
                #    firmwareにIOS XEを利用しているものはカウントから外してください。
                if model.startswith("C9") or model.startswith("C8455"): # Catalystシリーズのモデルを識別
                    if firmware and "IOS XE" in firmware:
                        continue

                # 4. 添付の表にしたがって、ネットワーク毎に必要なライセンス種別と数をカウントしてください。
                license_sku = hardware_to_license_sku.get(model)
                if license_sku:
                    current_network_licenses[license_sku] += 1
                else:
                    pass # ライセンスSKUが見つからないデバイスはカウントしない

            if current_network_licenses:
                all_network_license_counts[network_name] = dict(current_network_licenses)

    # 5. 収集されたライセンスカウント、または見つからなかった場合のメッセージを返します。
    if not all_network_license_counts:
        return {"message": "すべてのネットワークでライセンス対象デバイスが見つかりませんでした。"}
    else:
        return all_network_license_counts

# --- メイン関数を実行し、JSON結果を標準出力に出力します ---
# Meraki Workflowsは、この標準出力をresponse_bodyとしてキャプチャすると想定されます。
result = get_meraki_license_counts()
print(json.dumps(result, indent=2))

Meraki APIキーを生成

1.Merakiダッシュボードの左のメニューバーで「Organization」>「API & Webhooks」を選択します。
image.png
image.png

2.「API keys and access」タブで「Generate API Key」 をクリックしてAPIキーを生成します。生成されたAPIキーをコピーして忘れないように保存しておきましょう。注意書きにも記載されている通り、Merakiダッシュボード上で再度このAPIキーを確認することはできませんので、もし忘れてしまった場合は再度作り直しましょう。「Done」をクリックすると「Perasonal API keys」にAPIキーが下4桁だけ分かる形で記載されます。
image.png

image.png

WorkflowsでPythonスクリプトを実行する

  1. Merakiダッシュボードの左のメニューバーで「Automation」>「Varibables」を選択します。

image.png

image.png

2.「Global Variables」タブで「+ New variable」をクリックします。
image.png

3.以下の値を入力して「Save」をクリックします。
Data Type:Secure String
Display Name:API Key(任意の名前を付けます)
Descripition:Meraki API Key(空白でも構いません)
Scope:Environment(この後、Pythonで環境変数として使います)
Value:先ほど生成してコピーしておいたAPIキーを入力
image.png
設定が完了すると一覧に表示されます。
image.png

4.Merakiダッシュボードの左のメニューバーで「Automation」>「Workspace」を選択します。

image.png
image.png

5.「Wokflows」タブで「+Create」をクリックします。
image.png

6.「Blank Custom Workflow」を選んで「Continue」をクリックします。
image.png

7.「Workflow display name」に任意の名前を設定して「Continue」をクリックします。
image.png

8.「Activities」タブから「Python」>「Execute Python Script」を掴んで右の「Drag activity here」と書かれているところに持って行きます。
image.png
image.png

9.Pythonを実行するためのActivityがドラッグできました。右の「Script to execute on target:」の枠内に先ほど作ったPythonスクリプトを貼り付けます。
image.png

10.Pythonで実行する際のMerkai APIキーを、先ほど環境変数として設定した値を使うように設定します。貼り付けたスクリプト内の"API Key"を選択し、枠の右上にある「(x)」をクリックします。
image.png

11.先ほど設定した環境変数の「API Key」を選択して「Save」します。
image.png
環境変数が設定できました。
image.png

12.作業スペースのActivityが書かれていない部分をクリックします。
image.png
右側の設定画面が切り替わります。このWorkflowのGeneral設定画面になります。「Descripition」に英語で出来るだけ具体的に説明文を記載します。※設定は任意ですが「おまけ」で役立つので設定をお勧めします。
設定例:This Python script automates the process of counting Meraki license consumption across your Meraki organizations and networks.
It connects to the Cisco Meraki API to retrieve device information and then calculates the required license SKUs for each network based on specific business rules.
image.png
設定が完了したら、右上の「Validate」をクリックします。

13.右上の「Run」をクリックしてWorkflowを実行します。

14.Workflowが正常に完了すると「SUCCESS」と表示されます。
image.png
「Response body」を見ると、ネットワーク毎にライセンス種別と数がカウント出来ていることが確認できました!
image.png

おまけ

Merakiダッシュボートにこんなアイコンがあることをご存じでしょうか?

その名も今流行りの「AI Assistant」です。チャット形式で皆さんの作業をサポートしてくれます。日進月歩で進化中で、1週間前には上手く答えてくれなかったことが今日はできた!ということもあるので、気長にお付き合いください。
さて、このAI Assistantを使って、Workflowを実行することが可能です。
その際、大事なポイントが「Description」の記載です。AI AssistantでWorkflowを実行しようとすると、AI AssistantはDescriptionの内容を見て、どのWorkflowかな?と探してくれます。ですので、Descriptionの内容はできるだけ分かり易く英語で記載するようにしましょう。

1.チャットボックスに「サブスクリプションのライセンス数をカウントするWorkflowを実行してください」と入力します。

2.先ほど作成したWorkflowを探して来て、これで良いですか?と聞いてくれるので「yes」と回答
image.png

3.実行ボタンが出てくるのでクリックします。
image.png

4.エラーなく成功しました!
image.png

まとめ

今回は

  • Pythonスクリプト生成:Geminin
  • Python実行環境:Workflows

を利用して業務効率化をしてみました。
APIを使うと便利なのは知ってるけどプログラミングのハードルが高い...と悩んでいた人にも優しい時代になりました。上手く行かなくてもAIはデバッグにも根気強く付き合ってくれます。何か思いついたら、皆さんもまずは試してみると思いの外簡単に成功体験が得られるかもしれません。楽しみながら色々試してみて下さい:robot:

19
0
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
19
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?