Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

xonshにおけるpexpectを利用した対話コマンド自動化

- はじめに -

shellでsshのパスワード入力などの対話行動を自動化するには、expectコマンドを使うのが一般的である。

対して、xonshではPythonの記述やパッケージの利用が可能となる。

本記事は、pythonを利用しながら筆者が利用しているssh周りのコードを例にとってshell対話自動化を行う際のtipsをまとめるものである。


 

- pexpect -

pythonを利用して、対話自動化を行うパッケージとしてpexpectがある。

github.com

pipで導入する。

pip install pexpect

 

xonshでpexpectを利用する

xonshrcにでもそれっぽく書く。注意すべきは、pexpectの画面サイズで、デフォルトでは画面サイズが80, 120でセッションが作られるため、ssh先でtmuxやvimを開くと画面が小さいみたいな現象が発生する(iterm2やプロンプトで確認)。

sshとscpのパスワード自動入力の例を以下に示す。

import pexpect
import curses

passwd = 'password'

def _ssh(x):
    # 画面サイズ指定しながらssh
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    p.setwinsize(term_lines,term_cols)
    # password
    p.expect('ssword:*')
    p.sendline('passwd')
    # 操作できるように
    p.interact()
aliases["ssh"] = _ssh


def _scp(x):
    p = pexpect.spawn("scp " + x[0] + ' ' + x[1])
    p.expect('ssword:*')
    p.sendline('passwd')
    p.interact()
aliases["scp"] = _scp

パスワードだけなら普通に暗号鍵作っておけば問題ないが、キーパスフレーズや後述する2段階認証も同じ方法で取り繕える。

 

xonshでpxsshを利用する

最初にpexpectを利用した方法を記載したが、pexpectにはpxsshなるssh専用モジュールが付いてくる。
pxssh - control an SSH session — Pexpect 4.6 documentation


sshのみであれば、簡易に以下のように実装できる。

from pexpect import pxssh

username = 'root'
passwd = 'password'

def _ssh(x):
    p = pxssh.pxssh()
    p.login (x[0], username, password)
    p.prompt()
aliases["ssh"] = _ssh

簡易に書ける分、2段階認証や踏み台等の特殊な環境に対応するのが少しばかり難しいため、筆者は前述のpexpect実装を利用している。


 

ssh configと周辺関数

筆者のssh configは大体以下のように書かれている

HostKeyAlgorithms +ssh-dss
AddKeysToAgent yes
host *
    ForwardAgent yes

// 踏み台なし
Host hoge
    HostName hoge.jp

// 踏み台あり
Host piyo
    HostName piyo.aws.piyopiyo
    IdentityFile ~/.ssh/id_rsa
    LocalForward 8888 localhost:8888
    ProxyCommand ssh step.piyo.co.jp ncat %h %p
    IdentitiesOnly yes

これで踏み台にKeyを送りつつ、主にポートフォワーディングして作業を行っている形である。

ssh piyo

仕事柄、数十のサーバにアクセスするので、「Host名教えてくれ」と言われた時のためにxonshrcに以下を記載している。

def _get_host():
    cat ~/.ssh/config | grep Host
aliases['get_host']=_get_host

Hostの略称とポートを一括で引っ張ってきている。


また、ssh先のホスト名をpecoに流し、ctrl+sでssh先をよしなに検索できるようにしている。

import re
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, EmacsInsertMode, ViInsertMode)

# sshをpecoに流す
def get_ssh():
    items = re.sub(r'(?i)host ', '', $(cat ~/.ssh/config /etc/ssh/ssh_config | grep -i '^host'))
    return items

@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    # ptk 2.xでは不要
    handler = bindings.registry.add_binding

    # ptk 2.xでは @bindings.add('c-v') とする
    @handler(Keys.ControlS)
    def select_sh(event):
        hosts = '\n'.join([x for x in get_ssh().split('\n') if x!='*' and 'HostKey' not in x])
        selected = $(echo @(hosts) | peco)
        if selected:
            if 'kukai' in selected:
                event.current_buffer.insert_text('ssh ' + selected.strip())
            else:
                event.current_buffer.insert_text('ssha ' + selected.strip())


xonshrcの書き方については以下に書いてある。
vaaaaaanquish.hatenablog.com


 

- パスワード管理と2段階認証 -

上記方法だとxonshrcにパスワードやユーザ名を書くことになってあまり嬉しくない。
もちろん一般的に鍵を用意してAddKeysToAgentする方法を取るが、それでもパスフレーズを定期的に要求されるわけで、それらを全ての環境でよしなにやりたいのでパスワード管理ソフトウェアに一任する。

筆者は普段からパスワード管理に1password、2段階認証管理にAuthyを利用しており、それぞれCLIから呼び出して扱えるようにしている。
(※ 1passwordもAuthy同様に2段階認証管理が行えるが、一身上の技術的都合によりAuthyを利用している)

若干設定して使えるようになるまでがダルいので以下記事に設定を託す。

- 1password
support.1password.com
dev.classmethod.jp
- Authy
qiita.com
 

1password-cli

1password-cliの設定が終わってopコマンドが利用できるようになっていれば、1passwordにCLIからログインした上で、以下のように記載する事でssh先のHost名と同じパスワードを取得しssh時に利用できる。後はよしなにパスフレーズを要求されたら〜とかHost死んでたら〜みたいな分岐を好みで追加していく。

import pexpect
import curses

def _ssh(x):
    # 1passwordからパスワード取得 
    PASSWORD = $(op get item x[0] | jq '.details.fields[] | select(.designation=="password").value')
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    p.setwinsize(term_lines,term_cols)
    p.expect('ssword:*')
    p.sendline(PASSWORD.strip())
    p.interact()
aliases["ssh"] = _ssh

 

Authy

Authyで生成されるtokenをmfacodegenコマンドで取得できるよう設定していれば、以下のようにVerification codeとして入力する事ができる。2段階認証が必要なsshもこれで通す。

import pexpect
import curses

def _ssh(x):
    # 二要素tokenをクリップボードにコピー
    mfacodegen -c -s HOGE
    p = pexpect.spawn("ssh " + x[0])
    curses.setupterm()
    term_lines = int(curses.tigetnum("lines"))
    term_cols = int(curses.tigetnum("cols"))
    # クリップボードからtoken取得
    PS = $(pbpaste)
    p.expect("Verification code:")
    p.sendline(PS)
    p.interact()
aliases["ssh"] = _ssh

 

おわりに

大体これでなんとかなると思います。

pexpectもexpectも毎回書き方忘れるけど、結構便利なので使うと良いです。

以下も参考になります。
qiita.com