2012/07/31

録画をJenkinsのジョブで。hudson-remote-apiを使う。

せっかくJenkinsを動かしているのに録画がOS依存の’at'ジョブなのがどうなの?と思っていた。Jenkinsで全部やれればいいのにと。どうすれば簡単にできるか模索していた。今回一応思い通りのことが出来たので、その流れを書き記しておく。

外からJenkinsにアクセスする方法は?=>Remote AccessAPIがある。うん。この手のシステムはREST-APIアクセスが出来るのは当然だね。だけど直接WEB-APIをせっせと書くのは大変なので、通常はスクリプト言語で簡単にアクセスするラッパーライブラリがあったりするもの。

探してみた。色々あった。”rubygemsから探せるjenkinsライブラリ”
jenkins-0.6.8    何でも出来そうなやつ。最初はこれか?と思った。
jenkins_job     やりたい事はこれに近いなぁ。
jenkins-remote-api これか?
もう、お腹いっぱい。とりあえず使ってみようかと。色々やってみた。
結論。分からん。自分のJenkinsサーバーへアクセスさせる方法すら分からんかったり、ジョブリストは簡単に出るけど、ジョブ内情報へのアクセス方法が分からんかったり、やりたい事が出来ないよ。
きっと出来るんだろう。でも分からないんじゃ先に進まない。別のを探そ。
jenkinsではなくhudsonキーワードで探してみた。'rubygemsのhudsonライブラリ'
似た感じのものが、いくつか出て来た。

hudson-remote-api
これを使わせていただく事にしました。Jenkinsへのアクセス設定方法が簡単だった。ドキュメントが丁寧だった。バージョン更新が最近だ。やりたい事がちゃんと出来た。
インストールもリンクの通り、sudo gem install hudson-remote-api だけ。

ジョブ設定(コンフィグ)部分をどう操作するか
楽に出来そうなものは見つけた。後はどうやるかだ。hudson-remote-apiでJenkinsジョブを作成して定時に実行するコンフィグを定義してシェルコマンドを設定して登録する?
コンフィグ部分は、Hudson::Jobのconfigでアクセスできるけど、ここへ細かくアクセスするAPIが用意されている訳ではなくxmlの固まりだ。ちゃんと記述しないとJenkinsが理解してくれない。

ジョブコンフィグ部分のXMLテンプレートを用意する
きっちりXMLを構築するスクリプトを書くのは面倒なので、テンプレートを作る事にした。そのテンプレートもJenkinsでジョブ「record_0」として作成する。スクリプトからそのジョブの設定部分を取得して必要な変更を加えたものを新たなジョブの設定として使用するようにした。

録画ジョブ名は「record_#{issue.id}」
チケットIDとジョブを結びつけるために、こういう名前のつけ方にする。
Jenkinsのビューで分類するときも「record_.*」の正規表現でビューを分ければ録画ジョブだけのビューになって見やすくなる。

実行シェルは2つ
1つはむろんの事、録画を実行するシェルだ。これはatジョブで使用した前回記事と同じ'record2.sh'。
もう一つは、録画処理終了後にチケットにファイルをアタッチする処理。今までは定時にまとめて行っていたが、Jenkinsで個別に監視できるのだから、ここに持って来た。
これで、録画処理終了直後に視聴できる状態になる。

個別にチケットへファイルアタッチして録画チケットにする改良版
record2.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'rubygems'
require 'active_record'
require 'active_support/core_ext'
require 'digest/md5'

@media_path = '/opt/videos/'

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

class Issue < ActiveRecord::Base
end

class CustomValues < ActiveRecord::Base
end

class Attachments < ActiveRecord::Base
end

def attach( desc, container_id, filename, type )
  if FileTest.exists?( @media_path + filename )
    if Attachments.first( :conditions => {
                            :container_id => container_id,
                            :digest => Digest::MD5.hexdigest(filename)
                          }).nil?
      Attachments.create( :container_id => container_id,
                          :container_type => "Issue",
                          :filename => filename,
                          :disk_filename => filename,
                          :filesize => File::stat( @media_path + filename ).size,
                          :content_type => type,
                          :digest => Digest::MD5.hexdigest(filename),
                          :author_id => 1,
                          :description => desc
                          )
    end
    return true
  end
  return false
end

def start_date_time issue
  start_time = CustomValues.first( :conditions => {:customized_id=>issue.id, :custom_field_id=>1} )
  if start_time
    time = format("%04d", start_time.value.to_i)
    Time.parse(issue.start_date.strftime("%Y%m%d") + time)
  else
    Time.now
  end
end

#
# 録画が終了したチケットの後処理
#
abort "no arguments" if ARGV.size==0
issue = Issue.find(ARGV[0])
abort "no ticket" if issue.nil?

# アタッチ
t_start = start_date_time issue
if attach "サムネール", issue.id, t_start.strftime("%Y%m%d%H%M") + '.jpg', "image/jpg"
  attach "ビデオファイル", issue.id, t_start.strftime("%Y%m%d%H%M") + '.mp4', "video/mp4"

  # 録画完了
  issue.status_id = 4
else
  # 失敗
  issue.status_id = 7
end

# 録画トラッカーへ
issue.tracker_id = 3
issue.save

if issue.status_id == 4
  puts "successed #"+issue.id.to_s
else
  abort "failured #"+issue.id.to_s
end
1チケットのみの処理にした事とJenkinsへ失敗が伝わるように変更。

Jenkinsへジョブ登録するスクリプト
reserve2.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'rubygems'
require 'active_record'
require 'hudson-remote-api'
#require 'rexml/document'
include REXML

# :crumb => falseにしないとjenkinsから404エラーがかえってくる
Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

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

class Issue < ActiveRecord::Base
end

class CustomValues < ActiveRecord::Base
end

# ジョブ登録
def entry_job info

  # 設定取得
  return false if !Hudson::Job.list.include?("record_0")

  # コマンド定義
  commands = [
              "/opt/task/record2.sh %d %d %s" % [
                                                 info[:channel],
                                                 info[:duration],
                                                 info[:output]+info[:datetime]
                                                ],
              "/opt/task/record2.rb %d" % info[:issue_id]
             ]

  # Configure
  config = Document.new(Hudson::Job.get("record_0").config)

  # 時間設定
  config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text =
    "%s %s %s %s *" % [
                       info[:datetime][10..11],
                       info[:datetime][8..9],
                       info[:datetime][6..7],
                       info[:datetime][4..5]
                      ]

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

  # ジョブ作成
  job_name = "record_" + info[:issue_id].to_s
  job = Hudson::Job.get(job_name)
  job = Hudson::Job.create(job_name) if job.nil?

  # ジョブ更新
  job.update config.to_s
  job.enable

  puts job.name
  return true
end

# 新規予約チケット
issues = Issue.find( :all, :conditions => {:tracker_id => 2, :status_id => 1})
issues.each {|issue|
  time = CustomValues.first( :conditions => {
                               :customized_id => issue.id,
                               :custom_field_id => 1}).value.to_s
  channel = CustomValues.first( :conditions => {
                                  :customized_id => issue.id,
                                  :custom_field_id => 2}).value.to_i
  info = {
    # チケット番号
    :issue_id => issue.id,
    # 録画開始日時
    :datetime => issue.start_date.strftime("%Y%m%d") + format("%04d",time),
    # 録画時間
    :duration => issue.estimated_hours.to_i * 60,
    # チャンネル
    :channel => channel,
    # 保存場所
    :output => "/opt/videos/"
  }

  # ジョブ登録
  if entry_job info
    # '実行'ステータス
    issue.status_id = 2
    issue.save
  end
}
hudson-remote-apiでの流れはざっと以下のような感じ。

Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }
:crumb => false とやっておかないとJenkinsサーバーから404エラーになる。

return false if !Hudson::Job.list.include?("record_0")
テンプレート用の"record_0"というジョブがちゃんとあるか確認

config = Document.new(Hudson::Job.get("record_0").config)
テンプレートジョブのconfig部分をREXML::Documentで取得する。

config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text =
実行時間を設定する。elementへはXPathでアクセスが楽チン。

config.elements['/project/builders/hudson.tasks.Shell[1]/command'].text = 
config.elements['/project/builders/hudson.tasks.Shell[2]/command'].text = 
シェルコマンド2つ設定する。配列は1ベースなんだね。

job = Hudson::Job.get(job_name)
job = Hudson::Job.create(job_name) if job.nil?
新しいジョブを作る。最初にゲットしているのは既にあったら更新にしたいから。

job.update config.to_s
コンフィグXMLを文字列にしてupdateすると、ジョブ設定が出来る。

job.enable
最後に有効にして実行されるようにする。

イメージ通りJenkinsで録画ジョブを直接走らせる事が出来るようになったが問題点も無い訳じゃない。

Jenkinsだと時刻指定が何月何日何時何分なので、放っておくと一年後も走ってしまう。
まあこれは適当なサイクルで削除するスクリプトを回せばいいか。削除もhudson-remote-apiで、job.delete ってやるだけで簡単だし。

時刻指定が秒ではなく分なので開始時間ぴったりに合わせる事が出来ない。
これはちょっと問題。Jenkinsの反応時間と実際に録画が始まるまでの数秒のずれを微調整するにははやりatみたいに秒単位の設定がやりたいところだ。1分前からの録画にすれば一応安心かな?今はさほど気にしてないけど、いずれどうにかしたくなるかも。

これも、こんな事が出来るのかぁという勉強と実験だ。

0 件のコメント:

コメントを投稿