読者です 読者をやめる 読者になる 読者になる

量産型エンジニアの憂鬱

きっと僕は何物にもなれない。

ウェブでログインが必要なルーチンワークをPythonスクリプト書いてJenkinsさんに実行してもらう

Python Jenkins 自動化

最近、ルーチンワークを少しずつ自動化しています。
ウェブをチェックするようなルーチンはスクレイピングを行うのですが、ログインなどのブラウザ操作が必要な場合もあります。
ブラウザ操作が必要な場合でも自動化するのが今回の目標。

Python + Seleniumで自動操作

ウェブブラウザの自動操作はSeleniumの得意とするところです。
JavaPythonPerlRubyJavaScript(Node.js)など様々な言語で利用できます。

やっぱりパパッと書いて実行するのはスクリプト言語の得意とするところですね。
普段はPerlスクリプトを書くことが多いのですが、今回はPythonで書きました。
理由は、 Selenium Server を実行するのがめんどくさかっただけ(これも後述するDockerfileに書いちゃえばあとは簡単)。

今回は、Facebook
f:id:duck8823:20160903215516p:plain
この部分の新着通知をSlackに通知したいと思います。

書いたPythonスクリプトはこんな感じ。


WebDriverのAPIこちらを見れば書いてあります。
引数に渡されたユーザ名とパスワードを利用してFacebookにログインし、ログイン後のページ要素から情報を取得しています。

Slackへの通知はWebHooksを利用。
WebHooksのURLは適宜変更してください。

このスクリプト


で実行できます。

f:id:duck8823:20160904015408p:plain
ローカルマシンで実行する場合はWebDriverをFirefoxで指定するといいです。

Dockerで実行できるようにする

上のスクリプトをJenkins上で実行できるようにするには、Dockerコンテナを作ってその中で実行するのが楽です。
Python3.5、Selenium、PhantomJSをインストールしたDockerfileはこちら。


これを実行するのは、


Jenkinsに実行させる

ジョブはフリースタイルで作成しました。

ソースコード管理

f:id:duck8823:20160904012339p:plain
スクリプトとDockerfileがあればよいので、gistにのせてそこからcloneするようにしました。

ビルド環境

f:id:duck8823:20160904012624p:plain
パスワードをビルドに直書きしたくなかったので、credentialsに登録したユーザ名とパスワードを利用できるようにします。
(コンソールには表示されてしまいます。)
Credentials Buinding Pluginを利用しました。
参考にしたのは、ここ

ビルド

f:id:duck8823:20160904013052p:plain
Dockerを実行させるスクリプトのユーザ名とパスワードを、ビルド環境のUse secret text(s) or file(s)で指定したものに変更します。

定期的に実行させる

f:id:duck8823:20160904013401p:plain
あとは定期的に実行させるだけです。
ビルド・トリガ定期的に実行にチェックを入れ、cron形式でスケジュールを登録します。
上記の例は毎朝9時に実行する例。



これを応用すれば内部サイトのアップデート確認、実行などいろんなことができます。
JenkinsもDockerがあれば簡単に実行できるいい時代です。
ローカルJenkinsさんを雇ってルーチンワークを肩代わりしてもらいましょう。

GmailをSlackに通知するPerlスクリプトを書いた

Slack perl

チームSlackを導入してしばらくたったのですが、 まだ一部のやり取りはメールで行っています。

この通知をまとめたい場合、SlackにはEmail連携が用意されています
Slack便利。
が、これは有料版でしか使えません。
メールクライアントソフト使えばいいかもしれないんですが、自分は今までメールを開きっぱなしだったモニタの一部分をSlackに置き換えたので、
Slackを開いているとついついメールが来ていることに気づくのが遅くなってしまいます。

メールはGmailなんやし、Gmail APIで取得してSlackのBotに通知させりゃええやん。

f:id:duck8823:20160828134749p:plain
f:id:duck8823:20160828135323p:plain
こんな感じで通知あるとわかりすいですよね。 ってことで、スクリプトを書きました。

作成したスクリプト

use strict;
use warnings FATAL => 'all';

use Google::API::Client;
use Google::API::OAuth2::Client;

use utf8;
use Encode;

use Slack::RTM::Bot;

my $user_id = 'Gmailアカウント(メールアドレス)';
my $client_id = 'Google APIs プロジェクトのクライアントID';
my $client_secret = 'Google APIs プロジェクトのクライアントシークレット';
my $label_regex = qr/Gmailのラベルの正規表現/;

my $slack_api_token = 'Slack Bot のトークン';
my $channel = '通知するSlackのチャンネル';

my $interval = 60;

my $bot = Slack::RTM::Bot->new(
    token => $slack_api_token
);
$bot->start_RTM;

my $auth = Google::API::OAuth2::Client->new({
        auth_uri => "https://accounts.google.com/o/oauth2/auth",
        token_uri => "https://accounts.google.com/o/oauth2/token",
        client_id => $client_id,
        client_secret => $client_secret,
        redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
        auth_doc => {
            oauth2 => {
                scopes => {
                    "https://www.googleapis.com/auth/gmail.readonly"
                    => "gmail read"
                }
            }
        }
    });
print Encode::encode_utf8("以下のURLにアクセスし、表示されるコードを入力してエンターを押してください.\n");
print $auth->authorize_uri . "\ncode: ";
my $code = <stdin>;
$auth->exchange($code);

my $service = Google::API::Client->new()->build('gmail', 'v1');
my $labelIds = [];
my $res = $service->users->labels->list(body => {userId => $user_id})->execute({auth_driver => $auth});
for my $label (@{$res->{labels}}) {
    push @$labelIds, $label->{id} if $label->{name} =~ $label_regex;
}
$auth->refresh;

while(1) {
    my @new_messages;
    for my $labelId (@$labelIds) {
        my $body = {
            userId => $user_id,
            labelIds => $labelId,
            q => 'is:unread'
        };
        $res = $service->users->messages->list( body => $body )->execute( { auth_driver => $auth } );
        for my $message (@{$res->{"messages"}}) {
            $res = $service->users->messages->get( body => { userId => $user_id, id => $message->{id} } )->execute( {auth_driver => $auth } );
            if($res->{internalDate} / 1000 < time() - $interval) {
                last;
            }
            my %headers = ();
            for my $header (@{$res->{payload}->{headers}}) {
                $headers{$header->{name}} = $header->{value};
            }
            my $mes = <<"EOL";
From: $headers{From}
To: $headers{'Delivered-To'}
Date: $headers{Date}
Subject: $headers{Subject}
Snipet: $res->{snippet}
EOL
            push @new_messages, $mes;
        }
        $auth->refresh;
    }
    if (@new_messages > 0) {
        $bot->say(
            channel => $channel,
            text => Encode::encode_utf8(sprintf("%s 件の新規メッセージがあります.\n%s", scalar @new_messages, join("\n\n", @new_messages))))
    }
    sleep $interval;
}

Google APIs プロジェクトの作成

Gmail APIを利用するにはGoogle APIsでプロジェクトを作成する必要があります。
Gmailアカウントにログインしたら、Google APIs ライブラリにアクセスします。

f:id:duck8823:20160828113123p:plain
上部メニューのプロジェクト(すでにプロジェクトがある場合はその名前になっていると思います)をクリックし、プロジェクトを作成をクリックします。

下記のようなウィンドウが表示されるので、利用規約を読んで同意し、作成 をクリックします。
f:id:duck8823:20160828113535p:plain

しばらくしたら、ヘッダが作成したプロジェクトになっています。
f:id:duck8823:20160828113806p:plain

Gmail API の有効化

左メニュー > ダッシュボード
から、 [API を有効にする] をクリックします。
f:id:duck8823:20160828114209p:plain

すると検索フォームが現れるので、 Gmail と入力します。
リストにGmail APIが表示されるので、それをクリックします。 f:id:duck8823:20160828114316p:plain

ページ上部に 有効にする と表示されているので、クリックすることで有効化できます。
f:id:duck8823:20160828114512p:plain

認証情報を作成する

f:id:duck8823:20160828115455p:plain

Gmail APIは認証が必要ですので、このままでは使用できません。
以下のように表示されるので、認証情報を作成します。

左メニュー > 認証情報 > OAuth同意画面
をクリックし、
ユーザーに表示するサービス名を入力し 保存 をクリックします。
サービス名以外は任意です。
f:id:duck8823:20160828121459p:plain

続いて
左メニュー > 認証情報 > 認証情報
から、 認証情報を作成 をクリックし、 OAuth クライアントID をクリックします。
f:id:duck8823:20160828121750p:plain

クライアントIDの作成画面が表示されるのですが、
今回はスクリプトなので、 その他 にしました。
f:id:duck8823:20160828123219p:plain

作成をクリックすると、クライアントIDとクライアントシークレットが表示されるので、最初に紹介したスクリプトをこれで置き換えてください。
f:id:duck8823:20160828122053p:plain

使ったモジュール

Gmail APIGoogle::API::Clientを拝借しました。
こちらの記事を参考にさせていただきました。
Slackへの通知はSlack::RTM::Botを使っています。

使い方

上記スクリプトを実行すると、コンソールに認証URLが表示されます。

> perl script.pl
以下のURLにアクセスし、表示されるコードを入力してエンターを押してください.
https://accounts.google.com/o/oauth2/auth?client_id=Google APIs プロジェクトのクライアントID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/gmail.r
eadonly
code:

表示されるURLにアクセスすると、アプリケーション権限の許可画面になります。
この場合は表示のみのアクセス権です。
f:id:duck8823:20160828201809p:plain

許可をクリックするとコードが表示されるので、コンソールにコピペしてエンターキーを押します(上記スクリプトではコンソールには表示されないようしています)。
f:id:duck8823:20160828203054p:plain

この後は、Ctr + zを押した後に bgコマンドをうつとバックグラウンドに移せます。

Slack::RTM:BotGoogle::API::OAuth2::Clientを組み合わせれば、 SlackのBotにつぶやいて任意のタグの未読メールを取得するなんてことも簡単ですね!

参考

Google Gmail APIでメールを取得する - Qiita
Google::API::Clientを使う(perl) | @knok blog

Gitで試行錯誤したコミットをまとめたい

git

GitとCIサービスを連携していて、CIサーバの設定がうまくいかなくて何度もコミットしちゃうことありますよね。
amend をつけててもリモートリポジトリに push する段階でマージを余儀なくされたり。
--force で push すればよいのですが、あとから一連のコミットをまとめたい場合の操作。

以下のコマンドをうってログを表示

git log --oneline
2dae1e1 travis.yml修正
9bc8c31 travis.yml修正
ebb438c init. commit.

今回は travis.yml で試行錯誤したコミットをまとめたいと思います。

rebase を使います。まとめたい一連のコミットのひとつ前を指定。

git rebase -i ebb438c

すると編集画面が立ち上がります。

  1 pick 9bc8c31 travis.yml修正
  2 pick 2dae1e1 travis.yml修正
  3 
  4 # Rebase ebb438c..2dae1e1 onto ebb438c (1 command(s))
  5 #
  6 # Commands:
  7 # p, pick = use commit
  8 # r, reword = use commit, but edit the commit message
  9 # e, edit = use commit, but stop for amending
 10 # s, squash = use commit, but meld into previous commit
 11 # f, fixup = like "squash", but discard this commit's log message
 12 # x, exec = run command (the rest of the line) using shell
 13 # d, drop = remove commit
...

説明にある通り、s にすると一つ前のコミットとまとめてくれます。

  1 pick 9bc8c31 travis.yml修正
  2 s 2dae1e1 travis.yml修正

これで編集を保存して終了すると、新しいコミットメッセージを入力する画面になるので入力してできあがり。

あとは

git push --force

してやればまとまります。

でも force push は複数人で開発してるリポジトリにはやめといた方がいいですね。

Spring BootプロジェクトをIntelliJでJUnitするときプロファイルを指定したい

Java IntelliJ IDEA Spring Boot テスト

Spring Bootを使って開発してますが、Selenideによる画面テストを実施する際、ローカルではFirefox、CIサーバ上ではphantomjsドライバを使ってテストしています。
また、ローカルとCIサーバで利用するドライバの切り替えは application.properties と pom.xml のプロファイルで行ってます。

application.properties

# PhantomJSのexeファイルのパス
phantomjs.binary.path=@phantomjs.binary.path@
# Selenideで使用するブラウザ
selenide.browser=@selenide.browser@


pom.xml

<properties>
    <!-- デフォルトのプロパティ -->
    <selenide.browser>firefox</selenide.browser>
</properties>
...
<profiles>
    <profile>
        <!-- CIサーバ -->
        <id>ci</id>
        <properties>
            <phantomjs.binary.path>/usr/local/phantomjs/bin/phantomjs</phantomjs.binary.path>
            <selenide.browser>phantomjs</selenide.browser>
        </properties>
    </profile>
<profiles>

application.propertiesで値を @ で囲むことによって、 pom.xml に記述されたプロパティに置き換えてくれます。
また、 <profiles> を利用することによってそのプロパティを上書きすることができます。

上記の場合、

mvn test

を実行するとfirefoxドライバが指定され、

mvn test -Pci

とすると、phantomjsドライバが指定されます。

これでテストしてたのですが、ローカルではテスト通ったのにCIサーバ上ではテストこけることがしばしば。試しにローカルで phantomjs でテストするとやっぱりこける。

Maven から test を実行すればプロファイルの指定も出来ますが、失敗したテストだけ再実行などができない。テスト毎にログを見ることができない。
など、やっぱり IntelliJJUnit でドライバを指定できるようにしたい。
このボタンからドライバ指定してテストしたい。
f:id:duck8823:20160813144405p:plain

ていうことでちょっと調べたんですが、Spring Bootではプロファイルを切り替える仕組みがもともとある様子。

切り替えるには、application.yml か application.properties かで変わるのですが、 application.properties を使っている場合は、
application-[プロファイル名].properties という名前でファイルを作成し、上書きしたいプロパティを記述すれば良いようです。
以下のファイルを作成しました。

application-phantomjs.properties

# PhantomJSのexeファイルのパス
phantomjs.binary.path=/path/to/pahtnomjs.exe
# Selenideで使用するブラウザ
selenide.browser=phantomjs

IntelliJJUnit 実行時にこのプロパティファイルで上書きするには、 Run/Debug Configuration の VM options に -Dspring.profiles.active=プロファイル名 とすればよいです。
f:id:duck8823:20160813145830p:plain

-Dspring.profiles.active オプションの有と無で設定を作っておけば、選択するだけでドライバを切り替えられます。
リポジトリにプッシュする前にローカルの phantomjs でもテストしておけば、安心安心。

Redmineプラグインを作ってみた

Redmine プラグイン

業務における運用作業をRedmineで管理することになったのですが、そのままでは非常に使いにくい気がしたのでプラグインを作成することにしました。

運用作業をチケットとして管理するには

  • チケットを作成する
  • チケットの進捗率を変える
  • チケットのステータスを変える
  • チケットの作業時間を記録

これらのコストがかかります。
運用作業は「やった」か「やってない」かだと思うので、チケットの進捗率・ステータス・作業時間は自動的に更新したい。
チケットの作成も、同じものをいくつも登録しなければならないので、これを簡単に行えるようにしたいです。

これまで Rails でアプリを作ったことはおろか、 Rubyスクリプトを書いたこともないですが、作ってみました。
作成には以下の記事がとても参考になりました。

Redmine3.2プラグイン開発入門 (1) - 新規画面を追加する - Qiita
Rails を知らない人のための Redmine プラグイン開発ガイド

作成したプラグイン

作成したプラグインGitHubにおいてます。
github.com

インストール

git clone https://github.com/duck8823/redmine_operations.git plugins/redmine_operations
rake redmine:plugins:migrate RAILS_ENV=production

使い方

プロジェクトの設定で使用するモジュールにチェックを入れると、プロジェクトメニューに 「運用」 が追加されます。
「運用」メニューでは、運用作業の登録・編集と、スケジュールの登録が行えます。
各種設定もここで行います。
f:id:duck8823:20160813010558p:plain
スケジュールの登録はカレンダーをポチポチクリックするだけなので、楽だと思います。

ここで作成したチケットには、運用の項目が追加されます。
メニューで登録したタスク一覧が表示され、チェックボックスのチェックの数に連動して進捗率・ステータス・作業時間が自動的に更新されます。
f:id:duck8823:20160813011103p:plain

Redmineプラグイン作成用のコマンドも用意されていて、他言語の開発経験があれば結構簡単に作成できました。
どんどん作成して楽できるところは楽したいですね。

参考にしたサイト

Redmine3.2プラグイン開発入門 (1) - 新規画面を追加する - Qiita
Rails を知らない人のための Redmine プラグイン開発ガイド
jQueryUIのDatepickerで複数の日付を選択する(簡易版) - thinking now...