記事の内容
下図(等幅フォントで見てね)のように、Pythonコードから踏み台サーバーを経由してVPSに多段SSH接続します。
そしてVPSで何らかのコマンドを実行し、stdout/stderrへの出力をPython側で受け取ります。
┌───────┐
│ │configで設定するホスト名:vps
│ VPS │OS:CentOS 8.2
│ │sshポート:2222
│ │Ed25519鍵
└───────┘
↑
┌───┴───┐
│ │configで設定するホスト名:jump_host
│踏み台サーバー│OS:Ubuntu 20.04
│ │sshポート:22
│ │ECDSA鍵
└───────┘
↑
┌───┴───┐
│ │OS:WSL(Ubuntu 20.04)
│ Pythonコード │Python 3.8.5
│ │
└───────┘
ssh環境
Python側のマシンで、ssh用に以下のファイルを用意します。
- 踏み台サーバーの秘密鍵: ~/.ssh/id_ecdsa
- VPSの秘密鍵: ~/.ssh/id_ed25519
- sshのconfigファイル: 以下の通り
Host jump_host
Hostname XXX.XXX.XXX.XXX # 書き換えてください
User usrname # 書き換えてください
Port 22
IdentityFile ~/.ssh/id_ecdsa
Host vps
Hostname YYY.YYY.YYY.YYY # 書き換えてください
User usrname # 書き換えてください
Port 2222
IdentityFile ~/.ssh/id_ed25519
ProxyCommand sshpass -f ./.pass -P "Enter passphrase for key" ssh -o StrictHostKeyChecking=no -W %h:%p jump_host
vpsホストのProxyCommandは、「ProxyCommand ssh -W %h:%p jump_host」のように通常の書き方で書くと、Pythonコード実行時に踏み台サーバーのパスフレーズの手入力を求められます(パスフレーズが設定してあれば)。
今回はsshpass(=インストールしてください)を使用して、~/.ssh/.passファイル(=準備が必要です)からパスフレーズを読み込んで、踏み台サーバーのパスフレーズを自動入力させるように設定しています。
Pythonコード
以下にPythonコードを記します。
コードが読み込む設定ファイル(.secret.ini)は後述します。
paramikoというライブラリを使用してSSH接続します。
コードで logger という変数を使っていますが、logging周りのコードは割愛しましたのでご了承ください。
import configparser
from pathlib import Path
import datetime
import paramiko
from paramiko.config import SSH_PORT
current_directory = "."
def ssh_out(stderr, stdout, host, username, cmd):
"""
ホストで実行したコマンドの出力結果をログに出力します
"""
logger.info(f'''\
=============
[{username}@{host}]$ {cmd}
=============''')
s = ""
for e in stderr:
s = s + e
if 0 < len(s):
logger.info(s)
s = ""
for o in stdout:
s = s + o
if 0 < len(s):
logger.info(s)
def ssh_cmd(ssh_config, host, key_file, passphrase, sudo_password, list_cmd):
"""
ホストにssh接続してコマンドを実行し、その出力結果をログに出力します
"""
logger.info(f'''\
*************
host = {host}
*********''')
# sshのconfigファイルから値を取り出す
lookup = ssh_config.lookup(host)
hostname = lookup["hostname"]
username = lookup["user"]
try:
port = int(lookup["port"])
except Exception:
port = SSH_PORT
key_filename = lookup['identityfile'],
# 秘密鍵ファイルからキーを取得(パスフレーズを設定している場合)
try:
pkey = paramiko.Ed25519Key.from_private_key_file(key_file, passphrase)
except Exception:
try:
pkey = paramiko.ECDSAKey.from_private_key_file(key_file, passphrase)
except Exception:
try:
pkey = paramiko.RSAKey.from_private_key_file(key_file, passphrase)
except Exception:
pkey = None
# 踏み台経由の場合
try:
sock = paramiko.ProxyJump(lookup["proxyjump"])
except Exception:
try:
sock = paramiko.ProxyCommand(lookup["proxycommand"])
except Exception:
sock = None
# ssh接続
ssh_client = paramiko.SSHClient()
try:
ssh_client.load_system_host_keys()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(
hostname=hostname,
port=port,
username=username,
key_filename=key_filename,
pkey=pkey,
sock=sock,
)
# ホスト側でコマンド実行
for cmd in list_cmd:
stdin, stdout, stderr = ssh_client.exec_command(cmd)
if "sudo" in cmd:
stdin.write(sudo_password + '\n')
stdin.flush()
ssh_out(stderr, stdout, host, username, cmd)
finally:
ssh_client.close()
def main():
"""
メイン
"""
# iniファイルの読み込み
ini = configparser.ConfigParser()
ini.read(f"{current_directory}/.secret.ini", encoding='utf-8')
# ~/.ssh/config
SECTION = "common"
ssh_config_file = ini[SECTION]["ssh_config_file"]
# 踏み台
SECTION = "jump_host"
jmp_key_file = ini[SECTION]["key_file"]
jmp_passphrase = ini[SECTION]["passphrase"]
jmp_sudo_password = ini[SECTION]["sudo_password"]
# 踏み台経由で入るVPS
SECTION = "vps"
vps_key_file = ini[SECTION]["key_file"]
vps_passphrase = ini[SECTION]["passphrase"]
vps_sudo_password = ini[SECTION]["sudo_password"]
# ~/.ssh/configの読み込み
ssh_config = paramiko.SSHConfig()
ssh_config.parse(open(ssh_config_file, 'r'))
# まず、踏み台に入ってみる
host = "jump_host"
# ホストで実行させるコマンド
list_cmd = [
"cat /etc/issue", # Ubuntuのバージョン表示
"sudo -S ls -a",
]
ssh_cmd(ssh_config, host, jmp_key_file, jmp_passphrase, jmp_sudo_password, list_cmd)
# 踏み台経由でVPSに入ってみる
host = "vps"
# ホストで実行させるコマンド
list_cmd = [
"cat /etc/centos-release", # CentOSのバージョン表示
"sudo -S ls -a",
]
ssh_cmd(ssh_config, host, vps_key_file, vps_passphrase, vps_sudo_password, list_cmd)
if __name__ == '__main__':
main()
このPythonコードが読み込む設定ファイルを、以下の内容で準備する必要があります。
本記事では、ここに各ホストのパスフレーズとパスワードを書いてしまっていますので、注意が必要です。
運用時には、それぞれの状況に合わせて安全な方法をご検討ください。
[common]
ssh_config_file = /home/usrname/.ssh/config
[jump_host]
key_file = /home/usrname/.ssh/id_ecdsa
passphrase = secret
sudo_password = secret
[vps]
key_file = /home/usrname/.ssh/id_ed25519
passphrase = secret
sudo_password = secret
実行結果
Pythonコードを実行すると、jump_hostとvpsでそれぞれコマンドが動き、以下のように結果がログに出力されました(記事用に加工しています)。
*************
host = jump_host
*********
=============
[usrname@jump_host]$ cat /etc/issue
=============
Ubuntu 20.04.1 LTS \n \l
=============
[usrname@jump_host]$ sudo -S ls -a
=============
[sudo] usrname のパスワード:
.
..
.bash_history
.bash_logout
.bashrc
.ssh
ダウンロード
テンプレート
デスクトップ
ドキュメント
ビデオ
ピクチャ
ミュージック
公開
*************
host = vps
*********
=============
[usrname@vps]$ cat /etc/centos-release
=============
CentOS Linux release 8.2.2004 (Core)
=============
[usrname@vps]$ sudo -S ls -a
=============
[sudo] usrname のパスワード:
.
..
.bash_history
.bash_logout
.bash_profile
.bashrc
.ssh
.zshrc
Process finished with exit code 0
以上です。