fabric2のインストール手順と簡単な使い方

## 概要
fabric2のインストール手順と簡単な使い方です。
fabric1と2は別物だと考えた方がよいです。fabric2は、Pythonのinvokeライブラリのラッパーなので、invokeのドキュメントも参考になります。

## Version
- Python 3.7.1
- fabric 2.4.0

## fabric2のインストール
以下のコマンドでインストール。

```shell-session
$ pip install fabric
```

以下でもインストール可能だが、fab2コマンドになったり、`import fabric2`としなくてはいけなくなるので面倒。

```shell-session
$ pip install fabric2
```

## 基本的な使い方
fabfile.pyを作成。
以下は接続先でhostnameコマンドを実行するサンプルスクリプト。

```py:fabfile.py
#-*- coding:utf-8 -*-
import getpass
from fabric import Connection, Config
from invoke import task

@task
def sample(c):
    Config.user = 'username'
    hostname = 'example.com'

    sudo_pass = getpass.getpass("What's your sudo password?")
    config = Config(overrides={'sudo': {'password': sudo_pass}})
    c = Connection(hostname, config=config)
    c.run('hostname')
```

以下のコマンドで実行可能task一覧を確認できる。

```shell-session
$ fab -l
Available tasks:

  sample
```

sample taskを実行。

```shell-session
$ fab sample
What's your sudo password?
example.com
```

### fabコマンドではなくpythonコマンドで実行する方法
以下で実行できる。

```py:sample.py
from fabric import Connection, Config

Config.user = 'username'
hostname = 'yourhostname'
c = Connection(hostname)
c.run('hostname')
```

```shell-session
$ python sample.py
```

## sudoコマンドにパスワードを渡す方法
結論から言うと、以下の「方法3」がベスト。

### 方法１
sudoで`sudo: no tty present and no askpass program specified`と怒られる場合、runで`pty=True`を付ける。
ただし、この方法だとsudoコマンドがある度にパスワードの入力が必要になる。

- 参考
    - [fabricのsudoでtty必要って怒られたときの対処](https://tokibito.hatenablog.com/entry/20110225/1298562667)
    - [capistrano, tomahawk などから sudo ができなかった場合に確認する項目](http://akuwano.hatenablog.jp/entry/20120302/1330656001)

```py
#-*- coding:utf-8 -*-
from fabric import Connection, Config
from invoke import task

@task
def sample(c):
    Config.user = 'username'
    hostname = 'example.com'

    c = Connection(hostname)
    c.run('hostname')
    c.run('whoami')
    c.run('sudo whoami', pty=True)
```

実行

```shell-session
$ fab sample
example.com
username
[sudo] password for username:
root
```

### 方法2
c.sudoを使い、引数でpasswordを渡す。
この方法なら毎度sudoパスワードを入力しなくて済む。
環境変数でpasswordを渡すことで、ソース内にパスワードを記述することを回避できる。

```py
@task
def sample(c):
    c.sudo('whoami', password=os.environ.get('SUDO_PASSWORD'))
```

### 方法3
`--prompt-for-sudo-password`optionを使う。
このオプションを使うことで、最初にパスワードをpromptで入力させることができる。しかも、最初に1度だけ入力させればよい。`c.sudo()`へのパスワードの渡し方は、方法2と同じ。

```shell-session
$ fab --prompt-for-sudo-password sample
Desired 'sudo.password' config value:
```

`c.config.sudo.password`で入力した文字列を取得できます。

```py
@task
def sample(c):
    print(c.config.sudo.password)
    c.sudo('whoami', hide='both', echo=True, password=c.config.sudo.password)
```

ちなみに、以下のドキュメントを読んだが、入力したパスワードを取得する方法がよくわからなかったので
http://docs.pyinvoke.org/en/1.2/invoke.html

以下のinvokeのソースを追ったら分かった。
https://github.com/pyinvoke/invoke/blob/0de7020aa908d4a1afc5a7287e29196a657c292a/tests/_support/sudo_prompt.py


## fabfileを分割する
fabfileをtaskごとに分割します。
production, developmentで環境ごとに異なる設定を読み込み、接続先でwhoamiを実行するサンプルを作ってみます。

以下のディレクトリ構成でファイルを作成します。

```bash
.
└── fabfile
    ├── hosts.py # 環境ごとの設定を読むモジュール
    ├── __init__.py # モジュールをimportするファイル
    └── sample.py # mainのtask（whoamiを実行する）
```

\_\_init__.pyで、hosts, sampleをimportします。

```py:__init__.py
#-*- coding:utf-8 -*-
from fabfile.hosts import *
from fabfile.sample import *
```

production, developmentそれぞれの設定を定義します。

```py:hosts.py
#-*- coding:utf-8 -*-
from fabric import Connection, Config
from invoke import task

@task
def production(c):
    Config.environment = 'production'
    Config.hostname = 'prod.example.com'
    Config.user = 'username'

@task
def development(c):
    Config.environment = 'development'
    Config.hostname = 'dev.example.com'
    Config.user = 'username'
```

whoamiを実行するtaskを作成。

```py:sample.py
#-*- coding:utf-8 -*-
from fabric import Connection, Config
from invoke import task
import os

@task(default=True)
def check(c):
    print(Config.environment)
    print(Config.hostname)

    with Connection(Config.hostname) as c:

        '''
        結果を変数にセットすることも可能
        hide=Trueでrunコマンドの標準出力を抑制する
        '''
        result = c.run('whoami', hide=True)
        print(result.stdout.strip())

        '''
        echo=Trueで実行コマンドも出力する
        ただし、hide=Trueだと出力されなくなるので、hide='both'とする
        '''
        result = c.run('whoami', hide='both', echo=True)
        print(result.stdout.strip())

        '''
        sudoで実行
        '''
        result = c.sudo('whoami', hide='both', password=os.environ.get('SUDO_PASSWORD'))
        print(result.stdout.strip())

        '''
        warn=Trueで、コマンドでエラーが発生しても中断しないようにできる
        以下のように自分でエラー処理を定義できる
        '''
        result = c.run('abcdef', hide=True, warn=True)
        if result.ok:
            print(result.stdout.strip())
        else:
            print("ERROR: {}".format(result.stderr.strip()))
```

実行可能コマンド一覧を確認。

```shell-session
$ export SUDO_PASSWORD=mypassword
$ fab --list
Available tasks:

  check
  production
  development

Default task: check
```

実行

```shell-session
$ fab development check
development
dev.example.com
username
whoami
username
root
ERROR: bash: abcdef: command not found
```

## fabコマンドに任意のoptionを付与する方法
`foo`というoptionを付与する場合

```py:fabfile/__init__.py
#-*- coding:utf-8 -*-
from fabfile.sample import *
```

```py:fabfile/sample.py
#-*- coding:utf-8 -*-
from fabric import Connection, Config
from invoke import task

@task(optional=['foo'])
def foobar(c, foo=''):
    print(foo)
```

実行

```shell-session
$ fab foobar --foo=bar
bar
```

## コマンドを連続で実行する場合
`cd /tmp && pwd`のようなことする場合。

以下のやり方では、それぞれ独立して動いてしまうため、`pwd`しても`/tmp`とはならない。

```py
@task
def sample(c):
    with Connection('hostname') as c:
        c.run('cd /tmp'):
        c.run('pwd')
```


### 方法1: `cd()`を使って事前にディレクトリを移動する方法

```py
@task
def sample(c):
    with Connection('hostname') as c:
        with c.cd('cd /tmp'):
            c.run('pwd')
```

### 方法2: `prefix()`で事前にコマンドを実行する方法
こちらであれば、`cd`以外のコマンドを実行したい場合でも使える。

```py
@task
def sample(c):
    with Connection('hostname') as c:
        with c.prefix('cd /tmp'):
            c.run('pwd')
```

## ssh秘密鍵のパスを指定する方法
http://docs.fabfile.org/en/2.5/api/connection.html

```py
#-*- coding:utf-8 -*-
from fabric import Connection, Config
from invoke import task

@task
def sample(c):
    c = Connection(
        host='example.com',
        user='username',
        connect_kwargs={
            "key_filename": "/home/username/.ssh/private.key",
        },
    )
    c.run('hostname')
    c.run('whoami')
    c.run('sudo whoami', pty=True)
```

## 参考
- [Getting started](http://docs.fabfile.org/en/latest/getting-started.html)
- [2日間Fabricを触った俺が  色々解説してみる](https://www.slideshare.net/ryojiishii14/fabric-46215238)
- [fab オプションと引数](https://fabric-ja.readthedocs.io/ja/latest/usage/fab.html)
- [Fabric2 へ移行した際の雑Tips](https://kumak1.hatenablog.com/entry/2018/05/18/181347)
- [Pythonタスクランナー Fabric 2 の紹介 - 前編 Fabricの概要](https://heartbeats.jp/hbblog/2018/11/fabric2-01.html)
- [Python製デプロイツール Fabricを初めて使う際に役立つTips](http://dekokun.github.io/posts/2013-04-07.html)