読者です 読者をやめる 読者になる 読者になる

チューリング不完全

What are you afraid of? All you have to do is try.

Fabricで多段ssh

python

Fabricで多段sshした上でコマンド実行させることができたので、書いておきます。

  • 環境
    • Python 2.6.6
    • Fabric 1.5.1 (paramiko 1.9.0, ssh 1.8.0)
    • サーバは全てCentOS 6.3

やり方は.ssh/configに書かれたProxyCommand設定を読み込む方法とFabricのenvをいじる方法の2つがあります。
ここではFabricのenvをいじる方法について解説します。

環境

+---------------+
|192.168.200.99 |
+---------------+
       ↓
+---------------+
|192.168.200.100|
+---------------+
       ↓
+---------------+
|192.168.200.101|
+---------------+
       ↓
+---------------+
|192.168.200.102|
+---------------+
  • 192.168.200.99

プログラムを動かすところ。

  • 192.168.200.100

踏み台役。アクセス制限や鍵などの設定は何もしていない。

  • 192.168.200.101

TCP Wrapperで、sshでの接続は192.168.200.100からのみに制限している。

  • 192.168.200.102

TCP Wrapperで、sshでの接続は192.168.200.101からのみに制限している。


コード

# fabric_test.py
from fabric.api import run, env
from fabric.state import connections, output
from fabric.network import denormalize
from fabric.exceptions import NetworkError

via = [('user1@192.168.200.100:22', 'password1'),
       ('user2@192.168.200.101:22'. 'password2'),
       ('user3@192.168.200.102:22', 'password3')]

try:
    for host, passwd in via:
        env.gateway = env.host_string
        env.host_string = host
        env.password = passwd
        run('', quiet=True)
    run('hostname')    

except NetworkError as e:
    print(e)
finally:
    for key in connections.keys():
        if output.status:
            print("Disconnection from %s" $ denormalize(key))
            connections[key].close()

実行結果

% python fabric_test.py
[user3@192.168.200.102] run: hostname
[user3@192.168.200.102] out: server3
[user3@192.168.200.102] out:

Disconnection from user3@192.168.200.102...
Disconnection from user2@192.168.200.101...
Disconnection from user1@192.168.200.100...

鍵情報が必要な場合は、env.key_filenameにファイルパスを渡してあげるといいです。
(ただし、ファイルパスは当然プログラムが動いている環境のパスになります。)
基本的にはtryの部分の処理のみでOKなのですが、プログラムが終了するときにconnectionをcloseしてあげないと、以下の様なエラーが飛んできます。

Exception in thread Thread-2 (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "/usr/lib64/python2.6/threading.py", line 532, in __bootstrap_inner
  File "/usr/lib/python2.6/site-packages/paramiko/transport.py", line 1613, in run
<type 'exceptions.AttributeError'>: 'NoneType' object has no attribute 'error'
Exception in thread Thread-3 (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "/usr/lib64/python2.6/threading.py", line 532, in __bootstrap_inner
  File "/usr/lib/python2.6/site-packages/paramiko/transport.py", line 1613, in run
<type 'exceptions.AttributeError'>: 'NoneType' object has no attribute 'error'



初めはFabric1.4.3の状態で「うーん、できない・・・」と悩んでいたのですが、Fabric1.5でenv.gatewayと.ssh/configのProxyCommandを読み込む機能が追加されたのを知り、上記のような感じになりました。

追記(2014/10/07)

上記コードではFabricの現行のバージョンで動きませんので、修正しました。
Fabricの1.6.4/1.7.5/1.8.5/1.9.1/1.10.0で動作確認済みです。
1.5.5では動きませんでした。

# fabric_test.py
from fabric.api import run, env
from fabric.state import connections, output
from fabric.network import denormalize
from fabric.exceptions import NetworkError

via = [('user1@192.168.200.100:22', 'password1'),
       ('user2@192.168.200.101:22'. 'password2'),
       ('user3@192.168.200.102:22', 'password3')]

try:
    for host, passwd in via:
        env.gateway = env.host_string
        env.host_string = host
        env.password = passwd
        env.passwords[host] = passwd
        connections.connect(host)
    run('hostname')    
except NetworkError as e:
    print(e)
finally:
    for key in connections.keys():
        if output.status:
            print("Disconnection from %s" % denormalize(key))
            connections[key].close()