ラベル jenkins の投稿を表示しています。 すべての投稿を表示
ラベル jenkins の投稿を表示しています。 すべての投稿を表示

2012/09/12

Jenkins新規ジョブに所要時間を設定する

Jenkinsのジョブは繰り返しビルドされることが前提。2回目からどれくらいで終了するかプログレス表示される。これが精神衛生上とってもいいわけだけど、前回の結果が分からないと表示されない。

録画システムで使用しているJenkinsのビルド所要時間を最初に設定できないかな?

録画ジョブは性質上1回実行したらお役御免。同じ日時の2回目はない。だけど、どれくらいで終了するかを表示させたい。どうにか出来ないかな?ということでやってみた。

思惑
録画時間は分かっている。エンコード時間も予測できる。時間予測は可能だ。
Jenkinsが過去にビルドした情報として扱ってくれれば出るはず。

所要時間予測
今までの実績から算出すると録画時間に対しての所要時間は2.2倍程度かな。
番組時間が30分程度の場合は1.7〜2.2位、2、3時間の場合は2.2〜2.4位。
単純に、ビルド予想時間=番組時間 * 2.2 ってことでいいかな。
沢山のサンプリングデータから統計を取って非線形計算が出来れば精度は上がるだろうね。

ビルドデータ
#{JENKINS_HOME}/jobs/#{ジョブ名}/builds/#{ビルド日時}/build.xml
に、このビルドについての情報が記述されている。この中で重要なのが、durationタグだ。
Jenkinsは前回のビルドのこの情報をもとにメーター表示しているようだ。
その一例
<?xml version='1.0' encoding='UTF-8'?>
<build>
  <actions>
    <hudson.model.CauseAction>
      <causes>
        <hudson.triggers.TimerTrigger_-TimerTriggerCause/>
      </causes>
    </hudson.model.CauseAction>
  </actions>
  <number>1</number>
  <result>SUCCESS</result>
  <duration>3977984</duration>
  <charset>US-ASCII</charset>
  <keepLog>false</keepLog>
  <builtOn></builtOn>
  <workspace>/var/lib/jenkins/jobs/record_14147/workspace</workspace>
  <hudsonVersion>1.424.6</hudsonVersion>
  <scm class="hudson.scm.NullChangeLogParser"/>
  <culprits/>
</build>

適当なジョブを作って、適当なビルド結果をコピーしてどうなるか試してみた。
Jenkinsのシステム設定からリロードをしたら、情報を拾ってくれて反映した。
色々やって、最低限必要な情報のみにしたbuild.xmlは以下のようになった。
<build>
 <number>0</number>
 <result>SUCCESS</result>
 <duration>#{duration}</duration>
</build>

ビルド番号は0番でいけた。最初のビルドが1番っていう約束事が崩れないで済みそう。

APIじゃ出来なそう
ビルドディレクトリとファイルをあらかじめ作成してしまえばいいわけだけれど、REST-APIじゃ出来ないみたい。Jenkinsプラグインなら?ってちょっと調べたけど、ジョブ作成フックみたいな切り口がないようで断念。Redmineのチケット編集フックみたいに出来ればよかったんだけれど。

パーミッションをどうするかなぁ
Jenkins側を拡張する方法がだめっぽいので外部タスクで実行することになる。そうすると、その辺のディレクトリパスはJenkinsユーザだぜっていうパーミッションが絡んでくる。選択肢はいくつか考えられる。

1. Jenkins側をを実行タスクと同じユーザにする
この場合、redmine(rails)ユーザと同じwww-dataにするってことか。

Jenkinsが扱うパス/ファイルオーナー’をwww-dataにする。
/etc/default/jenkinsファイルのJENKINS_USERをwww-dataにする。

2. 実行タスク側をJenkinsと同じjenkinsにする
Redmine(rails)実行ユーザをJenkinsにするってことか。

redmineが扱うパス/ファイルオーナーをjenkinsにする。
結果、passengerを通してredmine(rails)が、redmine/config/environment.rbのオーナーで実行されることになる。

もしくは、redmine/filesとredmine/public/videosオーナーをjenkinsにして
apacheのpassengerオプションで
PassengerUserSwitching off
PassengerDefaultUser jenkins
を追加記述する。

3. Jenkinsをソースからいじって、誰でも書き込めるようにする
これはだめでしょ。

4. ビルド#0を作成する処理をJenkinsジョブにしてリモート実行する
これかなぁ。

1と2は簡単だし普通はこれかなぁと思うけど、システム依存だし保守性(オリジナルやデフォルトを極力変更しない)が落ちてしまうなぁ。3は可能だけど論外。最後の4の方法でやることにした。

hudson-remote-apiを使ってjob作成&実行するrubyスクリプト
/opt/task/prebuild.rb
#!/usr/bin/ruby                                                                                     
# -*- coding: utf-8 -*-                                                                             

require 'hudson-remote-api'

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

def create_estimated_build record_job, record_sec

  # estimated duration                                                                              
  duration = (1000 * record_sec * 2.2).round

  # build datetime                                                                                  
  build_name = Time.now.strftime("%Y-%m-%d_%H-%M-00")

  puts "create_estimated_build: #{record_job} #{record_sec} #{duration} #{build_name}"

  build_dir = "/var/lib/jenkins/jobs/#{record_job}/builds/#{build_name}"
  build_xml = "<build><number>0</number><result>SUCCESS</result><duration>#{duration}</duration></build>"

  # create estimated build                                                                          
  Dir::mkdir(build_dir)
  xml = File.open(build_dir+"/build.xml",'w')
  xml.puts build_xml
  xml.close

  # reload jenkins                                                                                  
  system("curl %s/reload" % Hudson[:url])
end

# MAIN START                                                                                        

abort "no arguments" if ARGV.size<2
job_name = ARGV[0]
rec_time = ARGV[1]
create_estimated_build job_name, rec_time.to_i
build#0として必要最低限の情報を記述したbuild.xmlを今の時間に実行したっていうビルドディレクトリに作成する処理。最後にJenkinsのリロードをcurlを使って実行する。
hudson-remote-apiではreload-APIがなかったので、こんななった。

以前の記事で作成した
/opt/task/video-jobs.rbに以下のメソッドを追加
def remote_estimated_build record_job, record_sec
  puts "remote_estimated_build"

  job_name = "prebuild_#{record_job}"
  job = Hudson::Job.create job_name

  command = "/opt/task/prebuild.rb #{record_job} #{record_sec}"

  config = REXML::Document.new job.config
  element = config.elements['/project/builders'].add_element 'hudson.tasks.Shell'
  element.add_element('command').add_text command
  job.update config.to_s
  job.build
#  job.wait_for_build_to_finish                                                                     
#  job.delete                                                                                       
end

create_jobメソッドの最後に以下の1行を追加
remote_estimated_build jobname, info[:duration]

このcreate_jobはJenkinsジョブとして動いている自動予約スクリプトから呼ばれたり、Redmineフックスクリプトから呼ばれたりする。

出来てしまえば簡単なのだけれど、最初のアプローチで、どうやってJenkinsシステムに分かってもらえるのかをあれこれ調べるのが手間がかかったかな。

実際に活用してみると、録画処理が後どれくらいで終わるのかが視覚的に分かるようになったので、「なんか走ってるなぁ」じゃなくて「後60分くらいかぁ」ってなるので気分がすっきりする。

2012/08/11

Jenkinsジョブ開始からの時間調整

Redmineのプラグイン対応でチケット操作で即時予約実行ができるようになったので
Jenkins側で回すバックエンドスクリプトを頻繁に回す必要がなくなった。

この辺のタスク整理をしていたら、さほど注視してなかった録画が開始されるまでのタイムラグが気になってきた。この辺の対応をやっておくと何かと便利そうなのと、簡単に対応出来そうなので、スクリプト整理と同時にやってしまおう。今回はそこら辺の話です。

reserve2.rb廃止
以前の記事で作ったジョブ登録スクリプトを一時間ごとに実行していたけど、不要になった。

クエリー予約へ統合
自動予約はクエリーからの予約だけで良くなったので、以前の記事で作ったquery.rbにジョブ登録までの機能を統合した。

録画ジョブは録画1分前に起動
Jenkinsジョブが実際の起動する時間を観測すると指定時間より28秒程度遅く起動しているようだ。どうしてこんなにずれるの?という深いところは置いておいて、
起動が思った以上に指定時間より遅いので、1分前に起動して実際の録画時間になるまでウエイトするような処理を加えることにした。
その時間を使って録画開始までの前処理をちょっと入れたり、どうせなら便利に使おう。ざっくり30秒くらいは別の用途に使えるはずだ。

Jenkinsの待機時間設定
Jenkinsでジョブごとに秒指定は出来ないと書いたが、ジョブ起動から何秒待つかという待機時間を秒設定することが出きる。が、ジョブ起動からの相対値なので今回のような用途には向かない。さらに、これをジョブに設定していない場合、Jenkinsのシステム設定の待機時間が使われてしまうので、システムの待機時間を0にしておくか、録画ジョブのテンプレートに0を設定しておく。念のため両方共0設定。

時刻同期設定
以前の記事に追記したntpの設定もしっかりやっておく。手持ちの電波時計と比べてもずれは無いみたいだ。

時間待ちをする ready.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'active_record'

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

class Issue < ActiveRecord::Base
end
class CustomValues < ActiveRecord::Base
end

abort "no arguments" if ARGV.size==0
issue = Issue.find(ARGV[0])
abort "no ticket" if issue.nil?

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

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

wait_time = (start_time - Time.now) - 4 # 4秒前に録画開始(外から設定できるといいかな)

# 開始待ち
if wait_time > 1
  p 'waiting...%d secs' % wait_time
  sleep( wait_time )
end
ウエイトして時間調整する前処理で、チケットのトラッカーを予約から録画にする程度のことをやっている。いずれ似たような事前処理が増えていくかも。
このready.rbが終わって次のrecrd2.shで録画が開始されるまでの時間が4秒くらいかかる。そのために4秒前に終わるように調整している。

録画ジョブの実行シェルは3つになった
ready.rb ジョブ起動した時に最初に動く
record2.sh 録画を行う
record2.rb チケットへファイルのアタッチをする
3つのシェルコマンドが登録できるように'record_0'ジョブテンプレートも直す。

対応したクエリー予約スクリプト query2.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-

require 'active_record'
require 'rest_client'
require 'hudson-remote-api'

key = 'api-key'
api = "http://localhost/projects/%s/issues.xml?key="+key+"&query_id="

# レコード
class Issue < ActiveRecord::Base
end
class CustomValues < ActiveRecord::Base
end
class Query < ActiveRecord::Base
end
class Project < ActiveRecord::Base
end

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

# project識別名を合成
api = api % Project.first(1)[0].identifier.to_s

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

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  config = REXML::Document.new(Hudson::Job.get("record_0").config)

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

  # 時間設定
  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.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# 予約クエリー検索
reserve = []
queries = Query.find :all, :conditions => { :project_id =>1, :is_public =>1 }
queries.each {|query|

  # REST-APIでクエリー実行
  doc = REXML::Document.new RestClient.get api+query.id.to_s

  # REXMLでトラバース
  doc.elements.each('issues/issue') {|issue|
    if issue.elements['tracker'].attributes['id'].to_i == 1 &&
        issue.elements['status'].attributes['id'].to_i == 1
      # 予約チケットID収集
      reserve << issue.elements['id'].text.to_i
    end
  }
}

Issue.find(reserve).each {|issue|
  channel = CustomValues.first( :conditions => {
                                  :customized_id => issue.id,
                                  :custom_field_id => 2}).value.to_i
  time = CustomValues.first( :conditions => {
                               :customized_id => issue.id,
                               :custom_field_id => 1}).value.to_i

  # 1分前にジョブがスタートするように
  datetime = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) ) - 60

  info = {
    # チケット番号
    :issue_id => issue.id,
    # 録画開始日時
    :datetime => datetime.strftime("%Y%m%d%H%M"),
    # 録画時間
    :duration => issue.estimated_hours.to_i * 60,
    # チャンネル
    :channel => channel,
    # 保存場所
    :output => "/opt/videos/%d" % issue.id
  }

  # ジョブ作成
  if create_job info
    # 実行中
    issue.status_id = 2
  end

  # 予約トラッカー
  issue.tracker_id = 2
  issue.save

  puts "ID:%d [%s]" % [issue.id, issue.subject]
}


以前の記事で作ったチケット操作での予約処理の方もジョブ登録機能を持つので直す。

対応したフックスクリプト video_hooks.rb
# -*- coding: utf-8 -*-

# 接続設定
Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  config = REXML::Document.new(Hudson::Job.get("record_0").config)

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

  # 時間設定
  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.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# ジョブ削除
def delete_job id

  job = Hudson::Job.get "record_#{id}"
  if !job.nil?
    job.delete
    return true
  end
  return false
end

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
    # ジョブ削除して、録画キャンセルチケットに
    if delete_job issue.id
      issue.status_id = 5
      issue.tracker_id = 1
    end
  end

  # 予約?
  def do_reserve issue

    time = issue.custom_field_values[0].to_s
    channel = issue.custom_field_values[1].to_s

    # 1分前にジョブがスタートするように
    datetime = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) ) + 60
    duration = issue.estimated_hours.to_i * 60

    # 時間調整
    nowtime = Time.now
    if datetime < nowtime
      if datetime + duration < nowtime
        # 終わってる・・・
        issue.tracker_id = 1
        return
      else
        # 時間が過ぎてる!後半だけでも!
        duration = duration - (nowtime - datetime) - 60
        datetime = nowtime + 60
      end
    end

    info = {
      # チケット番号
      :issue_id => issue.id,
      # 録画開始日時
      :datetime => datetime.strftime("%Y%m%d%H%M"),
      # 録画時間
      :duration => duration,
      # チャンネル
      :channel => channel.to_i,
      # 保存場所
      :output => "/opt/videos/%d" % issue.id
    }

    # ジョブ登録してチケットを実行中に更新
    if create_job info
      issue.status_id = 2
    end
  end

end

create_job関数の部分は共通になったので外部モジュール化ができそうだ。だけど何処に置くと使いやすいかなぁ。redmineプラグインのlibに置く? 外部タスクの所に置く? どっちを$LOAD_PATHに追加してrequireする?
この辺が固まってないんで、今のところはどっちにも記述しておくけど、こういうのがバグの元なんだよね。

駆け込み録画!
この記事を書いているときに突然思い立って追加したのがvideo_hooksスクリプト内の時間調整のところ。もう開始時間は過ぎてしまったんだけど、途中からでも残りを録画したいという予約時間調整をする部分。

ビデオファイル名をチケットIDに変更!
今まではファイル名はYYYYMMDDmmhh.mp4とかだったんだけれど、一分前処理とかしちゃったもんだから、番組情報と実際の時間が合わなくなった。もう思い切ってファイル名=チケット番号ってことにした。そうしたらあちこちシンプルになった。

今までのものもチケットにアタッチされているので、アタッチファイルからファイル名取得してれば、コンパチビリティも問題なさそう。

あちこちにバグがあって、時々変なことになるけど概ね良い感じに録画ができているかな。オリンピック録画チケットも着々と増えている。失敗もあるけど多くの原因はチューナデバイスの競合だ。これはこれで解決しなくては。

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/07/15

番組データ取得をjenkinsで自動化

録画したTSファイルからEPGデータを取得してredmineチケットにする事は出来るようになった。今度はそれを自動化する必要がある。必要はないか。でも毎日お手手で実行とか馬鹿げてるしね。機械的なルーチンワークはシステム化するのが普通だよね。

Jenkins
CIツールであるjenkinsを使って自動化する事にした。
Jenkinsは以前はHudsonと呼ばれていた、Webベースのビルド自動処理サーバーだ。
通常アプリ開発の効率化とかで便利なシステムだが、今回は番組録画システムの一部として使用してみる。


インストール
https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu
に色々書いてありますが、
$ sudo apt-get install jenkins
だけで済んだよ。ずいぶん前は結構苦労したような・・・

セットアップ
ドキュメントルートはredmineになっているんで、http://localhost/jenkinsでアクセスできるように若干の設定が必要だ。

/etc/default/jenkins ファイルの最後のJENKINS_ARGSに"--prefix=/jenkins"を追加
JENKINS_ARGS="~~ --prefix=/jenkins" って感じに。

/etc/apache/sites-enables/redmineのVirtualHost内に以下を追加
# Jekins
ProxyRequests Off
ProxyPreserveHost on

Order deny,allow
Allow from all
ProxyPass http://localhost:8080/jenkins
ProxyPassReverse http://localhost:8080/jenkins

リスタート
$ sudo service jenkins restart
$ sudo service apache2 reload

これでおしまい。

jenkinsユーザをvideoグループに追加
jenkinsをインストールするとjenkinsユーザが作成され、ジョブはjenkinsユーザで実行される。
使用している録画ツール'recfsusb2n'を実行するにはvideoグループに属していなければならないので、jenkinsユーザをvideoグループに追加した。

実行ユーザを自分の都合のいいユーザに変更する事は出来るが、可能な限り標準状態を崩さないようにしている。
カスタマイズや設定変更が多くなると、保守性が乏しくなるしね。

recepg.sh [channel] 60秒録画したTSファイルからEPGをXMLにするスクリプト
#!/bin/sh
recfsusb2n -b $1 60 $1.ts
epgdump $1 $1.ts $1.xml

entry.rb [channel] xmlでの番組情報からredmineへ番組チケット登録するスクリプト
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'rubygems'
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

class IssueCategories < ActiveRecord::Base
end

class CustomValues < ActiveRecord::Base
end

class CustomFields < ActiveRecord::Base
  # 使いたいな
end

$entry_count = 0
$ch_name = ''

#
# チケット登録
#
def entry_issue date, start, duration, title, desc, category, channel
  #  p date, start, duration, title, desc, category, channel
  #  return

  # カテゴリ
  cate = IssueCategories.first( :conditions =>
                                {:project_id => 1, :name => category } )
  cate = IssueCategories.create( :project_id => 1, :name => category ) if cate.nil?

  # マッチするチケットを検索
  if Issue.count( :conditions => { :start_date => date, :subject => title }) > 0
    return #登録済み
  end

  # チケット作成
  issue = Issue.create( :project_id => 1, #'番組プログラム'
                        :tracker_id => 1, #'番組'
                        :lft => 1,
                        :rgt => 2,
                        :status_id => 1,
                        :priority_id => 2,
                        :done_ratio => 0,
                        :author_id => 1,
                        :subject => title,
                        :description => desc,
                        :start_date => date,
                        :due_date => date,
                        :estimated_hours => duration,
                        :category_id => cate.id )

  # root_id 更新
  # NULLのままだとRedmineからのチケット更新でクラッシュする
  # 親がない場合は自分のIDを入れるらしい。自分? 更新しか無い?
  issue.root_id = issue.id

  if issue.save
    # 開始時間
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 1, #'開始時間'
                         :value => start )
    # チャンネル
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 2, #'チャンネル'
                         :value => channel )
  end

  $entry_count = $entry_count + 1

end # entry_issue

#
# TVプログラム登録
#
def entry_program prog
  start = Time.parse( prog['start'] )
  stop = Time.parse( prog['stop'] )
  duration = (stop - start) / 60

  date = start.to_date
  start = start.strftime("%H%M").to_i

#  channel = prog['channel'].gsub(/ontv.*/,'') + $ch_name
  channel = prog['channel']+ '.'+$ch_name
  category = prog['category'].join(".")
  title = prog['title']
  desc = prog['desc']
  desc = "" if desc.kind_of? Hash

  # 5分以下の短い番組は登録しない :todo: DBに設定できると良い
  return if duration <= 5

  # デスクリプションが無い番組は登録しない
  return if desc == ''

  title.gsub!(/(【二】|【デ】|【S】|【字】)/,"")

  entry_issue date, start, duration, title, desc, category, channel
end

#
# メイン
#
xml_file = ARGV.shift + '.xml'
h = Hash.from_xml( open(xml_file).read )["tv"]

h.each_pair {|key,value|
  if key == 'channel'
    $ch_name = value['display_name']
  elsif key == 'programme'
    value.each {|prog| entry_program prog}
  end
}

puts "%d entried" % $entry_count

ここは結構苦労しましたよ。前記事でも載せたけど、流れで同じものを置きました。

基本この2つのスクリプトを使って自動化を行う。
ジョブ1:recepg.sh [ch1]
ジョブ2:recepg.sh [ch2]
 ジョブ3:entry.rb [ch1]
  ジョブ4:recepg.sh [ch3]

  ジョブ5:entry.rb [ch2]
   ・・・
   ジョブN:entry.rb [chN]

最初のジョブ1のrecepgをトリガーにして処理が終わったら次のチャンネルのrecepgとチケット登録のentry.rbの2つを実行するように順に処理が行われるようにした。
recepg.shとentry.rbを全部1つのジョブにしなかったのは、recepgの録画処理中に並列処理でチケット登録を行う方が短時間で終わるから。

こういう同期処理とか平行処理とかをスクリプトプログラムで書かなくてもJenkinsで設定すればプログラムレスで自動化できるところがいいね。

1つのジョブの処理を単純化する事が出来るし、実行状況もブラウザから分かりやすく管理できる。タスクの見える化が簡単。タスクのログも残るので、失敗しているときの状況も把握しやすい。

早速失敗が・・・
egpdump がコアダンプで失敗することがある。
以前書いたrecfsusb2nのffmpeg対応処理で行うダミーループカウントを4回に増やしてリビルドした。
安定したようだ。

WANアドレス登録処理もJenkinsでやる事にした
Jenkinsが使いやすいので、以前記事にもしたWANアドレス管理サーバーへのコミット処理もJenkinsで行う事にした。crontabの方をコメントアウトして実行しないように変更した。crontabからのメールが溜まらなくなったのがうれしい。
gauth.py (jenkins対応)
#!/usr/bin/python
# -*- coding: utf-8 -*-

from subprocess import *
from os import path

mail = '????@gmail.com'
password = '????'
app = '????'

app_url = 'http://'+app+'.appspot.com/'
cookie_file = '.google_myapp_cookie'
if path.expanduser("~")[:5]=='/home':
    cookie_file = path.expanduser("~")+'/'+cookie_file
#print cookie_file

def login():
    try:
        # API認証キーを取得
        print 'Get Auth-Key...\n'
        auth = Popen(['curl','-f','-s',
                      'https://www.google.com/accounts/ClientLogin',
                      '-d','accountType=HOSTED_OR_GOOGLE',
                      '-d','Email='  + mail,
                      '-d','Passwd=' + password,
                      '-d','source=' + app,
                      '-d','service=ah'],
                     stdout=PIPE).communicate()[0]
        # ログインしてクッキー取得
        print 'Login...\n'
        login = Popen(['curl','-c','-',
                       app_url+'_ah/login?auth='+auth[ auth.rindex("Auth=")+5:-1]],
                      stdout=PIPE).communicate()[0]
        cookie = login[login.rindex("ACSID")+6:-1]

        # クッキーの保存
        f = open(cookie_file,'w')
        f.write(cookie)
        f.close()
        return cookie
    except:
        return ""

def get(controller):
    for i in range(2):
        try:
            f = open(cookie_file)
            cookie = f.read()
            f.close()
            ret = Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
                        stdout=PIPE).communicate()[0]
            if ret.find('COMMITED')>0:
                return 0
            else:
                raise
        except:
            cookie = login()
    raise RuntimeError('Login Failure')


jenkinsに失敗が伝わるようにログイン失敗したらpythonも失敗するようにgauth.pyを変更した。
jenkinsから実行された場合cookieファイルがjenkinsのワークスペースに保存されるように変更。

さて、これで日々の番組がredmineチケットに自動登録されるところまでが出来た。
次は録画予約かな。