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

現場で役立つ!Pythonによるネットワーク機器OSアップグレード作業の効率化

Tags: Python, ネットワーク自動化, OSアップグレード, Netmiko, 自動化スクリプト

はじめに

システムやインフラの運用において、ネットワーク機器のOSアップグレード作業は避けて通れない重要なタスクです。新しい機能の導入、セキュリティパッチの適用、不具合の修正など、OSを最新の状態に保つことは安定稼働のために不可欠です。しかし、この作業は手動で行う場合、複数のステップ(新しいOSイメージのダウンロード、機器への転送、チェックサム確認、ブート設定変更、リロード、起動確認など)があり、時間と手間がかかります。また、手作業による設定ミスや手順漏れは、サービス停止といった大きなインシデントにつながるリスクも伴います。

Pythonの高いスキルをお持ちでありながら、ネットワーク機器の直接的な操作に慣れていないエンジニアの方々にとって、これらの作業の一部をPythonで自動化・効率化できれば、作業負荷の軽減とミスの削減につながり、インフラ運用全体の自動化レベルを向上させることができます。

この記事では、Pythonを使ってネットワーク機器のOSアップグレード作業の一部を自動化・効率化するための具体的な手法と、Pythonスクリプトの実装例をご紹介します。特に、OSイメージファイルの転送や、ブート設定の変更、機器のリロードといった、Pythonスキルを活かせる部分に焦点を当てます。

ネットワーク機器OSアップグレード作業の概要と自動化のポイント

一般的なネットワーク機器のOSアップグレード作業は、概ね以下のステップで構成されます。

  1. 準備: 新しいOSイメージファイルの入手、互換性確認、作業計画の立案。
  2. 機器へのOSイメージ転送: FTP, TFTP, SCP, SFTPなどのプロトコルを使用して、新しいOSイメージファイルを機器のフラッシュメモリやストレージに転送します。
  3. ファイルの確認: 転送されたファイルのサイズやチェックサム(ハッシュ値)を確認し、ファイルが正しく転送されたことを検証します。
  4. ブート設定の変更: 次回起動時に新しいOSイメージを読み込むように、機器のブート設定(boot systemコマンドなど)を変更します。
  5. 設定保存: 現在の設定を保存します。
  6. リロード(再起動): 機器を再起動し、新しいOSイメージで起動させます。
  7. 起動後の確認: 機器が正常に起動したか、新しいOSバージョンで動作しているか、意図したブート設定になっているかなどを確認します。
  8. 疎通・機能確認: ネットワーク機能(ルーティング、スイッチング、ACLなど)が正常に動作しているか確認します。

これらのステップのうち、Pythonスクリプトによる自動化に適しているのは、ステップ2〜7の部分です。特に、ファイル転送、コマンド実行による設定変更と確認、リロード指示といった部分は、Pythonライブラリを活用して効率的に自動化できます。ただし、OSアップグレード作業は本番環境への影響が非常に大きいため、すべてのステップを完全に自動化するのではなく、スクリプトで支援し、重要な判断や最終確認は手動で行うハイブリッドなアプローチも現実的です。

Pythonライブラリの選定

ネットワーク機器へのアクセスやコマンド実行には、netmikoparamikoといったライブラリが広く利用されています。netmikoは多くのベンダーの機器に対応しており、SSH接続を介したコマンド実行や設定投入を容易に行えます。内部でparamikoなどのSSHライブラリを使用しています。paramikoはSSHv2プロトコルの純粋なPython実装であり、SCPやSFTPによるファイル転送機能も提供しています。

この記事では、コマンド実行にはnetmikoを、ファイル転送にはnetmikofile_transfer機能(多くの場合SCPを利用)を中心に説明を進めます。

具体的なスクリプト例

ここでは、Cisco IOS XEルーターを例に、OSイメージファイルの転送、ブート設定変更、リロードといった一連の作業を自動化するPythonスクリプトの抜粋例をご紹介します。

依存ライブラリのインストール

netmikoが必要です。多くの場合、netmikoをインストールすれば依存関係としてparamikoもインストールされます。

pip install netmiko

スクリプトの基本的な構造

import time
from netmiko import Netmiko
from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException
import os # ローカルファイルの存在チェックに使用

# 機器接続情報
device = {
    "device_type": "cisco_ios", # 適切なデバイスタイプを指定 (例: cisco_ios, arista_eos, juniper_junos)
    "host": "YOUR_DEVICE_IP",
    "username": "YOUR_USERNAME",
    "password": "YOUR_PASSWORD",
    # "secret": "YOUR_ENABLE_PASSWORD", # enableモードが必要な場合
}

# OSイメージファイル情報
local_image_path = "/path/to/your/new_ios_image.bin" # PC上のイメージファイルパス
remote_image_name = "new_ios_image.bin" # 機器上に保存するファイル名
remote_flash_path = "flash:/" # 機器上の保存先パス (例: flash:/, bootflash:/)
remote_file_system = remote_flash_path.replace(":/", "") # Netmikoのfile_transfer関数用に 'flash' などの形式に変換

### 機器へのファイル転送
# Netmikoのfile_transfer機能を利用
def transfer_image(net_connect, local_path, remote_path, file_system):
    """
    ローカルファイルをネットワーク機器に転送します。
    主にSCPを利用します。
    """
    if not os.path.exists(local_path):
        print(f"ERROR: Local file not found: {local_path}")
        return False

    print(f"INFO: Transferring {local_path} to {file_system}:/{remote_path}...")
    try:
        # Netmikoのfile_transfer関数はSCP/SFTPをサポートします
        # 機器側でSSH/SCPが有効になっている必要があります
        transfer_dict = net_connect.file_transfer(
            local_path,
            remote_path,
            file_system=file_system,
            direction="put",
            overwrite_file=True # 必要に応じて上書きを許可
        )
        if transfer_dict["file_transferred"]:
            print("INFO: File transfer successful.")
            # MD5ハッシュも転送結果に含まれる場合がありますが、
            # 機器側で改めて検証することが推奨されます。
            if 'remote_md5' in transfer_dict and transfer_dict['remote_md5']:
                 print(f"INFO: Remote file MD5 hash (reported by Netmiko): {transfer_dict['remote_md5']}")
            return True
        else:
            print(f"ERROR: File transfer failed: {transfer_dict.get('error', 'Unknown error')}")
            return False
    except Exception as e:
        print(f"ERROR: An error occurred during file transfer: {e}")
        return False

### ファイルのチェックサム確認 (機器上でコマンド実行)
def verify_checksum(net_connect, remote_full_path, expected_checksum):
    """
    機器上のファイルのチェックサムを確認します。
    ベンダーやOSによりコマンドは異なります。
    """
    print(f"INFO: Verifying checksum for {remote_full_path}...")
    # Cisco IOS/IOS XE の場合: verify /md5 flash:/image_name.bin
    command = f"verify /md5 {remote_full_path}"
    try:
        # チェックサム計算には時間がかかる場合があるため、コマンドタイムアウトを長めに設定することも検討
        output = net_connect.send_command(command, enable=True, delay_factor=10) # enableモードが必要な場合
        print(f"DEBUG: Checksum command output:\n{output}")

        # 出力からMD5ハッシュをパースしてexpected_checksumと比較するロジック
        # 実際のパース処理は正規表現などを使って正確に行う必要があります。
        # ここでは、出力文字列内に期待するチェックサムが含まれているかという簡易的な確認例とします。
        if expected_checksum.lower() in output.lower():
             print(f"INFO: Checksum matched for {remote_full_path}.")
             return True
        else:
             print(f"ERROR: Checksum mismatch for {remote_full_path}.")
             print(f"Expected: {expected_checksum}, Found in output.")
             return False
    except Exception as e:
        print(f"ERROR: An error occurred during checksum verification: {e}")
        return False

### ブート設定の変更
def change_boot_config(net_connect, remote_full_path):
    """
    機器のブート設定を新しいOSイメージファイルに変更します。
    既存設定を削除し、新しい設定を追加することで冪等性を考慮しています。
    """
    print("INFO: Changing boot configuration...")
    # 機器のOSやバージョンによってコマンドは異なります。ここではIOS/IOS XEを想定。
    config_commands = [
        "no boot system", # 既存設定を削除
        f"boot system {remote_full_path}", # 新しい設定を追加
        "end",
        "write memory" # 設定保存
    ]
    try:
        # send_config_setで設定コマンドリストをまとめて投入します
        output = net_connect.send_config_set(config_commands)
        print(f"DEBUG: Boot config change output:\n{output}")

        # 設定が正しく反映されたかを確認するコマンドを実行します
        # show running-config | include boot system
        verify_command = "show running-config | include boot system"
        verify_output = net_connect.send_command(verify_command)
        print(f"DEBUG: Verify boot config output:\n{verify_output}")

        # 期待する新しいブートパスが出力に含まれているか確認
        if remote_full_path in verify_output:
            print("INFO: Boot configuration successfully updated and verified.")
            return True
        else:
            print("ERROR: Failed to verify boot configuration change.")
            return False

    except Exception as e:
        print(f"ERROR: An error occurred during boot configuration change: {e}")
        return False

### 機器のリロード (再起動)
def reload_device(net_connect):
    """
    ネットワーク機器をリロード(再起動)します。
    リロードコマンドは通常確認プロンプトが表示されるため、対話的な応答が必要です。
    """
    print("INFO: Reloading device...")
    # リロードコマンドは機器やOSによって異なります (例: reload, system reboot)
    command = "reload"
    output = ""
    try:
        # 'reload'コマンドを送信し、確認プロンプトに応答する
        # Netmikoのsend_command_timingを使用し、プロンプトを検出して応答を送信します。
        # 典型的なプロンプト: "Proceed with reload? [confirm]" や "Save? [yes/no/cancel]"
        output += net_connect.send_command_timing(command, last_output=output, delay_factor=5) # プロンプト表示まで少し待つ

        # 設定保存のプロンプトが出た場合(設定保存していない場合など)
        if "Save? [yes/no/cancel]" in output:
             print("INFO: 'Save config?' prompt detected. Sending 'no'.")
             output += net_connect.send_command_timing("no", last_output=output, delay_factor=5)

        # リロード確認のプロンプト
        if "Proceed with reload? [confirm]" in output:
            print("INFO: 'Proceed with reload?' prompt detected. Sending 'confirm'.")
            output += net_connect.send_command_timing("confirm", last_output=output, delay_factor=5)
        elif "confirm" in output.lower(): # より一般的な確認プロンプト
             print("INFO: 'confirm' like prompt detected. Sending 'confirm'.")
             output += net_connect.send_command_timing("confirm", last_output=output, delay_factor=5)
        else:
             print("WARNING: No clear reload confirmation prompt detected. Proceeding assuming reload initiated.")


        print("INFO: Reload command sequence completed. Device should now be reloading.")
        # リロード後、機器はSSH接続を切断します。
        # この関数はリロード指示が成功したと判断して終了します。
        return True
    except Exception as e:
        print(f"ERROR: An error occurred during reload command execution: {e}")
        # reloadコマンド自体が失敗した場合など
        return False

### 起動後の状態確認
# リロード後、機器が起動するのを待ってから再接続し、バージョンやブート設定を確認します
def verify_post_reload(device_info, expected_version, expected_boot_path, wait_time=300, max_attempts=10, retry_interval=30):
    """
    リロード後の機器への再接続を試み、指定されたOSバージョンとブートパスを確認します。
    """
    print(f"INFO: Waiting for device to come back online ({wait_time} seconds initial wait)...")
    time.sleep(wait_time) # 機器が起動するまでしばらく待つ

    attempts = 0
    while attempts < max_attempts:
        attempts += 1
        try:
            print(f"INFO: Attempting to connect to {device_info['host']} (Attempt {attempts})...")
            # 接続確立を試みる
            net_connect = Netmiko(**device_info)
            print("INFO: Successfully reconnected after reload.")

            # バージョン確認
            print("INFO: Verifying OS version...")
            # show versionコマンドの出力は長くなるため、適宜フィルタリングやパースが必要です
            version_output = net_connect.send_command("show version", delay_factor=5) # ベンダーやOSによりコマンドは異なります
            print(f"DEBUG: show version output (excerpt):\n{version_output[:500]}...") # 出力の一部を表示
            # 出力からバージョン文字列をパースして比較するロジックが必要
            if expected_version in version_output: # 簡易的な確認例
                print(f"INFO: OS version '{expected_version}' confirmed.")
            else:
                print(f"WARNING: OS version mismatch. Expected '{expected_version}' not found in output.")
                # より正確なバージョンパースと比較ロジックを実装してください

            # ブート設定確認
            print("INFO: Verifying boot configuration...")
            boot_output = net_connect.send_command("show running-config | include boot system") # ベンダーやOSによりコマンドは異なります
            print(f"DEBUG: show running-config | include boot system output:\n{boot_output}")
            if expected_boot_path in boot_output:
                 print(f"INFO: Boot path '{expected_boot_path}' confirmed.")
            else:
                 print(f"WARNING: Boot path mismatch. Expected '{expected_boot_path}' not found in output.")
                 # より正確なブートパスパースと比較ロジックを実装してください

            net_connect.disconnect()
            print("INFO: Post-reload verification completed.")
            return True # 確認完了、成功

        except (NetmikoTimeoutException, NetmikoAuthenticationException) as e:
            print(f"WARNING: Connection failed after reload: {e}. Retrying in {retry_interval} seconds...")
            time.sleep(retry_interval) # 再接続を試みる前に待つ
        except Exception as e:
             print(f"WARNING: An unexpected error occurred during post-reload verification: {e}. Retrying in {retry_interval} seconds...")
             time.sleep(retry_interval) # その他のエラーの場合も待って再試行

    print(f"ERROR: Failed to connect or verify device state after {max_attempts} attempts.")
    return False # 指定回数試行しても成功しなかった

### メイン処理の例

if __name__ == "__main__":
    # 事前に確認しておく必要のある情報
    expected_os_version = "YOUR_EXPECTED_OS_VERSION" # 例: "17.6.3a" - show version出力から確認
    expected_checksum = "YOUR_EXPECTED_MD5_CHECKSUM" # 例: "a1b2c3d4e5f678901234567890abcdef" - ベンダーサイト等で確認
    remote_full_path = f"{remote_flash_path}{remote_image_name}" # 機器上での新しいOSイメージのフルパス

    try:
        # 機器へ初期接続
        print(f"INFO: Attempting initial connection to {device['host']}...")
        with Netmiko(**device) as net_connect:
            print("INFO: Initial connection successful.")

            # OSイメージ転送
            if not transfer_image(net_connect, local_image_path, remote_image_name, remote_file_system):
                print("FATAL ERROR: OS image transfer failed. Aborting process.")
                # ファイル転送に失敗した場合のクリーンアップ処理(機器に残った不完全なファイルを削除するなど)も検討
                exit(1)

            # チェックサム確認
            # OSアップグレード作業では、転送したイメージファイルのチェックサムが
            # ベンダーから提供された値と一致することを確認するのが極めて重要です。
            # これにより、ファイルの破損や改ざんを防ぎます。
            if not verify_checksum(net_connect, remote_full_path, expected_checksum):
                print("FATAL ERROR: Checksum verification failed. The transferred file may be corrupted or invalid. Aborting process.")
                # 不正なファイルを削除する処理なども検討
                exit(1)

            # ブート設定変更
            if not change_boot_config(net_connect, remote_full_path):
                print("FATAL ERROR: Failed to change boot configuration. Aborting process.")
                # 設定変更に失敗した場合のロールバック(boot systemを元に戻すなど)も検討
                exit(1)

            # ここで一度オペレーターによる最終確認のための待機を入れることも現実的です。
            # input("INFO: Boot configuration updated. Press Enter to proceed with reload or Ctrl+C to abort...")

            # リロード
            # リロードコマンド発行後、機器は切断されます。
            # このステップでは、リロードコマンド発行と確認プロンプト応答までを行います。
            # リロード自体の完了は verify_post_reload 関数で確認します。
            if not reload_device(net_connect):
                print("FATAL ERROR: Failed to initiate device reload command. Manual intervention required.")
                exit(1)

        # リロード後、機器が起動するのを待って再接続し、起動状態を確認
        # Netmikoオブジェクトはreload_deviceで切断されるため、withブロックの外で新しい接続を試みます。
        print("\nINFO: ---- Starting Post-Reload Verification ----")
        if not verify_post_reload(device, expected_os_version, remote_full_path):
             print("FATAL ERROR: Post-reload verification failed. Please check the device manually immediately.")
             exit(1)
        print("INFO: ---- Post-Reload Verification Completed ----")


        print("\nINFO: OS upgrade process steps completed successfully.")
        print("INFO: Please perform final manual checks (network functionality, etc.) to ensure successful operation.")

    except NetmikoAuthenticationException:
        print(f"FATAL ERROR: Authentication failed for {device['host']}. Check credentials.")
        exit(1)
    except NetmikoTimeoutException:
         print(f"FATAL ERROR: Connection timed out to {device['host']}.")
         exit(1)
    except Exception as e:
        print(f"FATAL ERROR: An unexpected error occurred during the main process: {e}")
        exit(1)

コード例の補足説明

上記のコードは、ネットワーク機器のOSアップグレード作業における自動化可能な主要ステップを示すための抜粋です。実際の環境や機器の種類(ベンダー、OSバージョン)によって、使用するコマンド、ファイル転送の方法(SCP, SFTP, TFTP, FTP)、エラーメッセージのパース方法などが異なります。

より高度な考慮点

まとめ

この記事では、Pythonとnetmikoライブラリを中心に、ネットワーク機器のOSアップグレード作業におけるファイル転送、ブート設定変更、リロード、起動確認といった一部のタスクを自動化・効率化する手法とスクリプト例をご紹介しました。

Pythonのスキルをお持ちのエンジニアの皆様にとって、ネットワーク機器の直接操作に慣れていなくても、既存のスキルを活かしてこれらの定型作業をスクリプト化することは十分に可能です。これにより、手動での作業負荷を大幅に軽減し、人為的なミスによるリスクを低減することができます。

OSアップグレードのような重要な作業では、すべてのステップを完全に自動化することが常に最善とは限りません。スクリプトで自動化できる部分と、人間の判断や確認が必要な部分を適切に分け、エラーハンドリングや事後検証をしっかりと組み込むことが、安全かつ確実に作業を進める上で非常に重要となります。

ご紹介したスクリプト例は基本的な構成を示したものであり、実際の環境や機器の種類に合わせて詳細なカスタマイズが必要です。特に、コマンドの出力パースやエラー発生時のリカバリー処理は、対象機器の特性に合わせて慎重に実装してください。この記事が、皆様のネットワーク自動化への取り組みの一助となれば幸いです。