メールでJenkinsにコマンドを送りたい(2) 〜Builder編〜
昨日の日記で書いたのは、
1.メールの件名(subject)にコマンドを書いて専用のアドレス宛てに送信することでJenkinsに実行させたい
だったので、サンプルプロジェクトをちょこっと修正したら実現できそうです。
実際にやった作業を箇条書きしておくことにします。
- プラグインプロジェクトの作成(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