CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: Fabric を組み込みで使うときの注意点

以前、このブログで Fabric をスクリプトに組み込んで使う方法について書いた。

blog.amedama.jp

ただ、このやり方はちょっとした注意点があるので追記しておく。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1108
$ python --version
Python 3.5.2

インストール

何はともあれ、まずは Fabric をインストールしておく。 Python 3 に対応したものは fabric3 という名前でインストールできる。

$ pip install fabric3

Python 2 なら数字なし。

$ pip install fabric

下準備

前回のエントリと同じように Fabric で操作する対象は Vagrant で作った仮想マシンにする。 そのため、まずは Homebrew Cask を使って Vagrant と VirtualBox をインストールしておく。

$ xcode-select --install
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew tap caskroom/cask
$ brew cask install vagrant virtualbox

次に、適当な作業ディレクトリに Vagrantfile を用意する。 これが操作する対象の仮想マシンの設定ファイルになる。

$ cat << 'EOF' > Vagrantfile
# -*- mode: ruby -*-
Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-16.04"
  config.vm.provider "virtualbox" do |vb|
    vb.cpus = "2"
    vb.memory = "1024"
  end
end
EOF

用意ができたら、仮想マシンを起動しよう。

$ vagrant up

ssh-config サブコマンドで OpenSSH の設定を確認しておこう。 主に使うポート番号をチェックする。 今回は 2200 がアサインされた。 つまり、ポートフォワーディング経由で 127.0.0.1:2200 に SSH すると仮想マシンに接続できる。

$ vagrant ssh-config
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2200
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/amedama/Documents/vagrant/fabric/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

リモートホストでコマンドを実行する

以前のエントリと同じように Fabric を組み込んだスクリプトを用意する。 ここでは exit コマンドに引数 0 を指定して実行している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute
from fabric.api import env

def task():
    run('exit 0')


def main():
    env.user = 'vagrant'
    env.password = 'vagrant'

    execute(task, hosts=['vagrant@127.0.0.1:2200'])
    print('Done!')


if __name__ == '__main__':
    main()

Fabric のタスク実行が終わった後に Done! を出力しているところがポイント。

上記を fabfile.py というファイルで保存して、実行してみよう。

$ python fabfile.py
[vagrant@127.0.0.1:2200] Executing task 'task'
[vagrant@127.0.0.1:2200] run: exit 0
Done!

ちゃんと Done! の出力までされている。

問題があるパターン

次は同じソースコードでも、実行する exit コマンドの引数に 1 を渡してみよう。 こうすると exit コマンドの返り値が 1 になる。 返り値が非ゼロということは、エラー扱いになるということだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute
from fabric.api import env

def task():
    run('exit 1')


def main():
    env.user = 'vagrant'
    env.password = 'vagrant'

    execute(task, hosts=['vagrant@127.0.0.1:2200'])
    print('Done!')


if __name__ == '__main__':
    main()

さて、それでは上記を実行してみよう。

$ python fabfile.py
[vagrant@127.0.0.1:2200] Executing task 'task'
[vagrant@127.0.0.1:2200] run: exit 1

Fatal error: run() received nonzero return code 1 while executing!

Requested: exit 1
Executed: /bin/bash -l -c "exit 1"

Aborting.

なんと、今度は Done! の出力が見当たらない。

何が起こったのか?

実は Fabric の run() や sudo() といった関数は、実行したコマンドの返り値が非ゼロのときにプロセスを終了してしまう。 そのため、さきほどのサンプルコードでは非ゼロが帰った時点で Python のプロセスが終了してしまった。 これによって Done! を出力する行まで到達しなかったわけだ。

対処方法

この問題を回避するには、コマンドを実行する関数の warn_only という引数に True を指定すれば良い。

今度は、先ほどと同じソースコードで run() 関数の引数に warn_only=True を指定してみよう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute
from fabric.api import env

def task():
    run('exit 1', warn_only=True)


def main():
    env.user = 'vagrant'
    env.password = 'vagrant'

    execute(task, hosts=['vagrant@127.0.0.1:2200'])
    print('Done!')


if __name__ == '__main__':
    main()

上記を実行してみよう。 今度は返り値が非ゼロであることについて警告は出るものの、そのまま処理が継続している。 そのため Done! が出力されていることも分かる。

$ python fabfile.py
[vagrant@127.0.0.1:2200] Executing task 'task'
[vagrant@127.0.0.1:2200] run: exit 1

Warning: run() received nonzero return code 1 while executing 'exit 1'!

Done!

より細かいエラー処理

先ほどの例では、エラーがあっても突っ走ってしまうことを意味している。 このままだと、むしろ扱いにくいことになるだろう。 そこで、次はコマンドの実行結果が成功か失敗かを判断できるようにしよう。

コマンドの実行結果が成功か失敗か判断するには、コマンドを実行する関数の返り値を受け取るようにすれば良い。 次のようにして返り値の succeeded アトリビュートを確認する。 今回のサンプルコードではコマンドの実行が失敗したときには RuntimeError 例外を上げるようにしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute
from fabric.api import env

def task():
    res = run('exit 1', warn_only=True, quiet=True)
    if not res.succeeded:
        raise RuntimeError('Oops!')


def main():
    env.user = 'vagrant'
    env.password = 'vagrant'

    try:
        execute(task, hosts=['vagrant@127.0.0.1:2200'])
    except RuntimeError:
        # ほんとはこんなエラーメッセージだしちゃダメダヨ
        print('Something wrong!')
    print('Done!')


if __name__ == '__main__':
    main()

では、上記を実行してみよう。 今度はエラーをハンドリングしつつも Done! の表示が得られている。

$ python fabfile.py
[vagrant@127.0.0.1:2200] Executing task 'task'
Something wrong!
Done!

かんぺきだね。 めでたしめでたし。