【pexpect】PythonでSSH接続とコマンド実行を自動化【対話型】

2023-07-29

PythonでSSH接続とコマンド実行を自動化

この記事の目的

  • PythonでルーターやサーバーへのSSH接続を自動化したい。
  • Tera Termマクロやシェルスクリプトではなく、Pythonが最善の選択肢である。
  • netmiko/paramiko以外のライブラリが要件に適う。
  • pexpectライブラリの実践的な使い方が知りたい。
  • ルーターやサーバーに対して対話的にコマンドを実行したい。

このような方向けに、Pythonの「pexpectライブラリ」を使って、「SSH接続とコマンド実行を自動化する方法」をまとめました。

ご注意事項

  • 本記事における自動SSH接続プログラムの実行環境は、LinuxOS上での動作を前提としています。
  • WindowsOS上では基本的に動作しません。
  • WindowsOSで実行する場合は、「WSL」や「VitualBox」や「VMwareWorkstation」などを用いて、LinuxOS環境をご用意下さい。

SSH接続自動化にPythonを使う理由

ネットワーク作業自動化には、主に以下のような手段があります。

  • シェルスクリプト
  • Tera Termマクロ
  • プログラミング言語(Python,Ruby,Perl .etc)

シェルスクリプトやTera Termマクロは、多くのインフラエンジニアが利用しています。

しかしより高度で自由度が高い処理には、純粋なプログラミング言語であるPythonが向いています。

更にPythonは初心者が習得しやすい言語と言われます。

よって、普段コードを書く機会が少ないインフラエンジニアとって、Pythonは最適な言語だと考えました。

自動化用Pythonライブラリ

ネットワーク作業が自動化出来る主なPythonのライブラリは、以下の4種類です。

ライブラリ名 特長
pexpect
(ピーエクスペクト)
  • SSHとtelnetのどちらでも接続可能
  • 対話的にコマンドを投入できる
  • コード量は増えるが操作の自由度は高い
  • 動作対象機器のベンダーを問わず、マイナーなベンダー機器でも動作可能

netmiko
(ネットミコ)

  • SSHでの接続に用いる
  • 単純な操作であれば使いやすい
  • 複雑な操作には向かない
  • 動作対象が主要ベンダー機器のOSに限定
  • マイナーなベンダー機器のOSはサポートしていない
  • paramiko(パラミコ)をベースに開発
napalm
(ナパーム)
  • SSHでの接続に用いる
  • 使いやすいが可能な操作が限られる
  • 動作対象が一部ベンダー機器のOSに限られる
  • マイナーなベンダー機器のOSはサポートしていない
telnetlib
(テルネットライブ )
or
(テルネットリブ)
  • telnetでの接続に用いる
  • Tera Termマクロと同様に対話的にコマンドを投入
  • telnet接続に限れば使いやすい
  • 動作対象のOSを問わない
  • マイナーなベンダー機器でも動作可能

結果的に「pexpect」を使用しました。

なぜなら、ネットワーク構築作業の現場では必ずしも大手の主要ベンダーの製品が使われているとは限らないからです。

主要ベンダーの製品のみで動作する「netmiko」や「napalm」では、対応できないケースもあると考えました。

あらゆるユースケースを想定して、汎用的に使用できるライブラリが有用です。

pexpectの基本動作

使用するPythonのバージョンはPython3.6で、実行環境のOSはLinuxのCentOS7です。

# python3 --version
# Python 3.6.8

まずpipでpexpectをインストールします。

# pip install pexpect

一般には具体的なプログラムを記述する前段階として、フローチャートを作成します。

フローチャートは、プログラムの流れを図解したものです。

フローチャートでは様々な形のアイコンを用いて、実行させたい処理内容を表現します。

以下にて主なアイコンとその処理内容を一覧にしました。

フローチャートアイコン一覧

続いてpexpectの基本動作を確認する実行プログラムとして、「pwd_ps.py」を作成します。

pwd_ps.pyの処理内容をまとめたフローチャートは以下の様になります。

pwd_ps.py_フローチャート_処理内容版

pwd_ps.pyのコード内容をまとめたフローチャートは以下の様になります。

pwd_ps.pyのフローチャート:コード版

処理の流れは一本道で非常にシンプルな動きとなります。

フローチャートを元に、pwd_ps.pyのコードを記述します。

# pexpectをimportする
import pexpect

# rootユーザーに切り替える
p = pexpect.spawn('env LANG=C sudo su -')
p.expect("# ")
print(p.after.decode(encoding='utf-8')) 

# 現在のディレクトリを確認する
p.sendline("pwd")
p.expect("# ")
print(p.before.decode(encoding='utf-8'))
print(p.after.decode(encoding='utf-8')) 


# 実行中のプロセスを一覧表示する
p.sendline("ps")
p.expect("# ")
print(p.before.decode(encoding='utf-8'))
print(p.after.decode(encoding='utf-8')) 

処理内容としては、現在ログイン中のユーザーからrootユーザーに切り替え後、「pwd」と「ps」コマンドを投入し、現在のディレクトリと実行中のプロセスを確認します。

続いて、各コード内容を具体的に解説していきます。

expect関数

カッコ内の文字列を想定して応答を待ち受けます。
p.expect(“# “)ではrootユーザーのプロンプトである「#」の応答を待ち受けています。
「#」の応答が返り次第、次のコマンドの実行に進みます。

sendline関数

カッコ内の文字列を送信します。

p.sendline(“pwd")は「pwd」のコマンドを送信。
p.sendline(“ps") は「ps」のコマンドを送信。

beforeプロパティによる実行結果の出力

beforeプロパティは、expect関数で待ち受ける文字列パターンの直前までの、全てのテキストデータを保持しています。

・print(p.before.decode(encoding=’utf-8′))


print関数とbeforeプロパティを組み合わせることで、sendline関数実行後から、expect関数実行前までの出力結果が表示されます。プロンプト「#」は含まれません。

afterプロパティによる実行結果の出力

afterプロパティは、 expect関数で待ち受ける文字列パターンに一致したテキストデータを保持しています。

・print(p.after.decode(encoding=’utf-8′)


print関数とafterプロパティを組み合わせることで、expect関数で待ち受けたプロンプト「#」が出力結果として表示されます。

プログラムの実行結果

「pwd_ps.py」 を実行すると、以下のような出力が返ります。

「hostname」とディレクトリの部分は、使用環境に応じて読み替えて下さい。

# 
pwd
/root
[root@hostname ~]
# 
ps
  PID TTY          TIME CMD
32182 pts/6    00:00:00 sudo
32184 pts/6    00:00:00 su
32185 pts/6    00:00:00 bash
32200 pts/6    00:00:00 ps
[root@hostname ~]
# 

beforeプロパティによる実行結果の出力

beforeプロパティは、expect関数で待ち受ける文字列パターンの直前までの、全てのテキストデータを保持しています。

・print(p.before.decode(encoding=’utf-8′))


print関数とbeforeプロパティを組み合わせることで、sendline関数実行後から、expect関数実行前までの出力結果が表示されます。プロンプト「#」は含まれません。

afterプロパティによる実行結果の出力

afterプロパティは、 expect関数で待ち受ける文字列パターンに一致したテキストデータを保持しています。

・print(p.after.decode(encoding=’utf-8′)


print関数とafterプロパティを組み合わせることで、expect関数で待ち受けたプロンプト「#」が出力結果として表示されます。

プログラムの実行結果

「pwd_ps.py」 を実行すると、以下のような出力が返ります。

「hostname」とディレクトリの部分は、使用環境に応じて読み替えて下さい。

# 
pwd
/root
[root@hostname ~]
# 
ps
  PID TTY          TIME CMD
32182 pts/6    00:00:00 sudo
32184 pts/6    00:00:00 su
32185 pts/6    00:00:00 bash
32200 pts/6    00:00:00 ps
[root@hostname ~]
# 
よくある悩み
  • beforeとafterの使い分けがややこしい
  • どっちが何のデータを保持してるのか全然分からない

補足として、beforeプロパティとafterプロパティが保持するデータの違いが分かりやすい様にしました。

まず以下スクリプトのprint部分に、コメントで「#出力結果1~5」を追記しました。

# pexpectをimportする
import pexpect

# rootユーザーに切り替える
p = pexpect.spawn('env LANG=C sudo su -')
p.expect("# ")
print(p.after.decode(encoding='utf-8')) #出力結果1

# 現在のディレクトリを確認する
p.sendline("pwd")
p.expect("# ")
print(p.before.decode(encoding='utf-8')) #出力結果2
print(p.after.decode(encoding='utf-8')) #出力結果3


# 実行中のプロセスを一覧表示する
p.sendline("ps")
p.expect("# ")
print(p.before.decode(encoding='utf-8')) #出力結果4
print(p.after.decode(encoding='utf-8')) #出力結果5

次に、プログラム実行後の以下出力内容を上記の「#出力結果1~5」に紐づけました。

つまり、以下の様な対応関係でプロンプトに「#」が表示されます。

print(p.after.decode(encoding=’utf-8′)) #出力結果1

# ⇒出力結果1

# ⇒出力結果1
pwd ⇒出力結果2
/root ⇒出力結果2
[root@hostname ~] ⇒出力結果2
# ⇒出力結果3
ps ⇒出力結果4
  PID TTY          TIME CMD ⇒出力結果4
32182 pts/6    00:00:00 sudo ⇒出力結果4
32184 pts/6    00:00:00 su ⇒出力結果4
32185 pts/6    00:00:00 bash ⇒出力結果4
32200 pts/6    00:00:00 ps ⇒出力結果4
[root@hostname ~] ⇒出力結果4
# ⇒出力結果5

beforeとafterで出力させたい内容を慎重に意識しながら、コードを作成してください。

慣れれば簡単に書けるようになります。

SSH接続と設定自動化

次に、いよいよ自動でSSHログインをします。pexpectライブラリの「pxssh」モジュールを使います。

ご注意

pxsshモジュールはwindowsOSでは非対応のため、importエラーとなります。

以降の記事は、Linux環境でのプログラム実行を前提としています。

もし自宅で学習するような場合には、独自にLinux上でPythonが動作する環境をご用意ください。

WSL」や「VitualBox」や「VMwareWorkstation」などは、手軽にWindowsPCでLinux環境が用意できます。

または、AWS等のクラウドサービスなどもLinuxサーバーのインスタンスが作成できるのでおススメです。

例えば、テスト用に最低限以下2つのサーバーを用意すれば事足ります。

  • Pythonプログラムの実行サーバー:SSHで接続する
  • SSHサーバー:SSHで接続される

これらがAWSサービスのLightsailなら、1台当たり月額5ドル前後でLinuxサーバーを構築できます。

AWSのクラウド環境下でのLinuxサーバーの知見も高まるので一石二鳥です。

サファイア
サファイア

Lightsailは月額利用料金が定額なので、普通に使えば高額課金の心配もありません。

事前準備

SSHで接続される側のノードでは、事前にSSHサーバーとしての設定が必要です。
具体的には、パスワード認証を許可する設定一般ユーザーの作成をしておきます。

未設定の場合は、事前に以下のサイトを参照してご準備下さい。

・外部サイト(Qiita):CentOS7.3でSSH接続(パスワード認証)する方法
 └SSHの設定
 └サービスの起動
 └一般ユーザーの作成

元々SSHサーバーとして起動済みだった場合には、以下のコマンドでsshd.serviceを再起動することで、設定変更を有効にしてください。

# systemctl restart sshd.service

実行プログラム

続いて、任意のSSHサーバーに接続した後に、対話的にコマンドを投入する実行プログラム として、「 ssh_test.py 」を作成します。

ssh_test.pyの処理内容をまとめたフローチャートは以下の様になります。

ssh_test.pyのフローチャート:処理内容版

ssh_test.pyのコード内容をまとめたフローチャートは以下の様になります。

ssh_test.pyのフローチャート:コード版

1つ目のプログラムと同様に、一本道の処理でループや条件分岐はありません。

フローチャートを元に、ssh_test.pyのコードを記述します。

# pexpectライブラリからpxsshモジュールをインポート
from pexpect import pxssh
# timeモジュールをインポート
import time

# ログイン情報を設定しSSHサーバーにログイン
ssh = pxssh.pxssh()
ssh.login(server="x.x.x.x", #接続したいSSHサーバーのIPを記述
          username="testuser", #SSHサーバー側のユーザー名を記述
          password="xxxxxxxx") #SSHサーバー側のユーザーのパスワードを記述
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果1

# テキストファイル「test.txt」を作成する
ssh.sendline("touch test.txt")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果2
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果3
time.sleep(1)

# カレントディレクトリのファイルを表示する
ssh.sendline("ls -l")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果4
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果5
time.sleep(1)

# SSHサーバーからログアウト
ssh.logout()
ssh_test.py:2行目

from pexpect import pxssh

「pxssh」はpexpectライブラリを拡張し、SSH接続に特化した機能を有するモジュールです。

SSHによるログインとログアウト、および接続先ノードのプロンプトを待ち受けるメソッドを追加します。

ssh_test.py:7行目

ssh = pxssh.pxssh()

ssh = pxssh.pxssh()では、pxsshモジュールのpxsshクラスから「ssh」というインスタンスを作成しています。

このsshインスタンスから各種関数を呼び出すことにより、目的となる作業を実行していきます。

1つ目のプログラムと同様に、一本道の処理でループや条件分岐はありません。

フローチャートを元に、ssh_test.pyのコードを記述します。

# pexpectライブラリからpxsshモジュールをインポート
from pexpect import pxssh
# timeモジュールをインポート
import time

# ログイン情報を設定しSSHサーバーにログイン
ssh = pxssh.pxssh()
ssh.login(server="x.x.x.x", #接続したいSSHサーバーのIPを記述
          username="testuser", #SSHサーバー側のユーザー名を記述
          password="xxxxxxxx") #SSHサーバー側のユーザーのパスワードを記述
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果1

# テキストファイル「test.txt」を作成する
ssh.sendline("touch test.txt")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果2
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果3
time.sleep(1)

# カレントディレクトリのファイルを表示する
ssh.sendline("ls -l")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果4
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果5
time.sleep(1)

# SSHサーバーからログアウト
ssh.logout()
ssh_test.py:2行目

from pexpect import pxssh

「pxssh」はpexpectライブラリを拡張し、SSH接続に特化した機能を有するモジュールです。

SSHによるログインとログアウト、および接続先ノードのプロンプトを待ち受けるメソッドを追加します。

ssh_test.py:7行目

ssh = pxssh.pxssh()

ssh = pxssh.pxssh()では、pxsshモジュールのpxsshクラスから「ssh」というインスタンスを作成しています。

このsshインスタンスから各種関数を呼び出すことにより、目的となる作業を実行していきます。

ssh_test.py:8行目

ssh.login()

ssh.login()でsshインスタンスからlogin関数を呼び出し、作業対象のSSHサーバーにログインします。

カッコ内の引数には、接続先情報としてIPアドレスとユーザー名とパスワードを設定します。

ここではあらかじめSSHサーバー側にて、ユーザー名「testuser」でユーザーを作成しておきました。

IPアドレスとパスワードは伏字にしているので、ご利用環境に応じて読み替えて下さい。

ssh_test.py:8行目

ssh.login()

ssh.login()でsshインスタンスからlogin関数を呼び出し、作業対象のSSHサーバーにログインします。

カッコ内の引数には、接続先情報としてIPアドレスとユーザー名とパスワードを設定します。

ここではあらかじめSSHサーバー側にて、ユーザー名「testuser」でユーザーを作成しておきました。

IPアドレスとパスワードは伏字にしているので、ご利用環境に応じて読み替えて下さい。

ssh_test.py:11行目他

print(ssh.after.decode(encoding=’utf-8′), flush=True)

print関数には「flush」オプションを追加しています。

flush=Trueとすることにより、CLI上でプログラム実行中の出力結果が即時表示されます。

つまり、プログラムの進捗を逐次確認するための機能となります。

これを追加しないと、全ての処理が完了してから結果が表示されます。

ssh_test.py:18行目他

time.sleep(1)

timeモジュールのsleep関数にてカッコ内の引数で指定した秒数の間、実行中のプログラムの処理を一時停止できます。

もし設定しない場合、CLIの表示が速過ぎて目で追うことが困難となります。

そのため、time.sleep(1)を挿入することで敢えて1秒間のインターバルを設けています。

ssh_test.py:28行目

ssh.logout()

logout関数を呼び出してexitを送信し、SSH接続先からログアウトしてプログラムを終了します。

プログラムの実行結果

「ssh_test.py」 を実行すると、以下のような出力が返ります。

[PEXPECT]$ 
touch test.txt

[PEXPECT]$ 
ls -l
total 0
-rw-rw-r--. 1 testuser testuser 0 Dec  3 23:12 test.txt

[PEXPECT]$ 

touch test.txtのコマンド実行により、testuserを所有者としてtest.txtが作成されています。

ここでは非常に単純なコマンドを用いましたが、要件に応じてより複雑な処理を対話的に行うことができます。

この自由度の高さが、pexpectのメリットと言えるでしょう。

ここでもbeforeプロパティとafterプロパティの具体的な出力内容が分かりやすい様に、実行プログラムの出力結果1~5と照らし合わせます。

[PEXPECT]$ ⇒出力結果1
touch test.txt ⇒出力結果2

[PEXPECT]$ ⇒出力結果3
ls -l ⇒出力結果4
total 0 ⇒出力結果4
-rw-rw-r--. 1 testuser testuser 0 Dec  3 23:12 test.txt ⇒出力結果4

[PEXPECT]$ ⇒出力結果5

本記事でご紹介する内容は以上となります。

あわせて読みたい記事

本記事の応用として、Pythonの「pexpect」ライブラリを使うことで、複数の機器に連続でリモートSSH接続する方法をまとめました。