メールでJenkinsにコマンドを送りたい(2) 〜Builder編〜

昨日の日記で書いたのは、


1.メールの件名(subject)にコマンドを書いて専用のアドレス宛てに送信することでJenkinsに実行させたい

    • コマンドの形式はCLIを流用すれば楽できそう
    • Builderの拡張としてプラグインを作成して、一定間隔で実行させればいいかな


だったので、サンプルプロジェクトをちょこっと修正したら実現できそうです。
実際にやった作業を箇条書きしておくことにします。

  • プラグインプロジェクトの作成(mvn -cpu hpi:create) ※参考(id:sikakura:20110314)
  • サンプルのクラス名を変更(リファクタリング)HelloWorldBuilder⇒MailCommandBuilder
  • リソースの削除(global.jelly)および内容変更(config.jelly)
  • MailCommandBuilderの実装


今回のプラグインは、MailCommandBuilderというシンプルなものにしました。
また、global.jellyは、システムの設定で入力させたいパラメータがある場合に記載するので、今回は不要と判断して削除しました。
同時にサンプルに存在していたコーディングも削除しています。config.jellyは使用します。
メールクライアントの実装としてPOP3を考え、メールサーバホスト、ポート番号、ユーザ、パスワードをジョブの設定画面で
指定してもらう想定としました。よって、以下のようなconfig.jellyを作成しました。

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="POP3 mail server address" field="address">
    <f:textbox />
  </f:entry>
  <f:entry title="POP3 mail server port" field="port">
    <f:textbox default="110"/>
  </f:entry>
  <f:entry title="POP3 User Name" field="username">
    <f:textbox />
  </f:entry>
  <f:entry title="POP3 Password" field="password">
    <f:password />
  </f:entry>
</j:jelly>


こんなイメージです。



次に実装ですが、パッケージやインポート、コメントを省いて、全て載せておきます。
Builderとして必須のperformメソッドの実装と、POP3サーバへ接続してメールを取得する処理のrecieveメソッドが主な修正点となります。

public class MailCommandBuilder extends Builder {

    private final String address;
    private final String port;
    private final String username;
    private final String password;

    @DataBoundConstructor
    public MailCommandBuilder(String address, String port, String username,String password) {
        this.address = address;
        this.port = port;
        this.username = username;
        this.password = password;
    }
    public String getAddress() {return address;}
    public String getPort() {return port;}
    public String getUsername() {return username;}
    public String getPassword() {return password;}

    public String receive(BuildListener listener, String commands) {

        Properties props = new Properties();
        Session sess = Session.getDefaultInstance(props);
        String subject = null;

        Store store;
        try {
            store = sess.getStore("pop3");//imapsでもそのまま行けそうな気が
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
            return null;
        }
        try {
            store.connect(address, Integer.valueOf(port), username, password);
            Folder rootFolder = store.getDefaultFolder();
            Folder inbox = rootFolder.getFolder("INBOX");
            inbox.open(Folder.READ_WRITE); //メールを消すためにREAD_WRITEが必要

            Message[] messages = null;
            if (inbox.getMessageCount() > 10)//後でパラメータ化
                messages = inbox.getMessages(inbox.getMessageCount() - 10, inbox.getMessageCount() - 1);
            else
                messages = inbox.getMessages();
            FetchProfile fp = new FetchProfile();
            fp.add("Subject");
            inbox.fetch(messages, fp);
            for (Message message : messages) {
                String findStr = message.getSubject().split(" ")[0];
                if (commands.indexOf(findStr) != -1) {
                    subject = message.getSubject();
                    message.setFlag(Flag.DELETED, true);
                    break;//1件でも処理したらループ終了
                }
            }
            inbox.close(true);
            store.close();
        } catch (MessagingException e) {
            e.printStackTrace();
            return null;
        }
        return subject;
    }

    @Override
    public boolean perform(AbstractBuild build, Launcher launcher,BuildListener listener) {

        if (Hudson.getInstance().getRootUrl() == null) { //nullのケースあり
            listener.getLogger().println("Please save a once System property!");
            return false;
        }
        StringBuffer commands = new StringBuffer();
        for (CLICommand c : CLICommand.all())
            commands.append(c.getName());

        String command = receive(listener, commands.toString());
        if (command == null) {
            listener.getLogger().println("Don't find command mail.");
            return true;
        }
        String cliCommand = "-s " + Hudson.getInstance().getRootUrl() + " " + command; //StringBuilder使うべき?
        List<String> args = Arrays.asList(cliCommand.split(" "));
        String url = System.getenv("JENKINS_URL");
        while (!args.isEmpty()) {
            String head = args.get(0);
            if (head.equals("-s") && args.size() >= 2) {
                url = args.get(1);
                args = args.subList(2, args.size());
                continue;
            }
            break;
        }
        if (url == null)
            return false;
        if (args.isEmpty())
            args = Arrays.asList("help"); // default to help

        CLI cli = null;
        int resultInt = -1;
        try {
            cli = new CLI(new URL(url));
            args = new ArrayList<String>(args);
            resultInt = cli.execute(args, System.in, listener.getLogger(), listener.getLogger());
            cli.close();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
        if (resultInt == 0)
            return true;
        else
            return false;
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }
        public String getDisplayName() {
            return "Mail Commander";
        }
    }
}

<performメソッドについて>
基本路線は手抜き再利用なので、メールの件名(subject)にCLIのコマンドを書いてもらうような想定にしています。
さすがに-s http://〜は省略したかったので、Hudson.getInstance().getRootUrl()からURLを取得することにしましたが、
システムの設定を一度も保存していないとnullが返却されるようです。
CLICommand.all()で実装されているCLIコマンドの一覧を取得し、文字列を作成してからrecieveメソッドをコールします。
recieveメソッドからnullが返却されれば、該当のメールなしと判断して終了します。
null以外は、CLIコマンドが返却されている想定ですので、「-s http://〜」を先頭に結合してCLIに渡してあげます。


<recieveメソッドについて>
引数にCLIコマンドを連結した文字列が渡されてきます。
メールボックスに溜まったメール件数×CLIコマンドの数だけループをまわすのが嫌だったので、CLIコマンドを連結させた文字列
とメールの件名(subject)をindexOfにより部分一致で判定しています。

Message[] messages = null;
if (inbox.getMessageCount() > 10)//後でパラメータ化
    messages = inbox.getMessages(inbox.getMessageCount() - 10, inbox.getMessageCount() - 1);
else
    messages = inbox.getMessages();

としている部分は、メールボックスにメールがたくさん溜まっていると重い処理になってしまうのでこうしています。
"10"と固定で指定している部分はあとからパラメータ指定できるようにする予定です。
一件でも該当するメールが見つかれば終了することにしています。
見つかった場合は、削除フラグをセットし、コネクション切断時にメールボックスから削除します。見つからなければnullを返します。


さて、ここまでの実装でメールによるコマンド実行は出来ます。例えば下のようなメールを送信しておいて

JenkinsのジョブでMailCommandをビルダーに設定したジョブを実行すると

このように実行されます。ジョブの設定を定期実行にしておけば、ある程度自動化できます。


しかし、このままだとJenkinsの画面を見ないと結果が分かりません。
次のステップとして結果をメール送信するように対応したいと思います。


そういえば、ついにWindows Installer対応しましたね!
http://jenkins-ci.org/content/windows-installers-are-now-available?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+ContinuousBlog+%28Hudson+Labs%29