Paramiko + scp 導入 ~ SSH接続 ~ SCPでファイル転送

# 概要
1. ssh接続からのコマンド送受信
2. scpでのファイル取得

補足. 
(A) PC側 paramiko + scp 導入
(B) パッケージ依存関係の調査
(C) インストール済みのパッケージのバージョンの調査
(D) パッケージ依存関係の可視化


# やりたいこと
- teratermなどで手作業でやっていたことをpythonで自動化したい。
 - [paramiko を使って Python で SFTP/SCPを行う - Librabuc][10]が近いがSCPのコードがなかった

1. ssh接続からのコマンド送受信
 PC(SSH Client) ---sshでコマンド実行(mkdir, tar, ...)--> ターゲット(SSH Host)

2. scpでのファイル取得
 PC(SSH Client) <---scpでファイルを取得(xxx.tar.gz, ...)---------- ターゲット(SSH Host)

| PC(SSH Client)                        |
|:--------------------------------------|
| Windows 7 (64bit)                     |
| Python 3.5.2 - Anaconda 4.2.0 (64-bit)|

| ターゲット(SSH Host)                    |
|:---------------------------------------|
| IPアドレスだけがわかっている(host名不明)) |
| sshでrootログインできる                  |
| scpできる                               | 

(・・・という処理と調査に参照したサイトを備忘録としてまとめた)

# コード
## 1. ssh接続からのコマンド送受信
192.168.1.100に対してrootでログインして、testというディレクトリを作成したい場合

```py3:ssh_mkdir1.py
import paramiko
with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.1.100', port=22, username='root', password='')
    stdin, stdout, stderr = ssh.exec_command('mkdir test')
```

短くかけていい感じ
なお、ssh.connect()でpassword か private key (もしくは両方)が必要とのこと。

[SSH - Python with paramiko issue - Stack Overflow][12]

> You should provide either a password or a private key (or both), otherwise the SSH client does not know how to authenticate with the login data.

コメント + α をつけると以下のような感じです。

```py3:ssh_mkdir.py
import paramiko

# ssh clientオブジェクト(ssh)を作る・・・
with paramiko.SSHClient() as ssh:

    # どうやってhostname & keyを登録するのかわからないので、AutoAddPolicy()としておく
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    # ssh接続する
    ssh.connect(hostname='192.168.1.100', port=22, username='root', password='')
    
    # mkdir を実行する
    stdin, stdout, stderr = ssh.exec_command('mkdir test')
    
    # 実行結果のstdoutとstderrを読み出す
    for o in stdout:
        print(o)
    for e in stderr:
        print(e)
```



## 2. scpでのファイル取得
### xxx.tar.gzを取得したい場合

```py3:scp.py
import paramiko
import scp

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.1.100', port=22, username='root', password='')

    # scp clientオブジェクト生成
    with scp.SCPClient(ssh.get_transport()) as scp:
       # scpに対してgetするだけでファイルが取得できる
       scp.get('xxx.tar.gz','xxx.tar.gz')
```

### ターゲットにtest.py(10個のファイルを作成するスクリプト)をscpでコピーして実行する場合

```py:test.py
# -*- coding: utf-8 -*-
for n in range(10):
  with open("test" + str(n), "w+") as f:
     f.write("test" + str(n))
```

```py3:scp2.py
# -*- coding: utf-8 -*-
import paramiko
import scp

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.1.100', port=22, username='root', password='')

    stdin, stdout, stderr = ssh.exec_command('mkdir test_dir')
    for o in stdout:
        print(o.strip())

    with scp.SCPClient(ssh.get_transport()) as scp:
       scp.put('test.py','test_dir/test.py')

    stdin, stdout, stderr = ssh.exec_command('cd test_dir;python test.py')
    for o in stdout:
        print(o.strip())

    stdin, stdout, stderr = ssh.exec_command('cd test_dir;ls')
    for o in stdout:
        print(o.strip())
```

```for o in stdout```などで処理待ちを入れないと、lsやpython test.pyの部分で同期が取れないようだ。

 - フォルダ作成前にscp.put()が実行される
 - sh.exec_command('cd test_dir;ls')のタイミングで、python test.pyが未完了であり、期待したフォルダが表示されない。

 といったことがあった

## 最終的な形
これをマージして以下のようにした。

```py3:get_logs.py
# -*- coding: utf-8 -*-
import paramiko
import scp
import pathlib
from datetime import datetime
import sys

log_dir = str(datetime.now().strftime('log_%Y%m%d_%H%M%S'))
log_tar_gz = log_dir + '.tar.gz'

remote_path = log_tar_gz
local_path  = str(pathlib.Path(__file__).resolve().parent.joinpath(log_tar_gz))

collect_log_cmds = [
   'mkdir ' + log_dir, 
   # To Do : log_dirにログを集める処理を何かしら書く
   'tar -zcvf ' + log_tar_gz + ' ' + log_dir
]

cleanup_cmds = [
	'rm -r ' + log_dir,
	'rm ' + log_tar_gz
]

def check_result(stdin, stdout, stderr):
    result = True
    for o in stdout:
        if o:
            print("    " + o.strip())
    for e in stderr:
        if e:
            print("    " + e.strip())
            result = False
    return result

def exec_command(ssh, cmds):
    result = True
    for cmd in cmds:
        print("  " + cmd)
        stdin, stdout, stderr = ssh.exec_command(cmd)
        if check_result(stdin, stdout, stderr):
            pass
        else:
            print("ERROR -- " + cmd)
            result = False
    return result

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.1.100', port=22, username='root', password='')
    
    print("start -- collect_log")
    if exec_command(ssh,  collect_log_cmds):
        print("done -- collect_log)
    else:
        sys.exit()

    print("start -- scp get " + remote_path)
    with scp.SCPClient(ssh.get_transport()) as scp:
        scp.get(remote_path,local_path)

    if pathlib.Path(local_path).is_file():
        print("done  -- scp get " + local_path)
    else:
        print("ERROR -- scp get " + local_path)
        sys.exit()

    print("start -- cleanup")
    if exec_command(ssh, cleanup_cmds):
        print("done  -- cleanup")
    else:
        print("ERROR -- cleanup")
        sys.exit()
 ```


# 補足
## (A) PC側 paramiko + scp 導入
### paramiko 2.2.1 パッケージ依存関係

![paramiko.png](https://qiita-image-store.s3.amazonaws.com/0/34777/2b37dec5-9ddb-3f05-d44d-a3857680eb79.png)

- 図の赤丸がインストールされていなかったパッケージ
- 調査方法は「補足(B)～(D)」を参照

### パッケージ取得
- [pypi][3]にブラウザでアクセスして.whlをダウンロードした

|     required       |   used            |  .whl                                 |
|:------------------:|:-----------------:|:-------------------------------------:|
| bcrypt(>=3.1.3)　| [bcrypt 3.1.3][4]  | bcrypt-3.1.3-cp35-cp35m-win_amd64.whl |
| pynacl(>=1.0.1)  | [pynacl 1.1.2][5]  | PyNaCl-1.1.2-cp35-cp35m-win_amd64.whl |
| paramiko(2.2.1)　| [paramiko 2.2.1][2] | paramiko-2.2.1-py2.py3-none-any.whl |
| scp(0.10.2)　    | [scp 0.10.2][9]     |scp-0.10.2-py2.py3-none-any.whl |

- bcryptの.whlはどれなのかわからなかったので、環境に合わないものをいれたときのエラーでOK／NGを判断した（トライアンドエラーで解決した。）

### パッケージインストール
- pip install <ダウンロードした.whl>で実施

```
> pip install bcrypt-3.1.3-cp35-cp35m-win_amd64.whl
・・・
> pip install PyNaCl-1.1.2-cp35-cp35m-win_amd64.whl
・・・
> pip install paramiko-2.2.1-py2.py3-none-any.whl
・・・・
> pip install scp-0.10.2-py2.py3-none-any.whl
・・・・
```

## (B) パッケージ依存関係の調査
- pip installを実行してごりごり調べた
- 「Collecting bcrypt>=3.1.3 (from paramiko==2.2.1)」といったメッセージで出てくる

```
> pip install paramiko-2.2.1-py2.py3-none-any.whl
Collecting bcrypt>=3.1.3 (from paramiko==2.2.1)
  Retrying (Retry(total=4, connect=None, read=None, redirect=None)) after connection broken by 'NewConnectionError('<pip._vendor.requests.packages.urllib3.connection.VerifiedHTTPSConnection object at 0x02DB4A30>: Failed to establish a new connection: [Errno 11004] getaddrinfo failed',)': /simple/bcrypt/
・・・
```

- paramikoのインストールが可能になると、以下のようになる

```
> pip install paramiko-2.2.1-py2.py3-none-any.whl
Requirement already satisfied: bcrypt>=3.1.3 in ・・・\lib\site-packages (from paramiko==2.2.1)
Requirement already satisfied: cryptography>=1.1 in ・・・\lib\site-packages (from paramiko==2.2.1)
Requirement already satisfied: six in ・・・\lib\site-packages (from pynacl>=1.0.1->paramiko==2.2.1)
Requirement already satisfied: cffi>=1.4.1 in ・・・\lib\site-packages (from pynacl>=1.0.1->paramiko==2.2.1)
Requirement already satisfied: idna>=2.0 in ・・・\ib\site-packages (from cryptography>=1.1->paramiko==2.2.1)
Requirement already satisfied: setuptools>=11.3 in in ・・・\lib\site-packages\setuptools-27.2.0-py3.6.egg (from cryptography>=1.1->paramiko==2.2.1)
Requirement already satisfied: pycparser in ・・・\ib\site-packages (from cffi>=1.4.1->pynacl>=1.0.1->paramiko==2.2.1)
Installing collected packages: pynacl, bcrypt, paramiko
```

[あとでわかったこと][6]・・・ .whlは.zipである。

- paramiko-2.2.1-py2.py3-none-any.whlを.zipに変換して解凍する。
- paramiko-2.2.1.dist-infoにMETADATAが入っている。
- METADATAをテキストエディタで開くと、Requires-Distに書いてある。

ので、調査しようと思えばできそう。

```paramiko-2.2.1.dist-info/METADATA
Metadata-Version: 2.0
Name: paramiko
Version: 2.2.1
・・・
Requires-Dist: bcrypt (>=3.1.3)
Requires-Dist: cryptography (>=1.1)
Requires-Dist: pynacl (>=1.0.1)
Requires-Dist: pyasn1 (>=0.1.7)
・・・
```

## (C) インストール済みのパッケージのバージョンの調査
- pip freezeをすると一覧でだいたい取れる

```
> pip freeze
alabaster==0.7.9
anaconda-client==1.6.0
anaconda-navigator==1.5
・・・略・・・
```

- setuptoolsとpycparserは、pip freezeで出てこないので["__version__"から取得][8]

```pycon:ipython
In [1]: import setuptools

In [2]: setuptools.__version__
Out[2]: '27.2.0'

In [3]: import pycparser

In [4]: pycparser.__version__
Out[4]: '2.14'
```

## (D) パッケージ依存関係の可視化
- 補足(B)と(C)の結果から、dotをテキストエディタでごりごり書いた。
- [pipdeptree][7]を使えばもう少し楽にできそう。


```paramiko_depend.dot
digraph pramiko {
   scp->paramiko
   paramiko->bcrypt;
   paramiko->pynacl;
   paramiko->pyasn1;
   paramiko->cryptography;
   bcrypt->cffi;
   bcrypt->six;
   cryptography->idna;
   cryptography->setuptools;
   cffi->pycparser;
   scp[label="scp\n(0.10.2)", color = "red"];
   paramiko[label="paramiko\n(2.2.1)", color = "red"];
   bcrypt[label="bcrypt\n(>=3.1.3)", color = "red"];
   pynacl[label="pynacl\n(>=1.0.1)", color = "red"];
   pyasn1[label="pyasn1\n(0.1.9)"];
   cryptography[label="cryptography\n(1.5)"];
   cffi[label="cffi\n(1.7.0)"];
   six[label="six\n(1.10.0)"]
   idna[label="idna\n(2.1)"]
   setuptools[label="setuptools\n(27.2.0)"]
   pycparser[label="pycparser\n(2.14)"]
}
```

- Graphvizでpng化(Graphviz2.38\bin\dot.exe)

```
> dot.exe -Tpng paramiko_depend.dot > paramiko_depend.png
```



# 参考
[paramiko 2.2.1 : Python Package Index][2]
[scp 0.10.2 : Python Package Index][9]
[Paramiko Release][15]
[Paramikoでコマンドの送信、結果の記録を自動で行う - Qiita][1]
[SSH - Python with paramiko issue - Stack Overflow][15]
[Paramikoを使ってシェルスクリプトをリモートホストに流しこむ - Qiita][14]
[paramiko を使って Python で SFTP/SCPを行う - Librabuc][10]
[Pythonでscp - 面白コンテンツ探求日記][13]
[SSH - Python with paramiko issue - Stack Overflow][12]
[【Python】pipとwheel – Qiita][6]
[pythonパッケージの依存関係をgraphvizで可視化 – Qiita][7]
[pythonモジュールのバージョンは - Drkcore][8]
[DOT言語入門  |  You Look Too Cool][11]

[1]:http://qiita.com/ototo/items/cc1bc7b83f0722dfd5ca
[2]:https://pypi.python.org/pypi/paramiko/2.2.1
[3]:https://pypi.python.org/pypi/
[4]:https://pypi.python.org/pypi/bcrypt/3.1.3
[5]:https://pypi.python.org/pypi/PyNaCl/1.1.2
[6]:http://qiita.com/kenta1984/items/16a14f3bfaf1f257c585
[7]:http://qiita.com/SaitoTsutomu/items/895dc98148942e740312
[8]:http://blog.kzfmix.com/entry/1158760626
[9]:https://pypi.python.org/pypi/scp/0.10.2
[10]:https://librabuch.jp/blog/2014/03/python3_paramiko_sftp_scp/
[11]:http://stabucky.com/wp/archives/4191
[12]:https://stackoverflow.com/questions/14326665/ssh-python-with-paramiko-issue
[13]:http://blog.horimisli.me/entry/2013/10/06/175637
[14]:http://qiita.com/jbking/items/e9aef2fe5c2fe033e04a
[15]:https://media.readthedocs.org/pdf/paramiko-docs/1.13/paramiko-docs.pdf