App::RadというCLIがだいぶ良い

App::Rad というコマンドラインツールがあるのですが、
手軽にサブコマンド作れてだいぶウマーな感じなのに
日本語情報が全くないので紹介してみます。



基本系

まずは「rad.pl」に「bucho」というサブコマンドを作ってみたいと思います。

use App::Rad;
App::Rad->run;

sub bucho {
     return "Hello Bucho!";
}

たったこれだけです。まずは実行してみましょう。

$ perl rad.pl
Usage: rad.pl command [arguments]

Available Commands:
    bucho    
    help      show syntax and available commands

いい感じにヘルプが出ました。


定義したサブルーチン「bucho」が勝手にサブコマンドとして認識されていますね。
buchoコマンドを実行してみましょう。

$ perl rad.pl bucho
Hello Bucho!

実行されました。簡単!

help

buchoコマンドもヘルプに出したいですよね。
こんな感じで書いてみましょう。

use App::Rad;
App::Rad->run;

sub bucho :Help(give a nice compliment) {
     return "Hello Bucho!";
}

変な構文ですね。ヘルプを実行してみましょう。

$ perl rad.pl
Usage: rad.pl command [arguments]
Available Commands:
    bucho     give a nice compliment
    help      show syntax and available commands

ヘルプがついた!

setup

setupコマンドはスクリプトの初期化処理のようなものです。
いくつか例を挙げてみましょう。
以下は先ほどのbuchoコマンドを定義したものです。

sub setup {
    my $c = shift;
    $c->register_commands( {
        bucho => 'give a nice compliment!',
    });
}

もちろん、ここに書いておけば:Helpみたいな変な構文もいりません。


また、サブルーチン全てがコマンドとして登録したくないこともあるでしょう。
その時にはこのように明示的に指定するのが良いでしょう。


逆に、コマンドとして認識させない、という設定もあります。
以下は「_」で始まるサブルーチンをコマンドとして登録させません。

sub setup {
    my $c = shift;
    $c->register_commands( { -ignore_prefix => '_' } );
}


ただし、コマンド管理については
最初に紹介した「使うコマンドを登録する」方式がオススメです。

なぜならuseしたときにExportされるサブルーチンも自動で登録されてしまうため、
除外方式だとカバーしきれなくなってくるからです。*1


ちなみにコマンドを削除する「unregister_command 」
というサブルーチンもあったりします。


引数/バリデーション

引数を扱うにはoptionsを使います*2

$ perl rad.pl create --id=aaa
...
sub create {
    my $c = shift;
    print $c->options->{id}, "\n";  # aaa
}


バリデーションを行いたい場合、getoptを使う方法と
自前でチェックする方法が考えられます。
お好みでどうぞ。

# getopt
sub create {
    my $c = shift;
    $c->getopt( 'id=s', 'version=i' )
            or $c->execute('usage') or return undef;
}

# Data::Validator
sub create {
    my $c = shift;
    my $args = eval {
        state $rule = Data::Validator->new(
            account_id => { isa => 'Str'},
        );
        $rule->validate($c->options);
    };
    if($@) {
        $c->execute('usage') or return undef;
    }
}

共有データ

stashを使うと変数を共有できます。

use Redis::hiredis;

sub setup {
    my $c = shift;
    my $redis = Redis::hiredis->new();
    $redis->connect('127.0.0.1', 6379);
    # stashに入れる
    $c->stash->{redis} = $redis;
}

sub create {
    my $c = shift;
    …
    # stashから取り出す
    $c->stash->{redis}->command('set foo bar');
}

他の便利メソッド

pre_processやpost_processもとても便利ですが
デフォルトの動作が変わってしまう可能性があるので
気をつけて使う必要があるでしょう。*3

example

DynamoDBを操作する簡易CRUDコマンドを作ってみます。

# 例1:createサブコマンド実行
perl dynamodb_crud.pl create --id=1 --name=tori


簡易デプロイツールのテンプレのようなもの。

# 例1:deployサブコマンドをdevelopmentモードで実行
perl deploy.pl deploy --env=development

その他注意点

デフォルトではサブコマンドの戻り値が標準出力されるため、*4
何も出力したくない場合はいちいち
return undef;
しないといけません。

最後に

今までコマンドラインツールといえばGetopt::Longでしたが、
簡単なスクリプトならお手軽にかけますし
Getopt::Longの代わりとしてもアリなのではないでしょうか。
defaultメソッドに処理を書けばサブコマンドなしでも動きますし。*5

use App::Rad;
App::Rad->run();

sub default { rand(10) }

紹介しきれなかった機能もたくさんあるので
ドキュメントをご参照ください。
http://search.cpan.org/~garu/App-Rad-1.04/lib/App/Rad.pm

*1:Data::DumperのDumperコマンドなど

*2:これだけではありません。詳しくはヘルプを

*3:デフォルトの動作はドキュメントに載っているコードです

*4:post_processをオーバーライドすれば変えれます

*5:これは、defaultの本来の使い方ではないような気もしますが…

App::Monitor::Simpleというモジュール書いた

https://github.com/toritori0318/p5-App-Monitor-Simple


シンプルに「リトライ数だけ指定して全部エラーなら何かする」
ということがしたくて探してみたけど見つからず。
コードボリュームからしてもわざわざモジュールにする程でもないかもしれませんが、
あったらあったでまあまあ嬉しいかも、と思い書いてみました。

仕様

やることは至極単純で
「コマンド実行した戻り値が0 or 0以外でOKかNGを判定するだけ」
です。
オプションとしてリトライ数とリトライ間隔を指定することが出来ます。

コマンドラインツール

monitor-simpleというのがついてきます。
詳しくは--help参照ですが、例を説明してみます。

httpサーバの監視
monitor-simple 'curl http://hogehoge.co.jp/'

curl が正常終了するかどうかを判定しています。

pingの監視とオプション
monitor-simple -r 5 -i 10 -q 'ping -c 1 fugafuga.co.jp'

ping のエラー判定を行なっています。
オプションでリトライ回数5、インターバル10秒に設定しています。

応用編

monitor-simple 'curl http://hogehoge.co.jp/' && '正常時の処理'
monitor-simple 'curl http://hogehoge.co.jp/' || 'エラー時の処理'

このようにするとcronなどで使えるのではないでしょうか。
またコマンドの部分をシェルやLLで書いてももちろんOKで、
むしろそういった応用を利かす使い方が多いかもしれません。

Perlモジュールとして使う

もちろんPerlからも使えます。
以下はSYNOPSISのコピペ。なんとなくわかるはず。

my $ret = run(
    {
        command => 'ping -c 1 blahhhhhhhhhhhhhhhh.jp',
        interval => 10,
        retry => 5,
        quiet => 1,
    }
);

example

いくつか例を書いてみます。*1

複数サーバのステータスを独自のデータストアに保存したい。

例えばですが、
「HTTP監視をした結果、それが成功しているサーバのリストを保持しておきたい」
という要望があったとしましょう。
以下のようなコードをcronで回すとよいです。

use strict;
use warnings;
use Data::Dumper;
use Parallel::ForkManager;
use Redis;
use App::Monitor::Simple qw/run/;

my $redis = Redis->new(server => 'localhost:6379');

my @hosts = ('xxx.xxx.xxx.xxx', 'yyy.yyy.yyy.yyy');

my $workers = scalar @hosts;
$workers = 10 if $workers > 10;

my $pm = Parallel::ForkManager->new($workers);
do {
    print "parallel: $workers process\n";
    $pm->run_on_start(
        sub {
            my ($pid, $ident) = @_;
            printf "[%s] start\n", $ident;
        }
    );
    $pm->run_on_finish(
        sub {
            my ($pid, $exit, $ident) = @_;
            printf "[%s] completes.\n", $ident;
        }
    );
};

# run
foreach my $host (@hosts) {
    $pm->start($host) and next;

    my $cmd = sprintf('curl --max-time 3 http://%s/', $host);

    my $status = run(
        {
            command     => $cmd,
            retry       => 3,
            interval    => 5,
            quiet       => 1,
        }
    );
    if($status == 0) {
        warn "success!";
        $redis->set($host, 1);
    } else {
        warn "error!";
        $redis->set($host, 0);
    }
    $pm->finish;
}

# wait...
$pm->wait_all_children;
Redisをフェイルオーバーしたい

Redisでは自動フェイルオーバーの機能は提供されていない…ですよね?
AWS上にマスター/スレーブ構成のRedisがあるとします。
マスターにはElasticIPがついていて、
WebアプリケーションからはそのIPを参照しています。


そのような構成になっていると仮定し、
App::Monitor::Simpleで定期的に死活監視しつつ
マスターが落ちたらスレーブを昇格させて
ElasticIPを自動的に付け替える、ということを実現してみたいと思います。
AWS デザインパターンのこれですね)
http://aws.clouddesignpattern.org/index.php/CDP:Floating_IP%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

use strict;
use warnings;
use Clone qw(clone);
use App::Monitor::Simple qw/run/;
use VM::EC2;

my $ec2 = VM::EC2->new(-access_key=>$id,-secret_key=>$key,-endpoint=>$url);

# マスターサーバ情報
my $master = {
    host => 'xxx.xxx.xxx.xxx',
    instance_id => 'aaaaaaaaaaaaa',
    is_master => 1,
};
# スレーブサーバ情報
my $slave = {
    host => 'yyy.yyy.yyy.yyy',
    instance_id => 'bbbbbbbbbbbbb',
    is_master => 0,
};

my $elastic_id='zzz.zzz.zzz.zzz';

for my $server ($master, $slave) {
    my $host = $server->{host};

    warn "[$host] start!";

    # redisの死活監視
    my $ret = run(
        {
            command     => "redis-cli -h $host -p 6379 dbsize",
            interval    => 5,
            retry       => 5,
            quiet       => 1,
        }
    );
    if($ret == 0) {
        warn "[$host] ok!";
    } else {
        warn "[$host] ng!";

        # マスターが落ちている場合
        if($server->{is_master}) {
            # スレーブをマスターに昇格
            my $change_host = $slave->{host};
            system("redis-cli -h $change_host -p 6379 slaveof no one");

            # Elastic IP 付け替え
            my @addr = $ec2->describe_addresses($elastic_ip);
            my $result = $addr[0]->associate_address($elastic_addr => $slave->{instance_id});

            # 情報swap
            my $tmp = clone($master);
            $master = clone($slave);
            $slave  = clone($tmp);
            $master->{is_master} = 1;
            $slave->{is_master} = 0;
        }

        # 自動でスレーブ作り直し?
        # ...
        # もしくは 通知して exit?
        return;
    }
}

Todo

  • タイムアウト処理もこのモジュールで出来たほうがいいのかなー
  • Warningも別判定にしたほうがいいのかなー

最後に

もちろんNagiosなどツールを使えば同じことが出来ますが、
シンプルな監視をしたいときには有用かと思うのですがいかがでしょうか。

*1:全てフィクションです!!!!

お遊びワンライナー

なんていうんでしたっけ。反射神経試す奴。
ワンライナーにしてみました*1


以下のコマンド叩いて「!」が出たらすばやく「Ctrl+C」を実行しましょう。

perl -MTime::HiRes -E '*z=*Time::HiRes::gettimeofday;$SIG{INT}=sub{if($f){$e=[z()];printf "\n %.4f\n",Time::HiRes::tv_interval($s,$e);exit;}else{say "NG!";exit;}};say "wait";sleep int(rand(15));$s=[z()];$f=1;say "!";while(1){}'

仕事にお疲れのあなた!
息抜きに遊んでみてください(・0・)

*1:perl力が足りないため長くなってしまった…

Amon2でエラーHTMLをカスタマイズしたい

Amon2のstatic以下にエラーページ用のHTML置いてあるけど
デフォルトでは見ていないようでどうすれば見てくれるのかなーと考えてたんだけど
Middlewareレベルでやらせればいいのかな。
app.psgi のbuilder にこんなの追加しておく。

      enable "Plack::Middleware::ErrorDocument",
          404 => "static/404.html",
          500 => "static/500.html",
          502 => "static/502.html",
          503 => "static/503.html",
          504 => "static/504.html",
          ;
      enable "Plack::Middleware::HTTPExceptions";


apacheとかnginxと共通にしたければ
そっちのディレクトリを指定すればよろし。

Amon2でFacebookAPIを使う その2

前回Facebook::Graphを使ってFacebookにアクセスできました。
しかし実はAmon2にはFacebookAPIにコネクトするプラグインも存在します。
プラグインを使うとFacebook::Graphなしでも簡単にトークンを取得することができます。
試してみましょう。


とりあえず再度新しくアプリケーションを作っておきましょう。

amon2-setup.pl FacebookAPIPlugin
cd FacebookAPIPlugin

プラグインの設定

FacebookAPIPlugin::Web.pmにプラグインの設定とコールバックを書いておきます。

__PACKAGE__->load_plugin(
    'Web::Auth',
    {
        module => 'Facebook',
        on_finished => sub {
            my ($c, $token, $user) = @_;
            my $name = $user->{name} || die;
            $c->session->set('name' => $name);
            $c->session->set('site' => 'facebook');
            $c->session->set('token' => $token);
            return $c->redirect('/');
        },
        on_error => sub {
            my ( $c, $error ) = @_;
            warn ("auth_error!![$error]");
            return $c->redirect('/');
        },
    }
);

プラグインの設定内容はconf/development.pl に追記しておきます。

    Auth => {
        Facebook => {
            client_id       => 'cccccccccccccc',            # アプリケーションID
            client_secret => 'dddddddddddddddd',   # アプリケーション秘密鍵
            scope           => 'read_stream',                # 権限
        }
    }


これだけでOK。
あとは /auth/facebook/authenticate にアクセスさせれば
そのまま認証プロセスに入り、上記で設定したコールバックで
アクセストークンを取得できます。

コントローラ

生でアクセスするのであれば以下のように書けばよいでしょう。
FacebookAPIPlugin::Web::Dispatcher

any '/' => sub {
    my ($c) = @_;
    my $data;
    my $token = $c->session->get('token');
    if ( $token ) { # loggedin
        my $ua = LWP::UserAgent->new();
        my $res = $ua->get("https://graph.facebook.com/me/home?access_token=${token}");
        $res->is_success or die $res->status_line;
        $data = decode_json($res->decoded_content);
    }
    $c->render(
        'index.tt',
        {
            name => $c->session->get('name'),
            data => $data->{data},
        }
    );
};


簡単ですね!
APIへのアクセスはFacebook::Graphを使うのが楽だと思うので、
プラグインFacebook::Graphのハイブリッドで使うのが良いのではないでしょうか。
yattane!

Amon2でFacebookAPIを使う その1

FacebookAPIを使う要件が出てきそうなのでいろいろ調査中。


perlでFacebookAPIを使うにはFacebook::Graphがいいんでしたよね!
Facebook::Graphのドキュメントにちょうどチュートリアルがあったので
試しに今回はその手順通りに設定していきたいと思います。
ただしそのままではなく一部Amon2用に置き換えてます。
また、今回は表示するデータを自分のニュースフィードにしてみました。


あと、Facebook アプリ登録画面ってログインするたびちょこちょこ変わってて
他のサイトの設定画面が参考にならなかったりするので
スクリーンショットも添付してみましょう。

Step 1: Set up the developer application on Facebook.

https://developers.facebook.com/ に行きます。

Step 2: Create your application.

https://developers.facebook.com/apps へ行き
「+新しいアプリケーションを作成」をクリックします。

Step 3: The Connect tab.

「アプリをFacebookに結合する方法を選択してください 」→「ウェブサイト」に
URL( ここでは http://localhost:5000/ )を入力します。

Step 4: Note your application settings.

"アプリケーションID"および"アプリケーションの秘訣"*1をメモしておくか、
このページをブックマークします。
後でこれらが必要になります。

Step 5: Create app

今回はAmon2で書くので、amon2-setup.pl を実行します

amon2-setup.pl FacebookAPI
cd FacebookAPI

Step 6: Create your Facebook::Graph object.

Facebook::Graphはリクエストごとに使いまわしたいので
アプリケーションのサブルーチンにしてしまいます。
今回はFacebookAPI.pm に以下のような記述を追加しました。

use Facebook::Graph;
sub fb {
    my $self = shift;
    if ( !defined $self->{fb} ) {
        my $conf = $self->config->{'Facebook::Graph'}
            or die "missing configuration for 'Facebook::Graph'";
        my $fb = Facebook::Graph->new(
            postback    => $conf->{postback},
            app_id      => $conf->{app_id},
            secret      => $conf->{secret},
        );
        $self->{fb} = $fb;
    }
    return $self->{fb};
}


Facebook::Graphの設定はconf/development.pl に追記しておきます。

    'Facebook::Graph' => {
        postback => 'http://localhost:5000/facebook/postback',
        app_id   => 'aaaaaaaaaaaaaaaaaaaaaa',
        secret   => 'bbbbbbbbbbbbbbbbbbbbbb',
    },

Step 7: Create your application's connect page.

認証リダイレクトを作成する必要があります。
Facebookの必要なアクセス許可を行う場所です。http://developers.facebook.com/docs/authentication/permissions
権限についての完全なリストがあります。


今回はサンプルと異なり、ニュースフィードを表示したいため
権限に read_stream を指定します。
vi lib/FacebookAPI/Web/Dispatcher.pm

get '/facebook' => sub {
    my ($c) = @_;
    $c->redirect(
        $c->fb->authorize->extend_permissions( qw(read_stream) )->uri_as_string
    );
};

Step 8: Create the Facebook access token postback page.

接続/認証ページには、アプリを承認するためにFacebookにユーザーをリダイレクトします。
ユーザーがFacebookからリダイレクトされるページを作成する必要があります。
これは、Step 6で作成したpostbackを指定します。
チュートリアルとは異なり、今回は取得したトークンをセッションに保持します。
vi lib/FacebookAPI/Web/Dispatcher.pm

get '/facebook/postback' => sub {
    my ($c) = @_;
    $c->fb->request_access_token($c->req->param('code'));
    $c->session->set('token' => $c->fb->access_token);
    $c->redirect( 'http://localhost:5000/' );
};

Step 9: Let's do something already!

アクセストークンを持っているとAPIへ要求を行うことができます。
vi lib/FacebookAPI/Web/Dispatcher.pm

get '/' => sub {
    my ($c) = @_;
    my $data;
    my $token = $c->session->get('token');
    if ( $token ) { # loggedin
        $c->fb->access_token( $token );
        $data = $c->fb->fetch('me/home')->{data};
    }
    $c->render(
        'index.tt',
        {
            data => $data,
        }
    );
};

テンプレートを書きます。
vi tmpl/index.tt

[% WRAPPER 'include/layout.tt' %]

<section>

[% IF data %]
    Hi! [% name %].
    <section>
    [% FOR v IN data %]
      <ul>
        <li>[% v.created_time %] [% v.from.name %] [% v.message %] </li>
      </ul>
    [% END %]
    </section>
    <form method="post" action="/account/logout">
        <input type="submit" value="logout" class="btn primary" />
    </form>
[% ELSE %]
    <a href="/facebook">login</a>
[% END %]

</section>

[% END %]

Step 10: Start the application and let's test this puppy out.

サーバー上で(あなたがapp.psgiのフォルダにいると仮定して)次のコマンドを実行します。

plackup

次に http://localhost:5000/ を表示します。


login リンクをクリックすると認証ページヘ遷移し、
認可すると認証プロセスを経て最終的に自分のニュースフィードが表示されます。


yatta!

コード

今回書いたコードをgithubに上げておきました。
https://github.com/toritori0318/Amon2-Facebook-Sample1


その2へ続く。。。

*1:秘訣って何…