ラベル hudson-remote-api の投稿を表示しています。 すべての投稿を表示
ラベル hudson-remote-api の投稿を表示しています。 すべての投稿を表示

2012/08/10

過去の番組チケットと録画ジョブの後処理

jenkinsとrest-clientとjsonとhudson-remote-apiの組み合わせ問題

日々過去のものとなる番組チケットクローズと録画ジョブ削除を行う処理の中での話。
結構前から構築して動かしていたけれど、色々試行錯誤していて落ち着かなかった。すっきりはしてないけれど妥協点として一度書き記しておこうかと。

チケットのクローズ
これはシンプルに、開始日時が今日以前の番組チケットをクローズするっていうだけの処理。削除することもやってみたけれど、削除は負荷が高く、データベース的にもよろしくないらしいのでクローズフラグだけにしている。

close.rb

#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'active_record'
require 'active_support/core_ext'

ActiveRecord::Base.establish_connection(
        :adapter => 'mysql2',
        :host => 'localhost',
        :username => 'redmine',
        :password => 'redmine',
        :database => 'redmine'
)

class Issue < ActiveRecord::Base
end

# 昨日までの開いてる'番組'チケットをクローズ(終了)する
Issue.find(:all, :conditions => ["tracker_id=1 and start_date<?", Date.today]).each {|i|
  i.status_id = 3
  i.save
}


Jenkinsジョブの削除
録画ジョブはワンショットなので、実行済みのジョブは不要だ。実行ログ確認のため数日間は残して、それ以降の古い録画ジョブを削除したい。これを行うのに結構試行錯誤してしまった。

過去の録画ジョブは成功失敗にかかわらず、一度はビルドされているはず。そのビルド日時を調べて数日前に行われたならば削除するようにしたかった。

hudson-remote-apiでビルドタイムスタンプへアクセス出来ない
これ便利なので全部これでやりたかったんだけれど、ビルド番号は取得できるけれど、ビルドのタイムスタンプへのアクセスができそうにない。

じゃあ使うのを諦めて、rest_clientで全部処理しようかと考えたが・・

/job/ジョブ名/doDeleteでエラーが
JenkinsのRemote APIでJenkins-path/job/ジョブ名/doDeleteが消すコマンド。これをRestClient.postで実行すればいいやと。だけど
/var/lib/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/abstract_response.rb:39:in `return!': 302 Found (RestClient::Found)
なんてエラーが出てアボートしてしまう。だけど削除はされる。うーん。色々手順踏まないとpostは正常に処理できないようだ。

'record'View内の録画ジョブだけをリストアップしたい
Hudson::Job.listとかだと全部列挙されてしまう。
Hudson::Job.find("recod_.*")とかやりたい。
Hudson::Job.listInView("record")とかview内のジョブリストを得たい。
ってのが出来ない。

結局、RestClientで取得することにした。

RestClient.get "http://jenkins/view/record/api/json"

って感じで今回はJSONで受けてみた。XMLでも大した違いはないし、hudson-remote-apiがREXML/documentをrequireしてるから、XMLでやった方がスマートだと思うけど。

気持ちは、hudson-remote-apiで全部やるか、rest-clientで全部やるか、だったけど上記のように、どっちも鎮座した。すぐには解決しそうにないんで、結果を優先して

  • ジョブ情報取得はrest-clientを使用
  • 削除はhudson-remote-apiを使用
  • 情報パースは気まぐれにJSONを使用

ということになった。

/job/ジョブ名/1/buildTimestamp をTime.parse 出来ない
ビルド#1のタイムスタンプを得るJenkinsのリモートコマンドから得られる日時文字列をそのままTime.parseするとエラーになってしまう。受け付けてくれないフォーマットなのだろう。
これだけのAPI使ってるのに自力てパースなんてやりたくないぞ。

/job/ジョブ名/1/api/json
で取得した中の'timestamp'を直接頂いて、Time.atに食わせるようにした。

こんな風に、どれもこれもが中途半端な感じですっきりしないんで公開するのをためらっていたが、ひとまずの結果として残しておこうと思う。

そんなこんなのJenkinsジョブ削除スクリプトがこちら。
close2.rb

#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'hudson-remote-api'
require 'rest_client'
require 'json'

Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

# 5日前
some_days_ago = 60 * 60 * 24 * 5

if true
  # REST-APIでtimestampアクセス
  jobs = JSON.parse RestClient.get "http://localhost/jenkins/view/record/api/json"
  jobs['jobs'].each {|job|
    if job['color']=='blue'

      #timestamp = RestClient.get Hudson[:url] + "/job/#{job['name']}/1/buildTimestamp"
      #datetime = Time.parse(timestamp)
      # パースがうまく出来ないんで

      build = JSON.parse RestClient.get Hudson[:url] + "/job/#{job['name']}/1/api/json"

      datetime = Time.at( build["timestamp"].to_i / 1000 )

      if datetime + some_days_ago < Time.now

        #RestClient.post Hudson[:url] + "/job/#{job['name']}/doDelete",nil
        #/var/lib/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/abstract_response.rb:39:in `return!': 302 Found (RestClient::Found)
        #このエラーを解消できないんで

        jjob = Hudson::Job.get job['name']
        if !jjob.nil?
          jjob.delete
          puts job['name']
        end
      end
    end
  }

else
  # Hudson-remote-apiだけで出来ないかなぁ

  jobs = REXML::Document.new Hudson::HudsonObject.get_xml "http://localhost/jenkins/view/record/api/xml"

  jobs.elements.each('listView/job') {|j|
    job = Hudson::Job.get j.elements['name'].text
    if !job.nil? && job.color == 'blue'
      #p job.last_build
      # うーんと、ビルドタイムスタンプへ辿り着けないぞ
    end
  }
end


うまく出来なかった部分のコメントやら、いずれ、hudson-remote-apiだけで出きるようにならないかな?という期待を込めてのコードも残してあるんで、ちょっと読みにくいよ。

この2つのスクリプトを1日1回実行するJenkinsジョブを作って回している。が・・

Jenkinsで動かすと以下の様なエラーが
+ /opt/task/close2.rb
/usr/lib/ruby/1.9.1/json/common.rb:148:in `encode': "\xE9" on US-ASCII (Encoding::InvalidByteSequenceError)
 from /usr/lib/ruby/1.9.1/json/common.rb:148:in `initialize'
 from /usr/lib/ruby/1.9.1/json/common.rb:148:in `new'
 from /usr/lib/ruby/1.9.1/json/common.rb:148:in `parse'
 from /opt/task/close2.rb:15:in `
' Build step 'Execute shell' marked build as failure
普通に自分の環境でコマンド実行する場合は問題ないけど、Jenkinsジョブとして動かすとこんなエラーが出てしまう。気まぐれに使ったJSONが仇になったか。ちょうど'record'ビューの説明文に日本語を使っていたので、引っかかってしまった。

JSON自体はunicodeにもちろん対応しているけど、encodeの動作はLANG環境変数に依存しているようだ。解決方法は2つ。
  • シェルコマンドに "LANG=ja_JP.UTF-8 /opt/task/close2.rb" って個別に書くか
  • Jenkinsのシステム設定のグローバルプロパティで環境変数を設定するか
どっちでもいいと思うけど、また同じようなことで対応するのは面倒なのでJenkinsの設定でLANG環境変数を設定することにした。

2012/08/04

予約・キャンセルをredmine-plugin-hookで即時実行

チケット操作で予約・キャンセルの即時実行がやっと出来るようになった。
今までは1時間に1回実行している予約スクリプトでチケットトラッカーとステートを確認して録画ジョブ登録や削除をしていたけど、リアルタイム性がなかった。
いろいろ調べてredmineプラグインのフック機能を使えば、チケット操作した時点でジョブ登録・削除を行えるんじゃないかと。
実際にやってみたらいい感じだったので、その辺を書き記しておく。

使用したフック機能は2つ
1. controller_issues_edit_before_save
2. controller_issues_bulk_edit_before_save
1は、普通にチケット更新ページで色々変更した時に呼ばれるフック関数
2は、チケットリストビュー上で複数チケットを選択して右クリックポップアップメニューから属性1つを変更した時にチケットごとに呼ばれるフック関数。この方法が断然便利。

どんなフックがあるかは、Redmine plugin hooks listを参照した。
$ rake redmine:plugins:hook_list
ってやれば確認できるっていうことだけど、そんなビルドタスクはないよって言われた。redmine2.0だとやり方違うのかもね。

クラス継承するものが以前とちょっと違うんだね。以前は
Redmine::Hook::Listenerを継承するんだったはずだけど、新しいredmineでは
Redmine::Hook::ViewListenerを継承するのだそうだ。微妙な違いだけどちょっと悩んだよ。


lib/video_hooks.rb
フックスクリプトは以前視聴ビュー生成のために作ったredmine_videoプラグイン内に追加することにした。フックするリプとはプラグイン内のlibに入れておけばいいようだ。

そして、init.rbの先頭に
require 'video_hooks'
って記述しておくとフックがかかるようになる。

使用するGEMモジュール
hudson-remote-api
前回記事でも登場したJenkinsにREST-APIアクセスする便利ラッパーを使用する。
rexml/document
これもJenkinsジョブ設定XMLを操作するために使用する。

フックスクリプトに、require 'hudson-remote-api'って書いたら見つからないって怒られた!
??パスが通っていないってことか?と思って、直接パスで
$LOAD_PATH << "/var/lib/gems/1.9.1/gems/hudson-remote-api-0.6.0/lib"
require 'hudson-remote-api'
ってやってみたら、読んでくれるようになったが・・・うーむ。こうじゃないよねぇ。

調べてみてもこの辺のことを明確に説明している記事とかが見つからない。
redmine/config/enviromnent.rbで$LOAD_PATHに追加しろとか、redmine/Gemfileに追加しろとか・・・ん?Gemfile?
redmine/Gemfileを覗いてみたら、最後のところでプラグイン内のGemfileがあったらそれも読むよって感じになってるぞ?なるほど。そういうことか。

自分のプラグインにGemfileを用意して
gem 'hudson-remote-api'
って1行記述。そうしたら、スクリプト内でrequireする必要なく使えるようになった。
今更ながらGemfileの存在意味が少し理解できた気がする。でも分かりにくいなぁ。

因みにrexmlは、requireもGemfile記述も必要なく使えた。

これでやっと中身が作れる。出来上がったフックスクリプトがこちら
video_hooks.rb
# -*- coding: cp932 -*-

Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

class VideoHooks < Redmine::Hook::ViewListener

  # チケット更新
  def controller_issues_edit_before_save(context)
    edit_hook context[:issue]
  end

  # バルク更新(チケットごとに呼ばれる)
  def controller_issues_bulk_edit_before_save(context)
    edit_hook context[:issue]
  end

  def edit_hook issue
    # 番組内のチケット
    if issue.project_id == 1
      do_cancel issue if issue.tracker_id == 1 || issue.status_id == 5
      do_reserve issue if issue.tracker_id == 2
    end
  end

  # キャンセル
  def do_cancel issue
    job = Hudson::Job.get "record_#{issue.id}"
    if !job.nil?
      # 予約されていたらジョブ削除してチケットを番組キャンセルに更新
      job.delete
      issue.status_id = 5
      issue.tracker_id = 1
    end
  end

  # 予約?
  def do_reserve issue
    config = configure issue
    if !config.nil?
      # Jenkinsジョブ登録してチケットを実行中に更新
      job_name = "record_#{issue.id}"
      job = Hudson::Job.get job_name
      job = Hudson::Job.create job_name if job.nil?
      job.update config
      job.enable
      issue.status_id = 2
    end
  end

  def configure issue
    return nil if issue.custom_field_values.nil?

    # Configure
    job0 = Hudson::Job.get("record_0")
    return nil if job0.nil?

    config = REXML::Document.new job0.config
    return nil if config.nil?

    time = issue.custom_field_values[0].to_s
    channel = issue.custom_field_values[1].to_s
    duration = issue.estimated_hours.to_i * 60
    datetime = issue.start_date.strftime("%Y%m%d") + format("%04d",time)
    output = "/opt/videos/#{datetime}"

    # コマンド定義
    commands = [
                "/opt/task/record2.sh %d %d %s" % [
                                                   channel.to_i,
                                                   duration,
                                                   output
                                                  ],
                "/opt/task/record2.rb %d" % issue.id
               ]
    trigger = "%s %s %s %s *" % [
                                 datetime[10..11],
                                 datetime[8..9],
                                 datetime[6..7],
                                 datetime[4..5]
                                ]
    # 時間設定
    config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text = trigger

    # コマンド登録
    config.elements['/project/builders/hudson.tasks.Shell[1]/command'].text = commands[0]
    config.elements['/project/builders/hudson.tasks.Shell[2]/command'].text = commands[1]

    return config.to_s
  end
end

フックの口は2つだけど、やることは同じなので、何方からも'edit_hook'メソッドへ飛ばす。
キャンセルなのか予約なのか判断して、それぞれのメソッドに飛ばす。

do_cancel
チケットに対応した録画ジョブがあればhudson-remote-api使って削除して、チケットを番組チケットにする。ステータスはキャンセル状態にする。理由は自動予約チケットだったら自動で再予約されないようにロックしたいから。

do_reserve
こっちは前記事のreserve2.rbから移植したもので、Jenkinsジョブを登録してチケットを実行中にする処理を行う部分。カスタムフィールドへのアクセス方法が外部タスクとやり方が違う。
何がどう変化したのか?というトリガーではなくどういう状態?というステートでしか判断できないので予約チケットを更新すると実行されてしまうけど、手動で録画時間などを変更したりする場合もこれで行けるだろうから、よしかな。

プラグインと外部タスクで処理モジュール共有したいけど、チケットへのアクセス方法とかが微妙に違うんで難しいな。Jenkinsジョブ処理部分だけでも整理しようかな。やる気になったらかな。

とにかく、これで即時実行が出きるようになったので、1時間毎に動かしていた予約スクリプトはクエリー予約だけ1日1回動けばいいだけになったので、その部分も変更しなくては。
次回はそのあたり。