- はじめに -
shellでsshのパスワード入力などの対話行動を自動化するには、expectコマンドを使うのが一般的である。
対して、xonshではPythonの記述やパッケージの利用が可能となる。
本記事は、pythonを利用しながら筆者が利用しているssh周りのコードを例にとってshell対話自動化を行う際のtipsをまとめるものである。
- pexpect -
pythonを利用して、対話自動化を行うパッケージとしてpexpectがある。
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