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

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

## sock

`proxycommand`には`$(command)`(シェル置換)を記述できるが、paramikoは[これを解釈しない](https://github.com/paramiko/paramiko/pull/1207)。subprocessを噛ませることでなんとかする必要があった。

## まとめ

```py: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を自前実装してしまいました。