Skip to content

Instantly share code, notes, and snippets.

@matthijsgroen
Created February 29, 2012 14:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save matthijsgroen/1941325 to your computer and use it in GitHub Desktop.
Save matthijsgroen/1941325 to your computer and use it in GitHub Desktop.
Fapi
#!/usr/bin/ruby
require 'net/http'
require 'net/https'
require 'base64'
require 'uri'
require 'yaml'
require 'cgi'
class Net::HTTP
alias_method :old_initialize, :initialize
def initialize(*args)
old_initialize(*args)
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
end
module Configuration
def config_file
"#{ENV['HOME']}/.fapirc"
end
def init args
return false unless args.length == 2
puts "storing credentials"
update_configuration(:username => args[0], :password => args[1])
true
end
def configuration
return @configuration if @configuration
@configuration = {}
@configuration = YAML.load_file(config_file) if File.exists? config_file
@configuration
end
def update_configuration attributes
@configuration = configuration.merge(attributes)
File.open(config_file, "w") { |f| f.write(@configuration.to_yaml) }
end
def map args
return false unless args.length == 2
configuration[:mappings] ||= {}
configuration[:mappings][repository] ||= {}
configuration[:mappings][repository][args[0]] = args[1]
update_configuration({})
puts %{"#{args[0]}" => "#{args[1]}"}
true
end
def control args
return false unless args.length == 1
update_configuration(:control_center => args[0])
puts %{control center set to: #{args[0]}}
true
end
def softfab_configuration
return @softfab_configuration if @softfab_configuration
return nil unless configuration[:mappings] and configuration[:mappings][repository]
mappings = configuration[:mappings][repository]
mappings.each do |pattern, configuration_name|
match_pattern = pattern.gsub("*", "(.*)")
if branch.match(match_pattern)
@softfab_configuration = configuration_name
end
end
@softfab_configuration
end
end
module Watches
def watch_file
"#{ENV['HOME']}/.fapi-watch"
end
def watch_list
watch_list = []
watch_list = YAML.load_file(watch_file)[:list] if File.exists? watch_file
watch_list ||= []
watch_list
end
def write_watch_list list
File.open(watch_file, "w") { |f| f.write({ :list => list }.to_yaml) }
end
def add_to_watch_list job_id
list = watch_list
list << { "job" => job_id, "repository" => repository, "branch" => branch }
write_watch_list list
end
end
module Git
def remote
@remote ||= `git remote -v | grep \\(push\\)`.split[1]
end
def repository
@repository ||= remote.split("/")[-1]
end
def branch
@branch ||= `git branch | grep \\*`.split[1]
end
def branch_changes
@branch_changes ||= `git status --short --porcelain`.split("\n")
end
end
module SoftfabCommunication
def form_encode data
params = []
data.each do |field, value|
params.push "#{CGI::escape field}=#{CGI::escape value}"
end
params.join "&"
end
def softfab_login
uri = URI.parse("#{configuration[:control_center]}/Login")
headers = {
'Content-Type' => "application/x-www-form-urlencoded"
}
data = {
"loginname" => configuration[:username],
"loginpass" => configuration[:password],
"url" => "Home"
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
res = http.post(uri.path, form_encode(data), headers)
res.response['set-cookie']
end
def fetch_configuration_options(config, session)
uri = URI.parse("#{configuration[:control_center]}/BatchExecute")
headers = {
'Cookie' => session
}
data = {
"sel" => config
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
res = http.get("#{uri.path}?#{form_encode data}", headers)
form_contents = /<form([^>]+)>(.*)<\/form>/.match(res.body.gsub("\n", ""))[2]
results = {}
form_contents.scan(/<input([^>]*)>/) do |hit|
name = /name="([^"]*)"/.match(hit[0])[1]
value = /value="([^"]*)"/.match(hit[0])[1]
results[name] = value
end
results
end
def parse_job_status xml_contents
xml_contents = xml_contents.gsub("\\\"", "\"")
task_results = []
task_exec_states = []
xml_contents.scan(/<task([^>]+)>/) do |task|
state = /execstate="([^"]*)"/.match(task[0])
task_exec_states.push(state[1]) if state
result = /result="([^"]*)"/.match(task[0])
task_results.push(result[1]) if result
end
task_results.uniq!
task_exec_states.uniq!
unfinished_states = (task_exec_states - ["done", "cancelled"])
return "pending" unless unfinished_states.empty?
return "ok" if task_results == ["ok"]
return "cancelled" if task_results.include? "cancelled"
return "error" if task_results.include? "error"
return "warning" if task_results.include? "warning"
"pending"
end
def execute_softfab_configuration(config, session)
uri = URI.parse("#{configuration[:control_center]}/BatchExecute")
headers = {
'Cookie' => session,
'Content-Type' => "application/x-www-form-urlencoded"
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
res = http.post(uri.path, form_encode(config), headers)
res.response['location']
end
def fetch_running_jobs username, session, watch_list
uri = URI.parse("#{configuration[:control_center]}/UserDetails")
headers = {
'Cookie' => session
}
data = {
"user" => username
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
res = http.get("#{uri.path}?#{form_encode data}", headers)
jobs = watch_list
included_jobs = watch_list.map { |e| e["job"] }
job_match = /<tbody id="jobs">(.*)<\/tbody>/.match(res.body.gsub("\n", ""))
puts "no jobs found for user: #{username}" unless job_match
if job_match and recent_jobs = job_match[1]
recent_jobs.split("<tr").each do |row|
row.scan(/class="busy">.+<a href="ShowReport([^<]*)<\/a>/) do |hit|
job = /jobId=([^"\\]*)/.match(hit[0])[1]
unless included_jobs.include? job
name = />(.*)/.match(hit[0])[1]
new_watch = {"job" => job, "repository" => name, "branch" => username }
jobs << new_watch
puts "added watch for: #{name} - #{job}"
end
end
end
end
jobs
end
end
module Notification
def notify_status watch, status
print Time.now.strftime "[%a %d %b %H:%M] "
puts "#{status}: #{watch["repository"]} - #{watch["branch"]}"
icon = case status
when "ok" then "laugh"
when "cancelled" then "surprise"
when "error" then "sick"
when "warning" then "crying"
end
icon_path = "/usr/share/icons/gnome/48x48/emotes/face-#{icon}.png"
# if linux
`notify-send "#{status} - #{watch["branch"]}" "#{watch["repository"]}" --icon=#{icon_path}`
play_sound status
end
def play_sound status
`paplay #{sound_path status}` if File.exists?(sound_path status)
end
def sound_path status
path = '/usr/share/sounds/freedesktop/stereo/'
path + {
"ok" => 'complete.oga',
"cancelled" => 'message.oga',
"error" => 'suspend-error.oga',
"warning" => 'suspend-error.oga'
}[status]
end
end
module Commands
def push args
unless softfab_configuration
puts %{could not find configuration for "#{repository}/#{branch}"}
return true
end
flags = args.collect { |flag| flag["--"] ? flag.gsub("--", "") : nil }.compact
if flags.include? "force"
unless branch_changes.empty?
puts "Your branch has the current uncommitted changes:"
branch_changes.each { |change| puts change }
return true
end
`git push origin #{branch}`
end
print Time.now.strftime "[%a %d %b %H:%M]"
puts %{triggering #{configuration[:control_center]} to execute "#{softfab_configuration}" for #{repository}/#{branch}}
session = softfab_login
configuration_options = fetch_configuration_options(softfab_configuration, session)
configuration_options.each do |key, value|
configuration_options[key] = branch if key["SCM_BRANCH"]
end
configuration_options["action"] = "Execute"
job_run_location = execute_softfab_configuration(configuration_options, session)
job_id = /jobId=([^&]+)/.match(job_run_location)[1]
add_to_watch_list job_id
puts "job-id: #{job_id}"
true
end
def serve args
flags = args.collect { |flag| flag["--"] ? flag.gsub("--", "") : nil }.compact
watch_all = flags.include? "auto-build"
print Time.now.strftime "[%a %d %b %H:%M] "
puts "watching executed jobs. Press Ctrl+C to exit"
uri = URI.parse("#{configuration[:control_center]}/GetJobInfo")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == "https"
headers = {
"Authorization" => "Basic #{Base64.encode64("#{configuration[:username]}:#{configuration[:password]}")}"
}
session = watch_all ? softfab_login : nil
while true
begin
keep_watching = []
watch_list.each do |watch|
result = http.get("#{uri.path}?jobId=#{watch["job"]}", headers)
job_status = parse_job_status result.body
if job_status == "pending"
keep_watching << watch
else
notify_status watch, job_status
end
end
if watch_all
keep_watching = fetch_running_jobs "svntrigger", session, keep_watching
end
write_watch_list keep_watching
sleep 5
rescue Errno::ENETUNREACH
sleep 60
end
end
rescue Interrupt => e
puts "exiting..."
return true
end
end
include Configuration, Git, Watches, Commands, SoftfabCommunication, Notification
command = ARGV.shift
known_commands = {
"init" => "initializes the config file in your home folder. usage: init username password",
"push" => "pushes the current branch as feature branch run to softfab. Use --force to push your latest commits to the server before running CI",
"map" => "link a branch pattern to a softfab configuration for the current repository. usage: map pattern-* configuration",
"control" => "set the url to the softfab control center. usage: control https://www.project.domain.com/softfab",
"serve" => "watch pushed jobs and notify on result. Use --auto-build to watch auto-build running jobs"
}
show_help = true
show_help = false if known_commands.has_key? command and send command.to_sym, ARGV
if show_help
puts "commands available:"
known_commands.keys.sort.each { |key| puts " #{"%-10s" % key}\t#{known_commands[key]}" }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment