実践ネット自動化スクリプト集

実践Python:ネットワーク機器の設定ドリフトを自動検出・レポート

Tags: Python, ネットワーク自動化, 設定管理, Netmiko, ドリフト検知

「実践ネット自動化スクリプト集」の記事へようこそ。

システムやインフラの運用において、設定の「ドリフト」、すなわち予期しない設定変更や本来あるべき設定からの逸脱は、セキュリティリスクや障害の原因となり得ます。サーバーやクラウドインスタンスではIaC(Infrastructure as Code)によってこのドリフトを抑制・検知する手法が一般的になりつつありますが、ネットワーク機器においても設定ドリフトの検知は運用管理の重要な課題です。

特に、Pythonを用いたインフラ自動化に慣れているものの、ネットワーク機器の細かなCLI操作には不慣れな方にとって、設定ドリフトを効率的に検知し、その差異を分かりやすく把握する仕組みの構築は有用でしょう。この記事では、Pythonを活用してネットワーク機器の設定ドリフトを自動的に検出し、レポートする基本的な手法とその実装例について解説します。

設定ドリフトとは?なぜ検知が必要なのか

設定ドリフトとは、基準となる設定(本来あるべき設定状態)から、実際の機器の設定が乖離してしまう状態を指します。これは、以下のような様々な要因で発生する可能性があります。

設定ドリフトを放置すると、構成管理の整合性が失われ、障害発生時の原因特定が困難になったり、セキュリティポリシー違反につながったりします。設定ドリフトを定期的に検知し、基準との差異を把握することで、これらのリスクを低減し、運用品質の向上に貢献します。

自動検出の基本的な考え方

設定ドリフトの自動検出は、主に以下のステップで実現できます。

  1. 基準コンフィグの定義: 機器ごとに「あるべき」設定状態を基準コンフィグとして定義・管理します。これはテキストファイルや構造化データとして保存されます。
  2. 現在コンフィグの取得: 対象のネットワーク機器から、現在の実行コンフィグレーションを取得します。
  3. コンフィグの比較: 取得した現在コンフィグと基準コンフィグを比較し、差異(Diff)を検出します。
  4. レポート生成: 検出した差異を、人間が理解しやすい形式でレポートとして出力します。
  5. 通知/アクション: 生成したレポートを関係者に通知したり、自動修正などの次のアクションをトリガーしたりします。

この一連のプロセスをPythonスクリプトで自動化します。

Pythonを使った実装例

ここでは、比較的シンプルな実装例として、netmikoを使って機器からコンフィグを取得し、Python標準ライブラリのdifflibを用いて基準コンフィグファイルと比較する方法を示します。

前提条件

サンプルコード

import os
import difflib
from netmiko import ConnectHandler

# --- 設定 ---
# ネットワーク機器の接続情報リスト
# 認証情報は別途安全な方法で管理することが推奨されます(後述)
DEVICE_LIST = [
    {
        'device_type': 'cisco_ios', # 機器タイプに合わせて変更
        'host': '192.168.1.100',
        'username': 'your_username',
        'password': 'your_password',
        # 'secret': 'your_enable_password', # enableモードが必要な場合
    },
    # 複数機器がある場合はここに追加
    # {
    #     'device_type': 'juniper_junos',
    #     'host': '192.168.1.101',
    #     'username': 'your_username',
    #     'password': 'your_password',
    # },
]

# 基準コンフィグが保存されているディレクトリのパス
BASELINE_CONFIG_DIR = './baselines'

# 取得したコンフィグを保存する一時ディレクトリのパス
CURRENT_CONFIG_DIR = './current_configs'

# 差異レポートを保存するディレクトリのパス
REPORT_DIR = './reports'

# --- 前準備 ---
# ディレクトリが存在しない場合は作成
os.makedirs(BASELINE_CONFIG_DIR, exist_ok=True)
os.makedirs(CURRENT_CONFIG_DIR, exist_ok=True)
os.makedirs(REPORT_DIR, exist_ok=True)

# --- 関数定義 ---

def get_current_config(device_info):
    """指定された機器から現在の実行コンフィグを取得する"""
    print(f"--- {device_info['host']} からコンフィグを取得中 ---")
    try:
        # Netmikoで機器に接続
        with ConnectHandler(**device_info) as net_connect:
            # コンフィグ取得コマンドは機器タイプによって異なる場合があります
            # Cisco IOS の場合: 'show running-config'
            # Juniper Junos の場合: 'show configuration | display set' など
            # NAPALM等を使えばコマンドを抽象化できますが、ここではシンプルにCLIコマンドを指定
            if device_info['device_type'] == 'cisco_ios':
                 command = 'show running-config'
            elif device_info['device_type'] == 'juniper_junos':
                 # Junosの場合はset形式が比較しやすいことが多い
                 command = 'show configuration | display set'
            # 他の機器タイプも必要に応じて追加...
            else:
                 print(f"警告: 機器タイプ {device_info['device_type']} のコンフィグ取得コマンドが未定義です。")
                 return None

            # コマンド実行
            output = net_connect.send_command(command)
            print("コンフィグ取得完了")
            return output

    except Exception as e:
        print(f"エラー: {device_info['host']} からコンフィグ取得中にエラーが発生しました: {e}")
        return None

def load_baseline_config(hostname):
    """指定されたホスト名の基準コンフィグをファイルから読み込む"""
    baseline_file = os.path.join(BASELINE_CONFIG_DIR, f'{hostname}.cfg')
    if not os.path.exists(baseline_file):
        print(f"警告: 基準コンフィグファイルが見つかりません: {baseline_file}")
        # ファイルが存在しない場合は、現在のコンフィグを基準として保存することを検討
        # ただし、これは初回実行時や基準更新時に限定すべき
        return None

    print(f"基準コンフィグを読み込み中: {baseline_file}")
    with open(baseline_file, 'r') as f:
        return f.readlines() # 行リストとして返す

def save_current_config(hostname, config_data):
     """取得した現在のコンフィグを一時ファイルとして保存する"""
     current_file = os.path.join(CURRENT_CONFIG_DIR, f'{hostname}_current.cfg')
     print(f"現在のコンフィグを一時保存: {current_file}")
     with open(current_file, 'w') as f:
          f.write(config_data)
     return current_file # 保存したファイルのパスを返す

def generate_diff_report(hostname, baseline_lines, current_lines):
    """基準コンフィグと現在のコンフィグの差異レポートを生成する"""
    print(f"--- {hostname} のコンフィグ差異を比較中 ---")
    report_file = os.path.join(REPORT_DIR, f'{hostname}_diff_report.txt')

    if baseline_lines is None:
        # 基準コンフィグがない場合は、現在のコンフィグの内容をレポートとして保存
        print(f"基準コンフィグが存在しないため、現在のコンフィグ全体をレポートとして保存します: {report_file}")
        with open(report_file, 'w') as f:
             if current_lines:
                 f.writelines(current_lines)
             else:
                 f.write("現在のコンフィグを取得できませんでした。\n")
        return report_file

    # difflib.unified_diff で差異を取得
    # fromlines, tolines は行リストである必要がある
    # n=0 とすると、差異のある行とその前後0行のみを表示 (コンテキストなし)
    diff_lines = list(difflib.unified_diff(
        baseline_lines, # 基準コンフィグ (ファイルから読み込んだ行リスト)
        current_lines.splitlines(keepends=True), # 現在コンフィグ (文字列を改行で分割し、行リスト+改行コードを保持)
        fromfile=f'{hostname}_baseline.cfg',
        tofile=f'{hostname}_current.cfg',
        lineterm='' # デフォルトの改行コードサフィックスを削除
    ))

    print(f"差異レポートを生成: {report_file}")
    with open(report_file, 'w') as f:
        if not diff_lines:
            f.write(f"--- {hostname} ---")
            f.write("\n設定ドリフトは検出されませんでした。\n")
        else:
            f.write(f"--- {hostname} の設定ドリフト検出 ---")
            f.write("\n以下の差異が検出されました (+: 現在コンフィグのみ, -: 基準コンフィグのみ):\n\n")
            f.writelines(diff_lines) # 差異行を書き込む

    print(f"差異レポート出力完了: {report_file}")
    return report_file # 生成したレポートファイルのパスを返す

# --- メイン処理 ---

if __name__ == "__main__":
    print("--- ネットワーク設定ドリフト自動検出スクリプトを開始します ---")

    for device in DEVICE_LIST:
        hostname = device['host'] # 例としてhost名をホスト名とする
        current_config = get_current_config(device)

        if current_config:
            # 現在のコンフィグを一時ファイルに保存
            save_current_config(hostname, current_config)

            # 基準コンフィグを読み込み
            baseline_config_lines = load_baseline_config(hostname)

            # 差異レポートを生成
            report_file_path = generate_diff_report(hostname, baseline_config_lines, current_config)

            # ここでレポートファイルの内容を確認したり、メールで送信したりする処理を追加できます
            # 例: 簡単な内容出力
            print(f"\n生成されたレポートの内容 (ファイル: {report_file_path}):")
            with open(report_file_path, 'r') as f:
                 print(f.read())
            print("-" * 20) # 区切り線

        else:
            print(f"エラー: {hostname} の処理をスキップします。\n")

    print("--- ネットワーク設定ドリフト自動検出スクリプトを終了します ---")

コードの解説と補足

現場での考慮点と発展的なアプローチ

上記のサンプルコードは基本的な検出処理を示していますが、実際の現場で利用するにはいくつかの考慮点と発展的なアプローチがあります。

1. 認証情報の安全な管理

コード中にパスワードを平文で記述するのは非常に危険です。以下のような方法で安全に管理することを強く推奨します。

2. 複数機器への並列処理とエラーハンドリング

対象機器が多い場合、1台ずつ処理すると時間がかかります。Pythonのconcurrent.futuresモジュールや、ネットワーク自動化に特化したフレームワークであるNornirを利用することで、複数機器への処理を並列化し、効率を向上させることができます。Nornirはプラグインによって様々なタスク(Netmikoによるコマンド実行、NAPALMによる構造化データ取得など)を抽象化し、機器のインベントリ管理や並列実行を容易に行えるため、より大規模な自動化に適しています。また、フレームワークを利用することで、個別の機器処理におけるエラーハンドリングも構造的に記述しやすくなります。

3. 基準コンフィグの更新とバージョン管理

基準コンフィグは、設定変更を実施した際に適切に更新される必要があります。手動での更新はミスにつながりやすいため、設定変更自動化のワークフローに組み込むことが理想的です。

4. レポート形式と通知方法

生成される差異レポートは、運用担当者が迅速に状況を把握できるよう、分かりやすい形式で出力される必要があります。

5. 定期実行の仕組み

ドリフト検知は定期的に実行することで効果を発揮します。LinuxのcronやWindowsのタスクスケジューラ、あるいはJenkinsやGitLab CI/CDなどのCI/CDツールを用いて、スクリプトを定時または特定イベント(例: 設定変更後)に実行する仕組みを構築します。

IaC/CI/CD文脈での位置づけ

設定ドリフトの自動検出は、インフラ自動化全体のライフサイクルにおいて重要な役割を果たします。

まとめ

この記事では、Pythonを使ってネットワーク機器の設定ドリフトを自動的に検出し、レポートする基本的な手法を解説しました。netmikoによるコンフィグ取得とdifflibによるテキスト比較は、比較的シンプルにドリフト検知を実現できる方法です。

しかし、実際の運用においては、認証情報の安全な管理、複数機器への効率的な適用(Nornirなどの活用)、基準コンフィグの適切な管理(Git, IaCツール連携)、分かりやすいレポートと通知方法など、様々な要素を考慮する必要があります。

設定ドリフトの自動検出は、ネットワーク運用の品質向上とリスク低減に大きく貢献します。ぜひこの記事で紹介した手法を参考に、現場の課題に合わせた自動化スクリプトの作成に挑戦してみてください。IaCやCI/CDといった broader な自動化戦略の中で、ネットワーク自動化がどのように組み込めるかを考える上でも、こうした基本的な自動化スクリプトの理解と実践は非常に有用となるでしょう。