こんにちは、滝澤です。
いくつかのプロジェクトでタスクランナーFabric 2を使う機会がありました。少しですが知見が溜まったので紹介します。 また、Fabric 1.xを利用していた方は互換性も気になると思いますでのその点についても紹介します。
記事が長くなったので3編に分けます。
- 前編: Fabricの概要
- 中編: Invokeの使い方
- 後編: Fabricの使い方
本記事は中編の「Invokeの使い方」になります。
なお、執筆時点(2018年11月21日)での最新バージョンはFabric 2.4.0、Invoke 1.2.0です。 動作確認はPython 3.7.1にて行っています。
Invokeについて
Invokeはローカルでシェルコマンドを実行するPythonライブラリです。 タスクランナーとして利用できます。
前編でも述べましたが、Fabric 2はタスクランナーInvokeのSSHラッパーとして動作します。 タスクの定義方法はInvokeとほぼ同じですので、まずInvokeの使い方を確認してみましょう。
Invokeのインストール
pipコマンドでインストールできます。
$ pip install invoke
タスクランナーInvoke
タスク実行
タスクの定義方法を紹介します。
例として、与えられたシェルコマンドを単純に実行するタスクsh
を定義してみます。
まず、tasks.py
という名前のファイルを作成します。
invokeパッケージからtaskモジュールをインポートします。
from invoke import task
タスク名sh
の関数を定義し、1つ目の引数にはInvokeのコンテキストの変数としてc
を指定し、2つ目以降の引数にはinvokeコマンドで渡される引数を指定します。さらに、タスクとして認識させるためにデコレーター@task
を付けます。
run()
の引数にシェルで実行するコマンドを指定します。
@task def sh(c, command): """Execute a shell command.""" c.run(command)
tasks.py
ファイルと同じディレクトリにおいて、invokeコマンドを実行します。invokeコマンドの基本的な使い方は次の通りです。
invoke タスク名 [引数...]
ここでタスク名sh
と引数のdate
を指定してinvokeコマンドを実行してみます。
$ invoke sh 'date' Tue Nov 20 17:30:22 JST 2018
dateコマンドの実行結果が出力されます。
ここでもう一つ関数名の長いタスクを定義してみます。
@task def print_date(c): """Print date.""" c.run("date")
invokeコマンドで--list
オプションを付けて実行するとタスク一覧が表示されます。
$ invoke --list Available tasks: print-date Print date. sh Execute a shell command.
ここで定義したタスクの関数名print_date
とタスク名print-date
が異なることがわかります。
"_
"が"-
"に置き換わっています。
同じ名前に戻したい場合は設定ファイルを用意します。
設定ファイルの配置方法や記述方法にはいくつかあるのですが、ここでは同じ場所にinvoke.yaml
という名前のファイルを用意し、次のようにtasks.auto_dash_names
の値をfalse
にします。
--- tasks: auto_dash_names: false
再び実行すると、タスク名がprint_date
になり、定義したタスクの関数名と一致するようになりました。
$ invoke --list Available tasks: print_date Print date. sh Execute a shell command.
公式ドキュメント:
コマンド実行結果Resultの利用
コマンドの実行結果の出力をprint()
関数で出力してみます。
run()
自体での出力を抑制するためにhide=True
あるいはhide="both"
オプションを追加します。
run()
の実行結果をresult
に格納し、標準出力をresult.stdout
から取得します。なお、出力には改行コードも含まれるため、strip()
で取り除いています。
@task def sh(c, command): """Execute a shell command.""" result = c.run(command, hide=True) print(result.stdout.strip())
再びinvokeコマンドを実行してみます。
$ invoke sh 'date' Tue Nov 20 17:48:55 JST 2018
print()
関数で実行結果を出力できました。
なお、実行コマンドそのものも出力したい場合はecho=True
オプションを追加するか、invokeコマンドのオプションとして--echo
を追加するかしてください。ただし、hide=True
オプションはこの設定を上書きするため、hide="both"
などに書き換える必要があります。
$ invoke --echo sh 'date' date Tue Nov 20 17:48:55 JST 2018
公式ドキュメント:
エラーへの対応
エラーが発生したときの動きを見てみましょう。
引数にdate
と入力すべきところを誤ってdete
と入力してしまいました。
$ invoke sh 'dete' Encountered a bad command exit code! Command: 'dete' Exit code: 127 Stdout: Stderr: /bin/bash: dete: command not found
コマンドの終了コードが0ではないため、エラーが発生してタスクが中断されました。
ここで、エラー処理を追加してみます。
エラー発生時に中断しないようにrun()
にwarn=True
オプションを追加します。
終了コードの判断には終了コードが0のときに真になるresult.ok
を利用します。ちなみに、result.failed
はこの逆です。
標準エラーはresult.stderr
で取得できます。
@task def sh(c, command): """Execute a shell command.""" result = c.run(command, hide=True, warn=True) if result.ok: print(result.stdout.strip()) else: print("ERROR: {}".format(result.stderr.strip()))
再び実行すると次のようになり、エラーを処理することができました。
$ invoke sh 'dete' ERROR: /bin/bash: dete: command not found
公式ドキュメント:
複数のコマンドの実行
複数のコマンドの実行について確認してみます。
次のタスクはディレクトリを移動してコマンドを実行することを期待して定義したタスクです。
@task def cd(c): """Execute cd().""" c.run("cd /opt/project") c.run("pwd")
これを実行すると次のような出力が得られます。
$ invoke cd /home/foo/fabric
/opt/project
ディレクトリに移動してからコマンドを実行したかったのですが、期待したとおりに動いていないことがわかります。
これは、それぞれのrun()
が独立して実行され、状態を維持していないためです。
期待した動作を行わせるためには次のように実行するコマンドを"&&
"で結合します。
@task def cd(c): """Execute cd().""" c.run("cd /opt/project && pwd")
cd()
コマンド実行前にディレクトリを移動したいだけの場合は、コンテキスト内でディレクトリを変更するcd()
を使うことで実行できます。
@task def cd(c): """Execute cd().""" with c.cd("/opt/project"): c.run("pwd")
これを実行すると次のようになります。
$ invoke cd /opt/project
実際にどのようなコマンドが実行されているかを確認するために、--echo
オプションを付けて実行してみます。
$ invoke --echo cd cd /opt/project && pwd /opt/project
"cd /opt/project &&
"が実行コマンドのプレフィックスとして付与されていることがわかります。
似たようなものにprefix()
があります。これは次のように記述して、run()
を実行する前にprefix()
で指定したコマンドを実行するものです。
@task def cd(c): """Execute cd().""" with c.prefix("cd /opt/project"): c.run("pwd")
実行してみると、次のようにコマンドが"&&
"で結合されていることがわかります。
$ invoke --echo cd cd /opt/project && pwd /opt/project
公式ドキュメント:
sudo()
タスクの処理の中でsudoコマンド経由でコマンドを実行したいときがあります。
この例として、whoami
コマンドを実行するタスクを用意してみました。
sudoコマンド経由でコマンドを実行するためにはrun()
の代わりにsudo()
を利用します。
@task def whoami(c): """Execute whoami.""" result = c.run("whoami", hide="both") print("run: {}".format(result.stdout.strip())) result = c.sudo("whoami", hide="both") print("sudo: {}".format(result.stdout.strip()))
sudoのパスワードを入力するために--prompt-for-sudo-password
オプションを指定してinvokeコマンドを実行します。パスワードなしにsudoが実行できる場合はこのオプションは不要です。
$ invoke --prompt-for-sudo-password whoami Desired 'sudo.password' config value: run: foo sudo: root
また、設定ファイル内にパラメータsudo.password
とそのパスワードを設定することによりパスワードの入力を省略することもできます。
--- sudo: password: 'wveF}bWNYp4Wsu6m'
--echo
オプションを付けて実行すると次のようになります。
$ invoke --echo whoami whoami run: foo sudo -S -p '[sudo] password: ' whoami sudo: root
sudoコマンドが実行されていることがわかります。
sudo()
で指定したコマンドの前に単に"sudo -S -p '[sudo] password: '
"が付与されているだけであることをよく覚えておいてください。ここが嵌まりどころになります。
公式ドキュメント:
sudo()による複数コマンドの実行
一つのsudo()
で複数のコマンドを実行することを考えてみましょう。
この例ではwhoami
コマンドを2回実行します。出力としてはroot
が2回表示されることを期待しています。
@task def whoami(c): """Execute whoami.""" result = c.sudo("whoami && whoami", hide="both") print(result.stdout.strip())
実行されたコマンドを確認するために--echo
オプションを付けて実行してみます。
$ invoke --echo whoami sudo -S -p '[sudo] password: ' whoami && whoami root foo
1つ目は"root
"で2つ目は実行時のユーザー"foo
"が出力され、期待した通りには動作していないことがわかります。
ここで、実行されたコマンドに着目すると"sudo -S -p '[sudo] password: ' whoami
"というコマンドと"whoami
"というコマンドが実行されたことがわかります。
この動作は先に紹介したcd()
とprefix()
にも当てはまり、sudo()
は期待した通りには実行されません。
期待した動作を行わせるためには、実行したいコマンドにbash -c
を付与してsudo()
に渡すとよいでしょう。
@task def whoami(c): """Execute whoami.""" result = c.sudo('bash -c "whoami && whoami"', hide="both") print(result.stdout.strip())
実行してみると、期待した動作になります。
$ invoke --echo whoami sudo -S -p '[sudo] password: ' bash -c "whoami && whoami" root root
また、sudo()
内でリダイレクトでファイル入出力を行う場合は、ファイルのオープンそのものは実行時のユーザーで行われるため、ファイルやディレクトリの権限によりアクセスできないことがあります。
このようなことを防ぐために、同様にbash -c
を付与するとよいでしょう。
@task def whoami(c): """Execute whoami.""" c.sudo('bash -c "whoami > /opt/project/data/w.dat"', hide="both")
$ invoke --echo whoami sudo -S -p '[sudo] password: ' bash -c "whoami > /opt/project/data/w.dat"
以上でInvokeの基本的な使い方を紹介しました。
公式ドキュメント:
目次
- 前編: Fabricの概要
- 中編: Invokeの使い方
- 後編: Fabricの使い方 ←次の記事