fabric2いいかも

最近になって「fabricで作業の自動化を増やしたい」と思うことがありました。 調べてみたらPython3対応されたfabricはfabric2として独立していました。そしてfabric2を待ちきれずにフォークされたfabric3はメンテされている様子はありません。 正解はfabric2です。

fabric2はfabricと違いinvokeのssh拡張がついたラッパーなようです。 使うにあたりConfig, Connection, Groupの違いを確認したりしました。 また気楽なタスクランナーとして使うためにヘルパーやパターンが出てきたのでメモします。

実験した残骸はここに残してます。

pkg version
fabric2 2.6.0
Python 3.8.5

基本的なこと

したのように今まで通り @task デコレーターでタスクを登録します。引数は invoke.config.Config です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import core

@task
def check_hostname_and(c):
    c.run("hostname") # use c as Runner
    core.check_hostname(c) # pass c as Runner to func.
    uname_a(c) # pass c as Config to task

@task
def uname_a(c):
    core.uname_a(c)

タスク名は関数名の_-に置き換えたものみたいです。

1
2
3
4
5
$ fab2 --list
Available tasks:

  check-hostname-and
  uname-a

invoke.config.Configinvoke.runners.Runner を実装しているため c.run(“hostname”) のように外部コマンドを呼び出せます。 この Config は関数なのでさっきの例のように他のタスクから呼び出せます。引数は invoke.config.Config です。 fabric2で実行ホストは次の2つの場所で指定できます。 1つはPythonスクリプトの中です。もう1つはコマンド呼び出し時の -H フラグです。

fabric.Connection.ConnectionConfig であり Runner です。 そのためさっきの例のように他のタスクを呼び出すときに渡したり、 run メソッドで特定ホストでコマンド実行できます。

フラグについてです。 fab2 コマンドの -H オプションでホスト名が渡されない場合は invoke.config.Config です。 そのためローカルホストで直接実行されます。 -H でホスト名を , 区切りで与えると全てのタスクに各ホスト名に対応した fabric.connection.Connection が渡されます。

ちなみに -H を使って複数ホストを指定した fab2 -H host1,host2 other_task_1 other_task_2 の動きは下のようなタスクの動きと一致します。

1
2
3
4
5
6
def example(c):
    sg = SerialGroup("host1", "host2")
    for s in sg:
        other_task_1(s)
    for s in sg:
        other_task(s_2)

この Connectioninvoke.runners.Runner なので run() が使えますが他にも forward_localforward_remote という便利メソッドを提供しています。

fabric を使う動機として複数ホストでのコマンド実行があります。そのために fabric.group.Group という Runner があります。 これには SerialGroup, Threading.Group が準備されています。つぎのように使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@task

from fabric2 import ThreadingGroup

def use_group(c):
    tg = ThreadingGroup("example00.local",
                      "example01.local")
    tg.run("ip r")   # use tg as Runner
    core.uname_a(tg) # pass tg as Runner
    # uname_a(tg) # ERROR: tg isn't Config

fabric.group.Group を使うことで柔軟な作業フローを設計できます。 注意するところとしては fabric.group.Groupfaric.connection.Connection のコレクションであって invoke.config.Config を継承していないところです。そのためタスクに渡すことはできません。 invoke.runners.Runner ではあるのでさっきの例の tg.run("ip r") のように使うことはできます。 どうすると管理しやすいか悩んでいます。とりあえず Runner を渡す関数と呼び出すタスクを分離することにしました。 ほかにはfabric.transfer.Transferというファイル転送に使えるクラスがいます。

タスクランナーで補足的なこと

  • 作業記録を取る
    • teeパッケージを使う teeパッケージのPython3対応
    • 複数のコンテキストマネージャをまとめるヘルパーを準備する
  • Serverspecの出力を色付きにする
  • 手動確認を入れる

作業記録を取る

with 使って標準出力をログに残しておく。Python3ではバッファリング0はできないので buff=1 を与えておく。

1
2
3
with StdoutTee("./log/stdout.log", buff=1):
    tg = ThreadingGroup('test-lte', 'test-enb')
    core.check_hostname(tg)

StdoutTee, StderrTee と2つのコンテキストマネージャを同時に使うのでヘルパークラスを書くと楽です。

1
2
3
4
5
6
7
8
9
class MultipleContext():
    def __init__(self, *args):
        self.opts = args
    def __enter__(self):
        for opt in self.opts:
            opt.__enter__()
    def __exit__(self, *args):
        for opt in self.opts:
            opt.__exit__(args)

Serverspecの出力を色付きにする

Serverspecなどのコマンドツールはptyを有効にしないとカラー出力してくれません。 pty=True フラグを使うことで解決できます。しかし tee.Tee を使っているとランタイムエラーになります。 なので、その場合は with の外側で実行する。

1
2
3
result = run("cd ../serverspec && bundle exec rake spec:test-lte", pty=True)
with log_tees():
    print(result.stdout)

手動確認を入れる

次のようなヘルパー関数を準備すると楽できそうです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# coding: utf-8

import re

__yes = re.compile('[y](es?)?', flags=re.IGNORECASE)

def pass_checkpoint(msg):
    choice = input("{} [y/N]: ".format(msg))
    print(choice)
    if __yes.fullmatch(choice):
        return True
    return False

おしまい

Goユーザーだしmageどうかなと少し調べました。 mageはRakeのような開発を支援するローカルで使われることを意図していてFabricの代わりには向かなそうでした。 また外部コマンドをたくさん呼ぶタスクランナーはシングルバイナリにして安定するということも少ないので無理して選ぶ必要は感じませんでした。

最近、会社の人たちにここが補足され公式読めよくらいのメモだったりを書くの迷ったりしましたがサブタイトル通り書いて覚える整理するが目的なので気にしないことにしました。

comments powered by Disqus