paramikoの接続設定をssh/configから読み込む

URL

基本
https://qiita.com/cielavenir/items/9d068c49186060a47650

Pythonの.ssh/config読み出し
https://qiita.com/cielavenir/items/6aa9e6dc1166ae947c6f

大きいファイルの取得
https://qiita.com/cielavenir/items/f38223c156e4aaab58ad


前説

ホスト名にエイリアスを指定すると、Perl/Rubyの場合は良きに計らってくれるが、Pythonはそうではないので、手動で設定する必要があるらしい。幸い、パーサはparamikoライブラリが用意してくれている。ただ、はまりどころが多い。


port

int()で囲う必要がある。


key_filename

certificatefileidentityfileを結合するのが普通と思うが、返すものが単一の文字列なのか文字列のリストなのかよくわからない。リストでなければリストに変換するgetlist()ラッパーを作った。


sock

proxycommandには$(command)(シェル置換)を記述できるが、paramikoはこれを解釈しない。subprocessを噛ませることでなんとかする必要があった。


まとめ


paramiko_config.py

#!/usr/bin/python

#Py2/Py3 are supported
import os,sys,subprocess,glob
import paramiko

def getlist(h,k):
if k not in h:
return []
if not isinstance(h[k],list):
return [h.pop(k)]
return h.pop(k)

def bash_expand(s):
return subprocess.check_output(['bash','-c','echo "%s"'%s]).decode('utf-8')

def parse_config(hostname, username=None, configfile='~/.ssh/config'):
configfile = os.path.expanduser(configfile)
cfg = {
'hostname':hostname,
'username':username if username is not None else os.environ['USER'],
'port':22,
'compress':False,
}
configfile_dir = os.path.dirname(configfile)
configfiles = [configfile]
configfiles_added = {configfile}
while configfiles:
configfile = configfiles.pop(0)
if os.path.exists(configfile):
print('Loading '+configfile)
with open(configfile) as f:
for line in f:
args=line.split()
if args and args[0].lower()=='include':
for incl in args[1:]:
if incl[0]=='~':
incl = os.path.expanduser(incl)
elif incl[0]!='/':
incl = os.path.join(configfile_dir,incl)
for e in sorted(glob.glob(incl)):
if e not in configfiles_added:
configfiles_added.add(e)
configfiles.append(e)
f.seek(0)
ssh_config = paramiko.SSHConfig()
ssh_config.parse(f)
user_config = ssh_config.lookup(hostname)
print(user_config)
cfg['hostname'] = user_config.pop('hostname') # hostname could be alias(?)
cfg['username'] = user_config.pop('user',cfg['username'])
cfg['port'] = int(user_config.pop('port',cfg['port']))
if 'compression' in user_config:
v = user_config.pop('compression')
if v.lower()=='yes':
cfg['compress'] = True
elif v.lower()=='no':
cfg['compress'] = False
if 'certificatefile' in user_config or 'identityfile' in user_config:
if 'key_filename' not in cfg:
cfg['key_filename'] = []
cfg['key_filename'] += [os.path.expanduser(e) for e in getlist(user_config,'certificatefile')+getlist(user_config,'identityfile')]
if 'proxycommand' in user_config:
cfg['sock'] = paramiko.ProxyCommand(bash_expand(user_config.pop('proxycommand')))
print('[paramiko config]')
print(cfg)
return cfg

with paramiko.SSHClient() as ssh:
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(**parse_config(host))
with ssh.open_sftp() as sftp:
with sftp.file('.ssh/authorized_keys','r') as f:
sys.stdout.write(f.read().decode('utf-8'))



190125

これをベースにしたスクリプトが一定の条件下で動かなかったので、怒りに任せて(笑)Includeを自前実装してしまいました。