#!/usr/bin/env ruby # Target 2 # written by Alex Legler # dependencies: app-portage/gentoolkit, dev-lang/ruby[ssl], dev-ruby/highline # vim: set sw=2 ts=2: require 'optparse' require 'highline' require 'fileutils' require 'xmlrpc/client' 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 GenSec module Target # These architectures don't stabilize packages NOSTABLE_ARCHES = ['mips'] def main(argv) $opts = { :debug => false, :force => false, :liaisons => false, :username => nil, :prestable => false, :quiet => false } $ui = HighLine.new bug = nil version = nil slot = nil optparse = OptionParser.new do |opts| opts.on('-b', '--bug BUGNO', 'The number of the bug to change') do |b| bug = Integer(b) end opts.on('-v', '--version VERSION', 'Use this version as stabilization target') do |v| version = v end opts.on('-s', '--slot SLOT', 'Use ebuilds from this slot to find the best ebuild') do |s| slot = s end opts.on('-l', '--liaisons', 'CC the arch liaisons instead of arch teams') do $opts[:liaisons] = true end opts.on('-p', '--prestable', 'Use prestabling instructions') do $opts[:prestable] = true end opts.on('-u', '--username USERNAME', 'Use this user name to log in at Bugzilla') do |username| $opts[:username] = username end opts.on_tail('--debug', 'Print debug output.') do $opts[:debug] = true end opts.on_tail('-f', '--force', 'Force the operation. Disables asking for confirmation and version checks.') do $opts[:force] = true end opts.on_tail('-q', '--quiet', 'Be less noisy') do $opts[:quiet] = true end opts.on_tail('-h', '--help', 'Display this screen') do puts opts exit end end optparse.banner = "Usage: #{$0} [options] [package]\n\nAvailable options:\n" cmd_options = optparse.parse!(argv) if argv.length > 0 package = argv.shift else package = Dir.pwd.split('/').last(2).join('/') end metadata = get_metadata(package) do_package(metadata, bug, version, slot) end def do_package(metadata, bug, version, slot) if metadata[:package] == nil or metadata[:package] == '' e("No package found.") end i("Package: #{$ui.color(metadata[:package], :green)}") unless $opts[:quiet] if $opts[:debug] require 'pp' pp metadata end best_version = find_best_version(metadata, slot, version) i("Target version: #{$ui.color(best_version, :green)}") unless $opts[:quiet] # Cover a custom version string that is not there in the local tree if metadata[:versions].include? best_version already_stable = filter_unstable(metadata[:keywords][best_version]) - NOSTABLE_ARCHES else w($ui.color("Warning: Target version not found. Proceed with care.", :yellow)) already_stable = [] end need_stable = filter_negative_keywords(metadata[:stable_arches] - NOSTABLE_ARCHES) i("Arches this package was ever stable on: #{$ui.color(need_stable.join(', '), :red, :bold)}") unless $opts[:quiet] if already_stable.length > 0 i("Target version is already stable on: #{$ui.color(already_stable.join(', '), :green, :bold)}") unless $opts[:quiet] end if $opts[:prestable] msg = "Arch Security Liaisons, please test the attached ebuild and report it stable on this bug.\n" elsif $opts[:liaisons] and not $opts[:prestable] msg = "Arch Security Liaisons, please test and mark stable:\n" else msg = "Arches, please test and mark stable:\n" end if not $opts[:prestable] msg += "=%s-%s\n" % [metadata[:package], best_version] end msg += "Target keywords : \"%s\"\n" % need_stable.join(' ') if already_stable.length > 0 and not $opts[:prestable] msg += "Already stable : \"%s\"\n" % (already_stable.join(' ')) msg += "Missing keywords: \"%s\"\n" % (metadata[:stable_arches] - already_stable).join(' ') end puts puts msg puts if $opts[:liaisons] require File.join(File.dirname(__FILE__), 'liaisons') cc_list = need_stable.map {|arch| @liaisons[arch]}.flatten.map {|liaison| "#{liaison}@gentoo.org"} else cc_list = need_stable.map {|arch| "#{arch}@gentoo.org" } end puts "CC: %s" % cc_list.join(',') exit if bug == nil bugi = bug_info(bug) new_whiteboard = update_whiteboard(bugi['whiteboard']) puts "Whiteboard: '%s' -> '%s'" % [bugi['whiteboard'], new_whiteboard] puts if $opts[:force] or $ui.agree('Continue? (yes/no)') update_bug(bug, new_whiteboard, cc_list, msg) end end # Collects metadata information from equery meta def get_metadata(ebuild = Dir.pwd.split('/').last(2).join('/')) keywords = IO.popen("equery --no-color --no-pipe meta --keywords #{ebuild}") result = {:slots => {}, :keywords => {}, :stable_arches => [], :versions => []} keywords.lines.each do |line| if line =~ /^ \* (\S*?)\/(\S*?) \[([^\]]*)\]$/ result[:package] = "#{$1}/#{$2}" result[:repo] = $3 next end if line =~ /^(.*?):(.*?):(.*?)$/ version, slot, kws = $1, $2, $3 result[:versions] << version result[:slots][slot] = [] unless result[:slots].include? slot result[:slots][slot] << version result[:keywords][version] = [] kws.strip.split(' ').each do |arch| result[:keywords][version] << arch if arch =~ /^[^~]*$/ result[:stable_arches] << arch end end result[:keywords][version].sort! next end raise RuntimeError, "Invalid line in equery output. Aborting." end result[:stable_arches].uniq! result[:stable_arches].sort! result end # Tries to find the best version following the needed specification def find_best_version(metadata, slot, version) if slot == nil and version == nil return metadata[:versions].reject {|item| item =~ /^9999/}.last elsif slot == nil return version else if version == nil return metadata[:slots][slot].reject {|item| item =~ /^9999/}.last elsif metadata[:slots][slot].include?(version) return version else return false end end end def update_whiteboard(old_wb) old_wb.gsub(/(ebuild\+?|upstream\+?|stable)\??/, 'stable').gsub(/stable\/stable/, 'stable') end def update_bug(bug, whiteboard, cc_list, comment) i("Updating bug #{bug}...") client = xmlrpc_client did_retry = false begin result = client.call('Bug.update', { 'ids' => [Integer(bug)], 'whiteboard' => whiteboard, 'cc' => {'add' => cc_list}, 'keywords' => {'add' => 'STABLEREQ'}, 'status' => 'IN_PROGRESS', 'comment' => {'body' => comment} }) i("done!") return true rescue XMLRPC::FaultException => e if did_retry e "Failure updating bug information: #{e.message}" return false end if e.faultCode == 410 log_in did_retry = true retry else e "Failure updating bug information: #{e.message}" end end end def bug_info(bugno) client = xmlrpc_client did_retry = false begin result = client.call('Bug.get', {'ids' => [Integer(bugno)]}) result['bugs'].first rescue XMLRPC::FaultException => e if did_retry e "Failure reading bug information: #{e.message}" return false end if e.faultCode == 410 log_in did_retry = true retry else e "Failure reading bug information: #{e.message}" end end end def log_in client = xmlrpc_client if $opts[:username] == nil user = $ui.ask("Bugzilla login: ") else user = $opts[:username] end password = $ui.ask("Password: ") {|q| q.echo = false} begin i("Logging in...") result = client.call('User.login', { 'login' => user, 'password' => password }) cookie_file = File.join(ENV['HOME'], '.gensec-target-auth') FileUtils.rm(cookie_file) if File.exist?(cookie_file) FileUtils.touch(cookie_file) File.chmod(0600, cookie_file) File.open(cookie_file, 'w') {|f| f.write client.cookie } return true rescue XMLRPC::FaultException => e e "Failure logging in: #{e.message}" return false end end def xmlrpc_client client = XMLRPC::Client.new('bugs.gentoo.org', '/xmlrpc.cgi', 443, nil, nil, nil, nil, true) client.http_header_extra = {'User-Agent' => "Target/2.0 (arch CC tool; http://security.gentoo.org/)"} cookie_file = File.join(ENV['HOME'], '.gensec-target-auth') if File.readable? cookie_file client.cookie = File.read(cookie_file) end client end # Output and misc methods def i(str) $ui.say($ui.color(" * ", :green, :bold) + str) end def w(str) $ui.say($ui.color(" * ", :yellow, :bold) + str) end def e(str) $ui.say($ui.color(" * ", :red, :bold) + str) exit 1 end def filter_unstable(ary) ary.reject {|item| item =~ /^[~-]/} end def filter_negative_keywords(ary) ary.reject {|item| item =~ /^[-]/} end end end if __FILE__ == $0 include GenSec::Target main(ARGV) end