aboutsummaryrefslogtreecommitdiff
blob: 22001ddd67818890e0005fcce4810432356cb193 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#!/usr/bin/env ruby
# Target 2
# written by Alex Legler <a3li@gentoo.org>
# 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 = {
				:auth_cache => true,
				: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('-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("Using #{metadata[:package]}") unless $opts[:quiet]
			#puts metadata.inspect

			best_version = find_best_version(metadata, slot, version)
			i("Target version: #{best_version}") unless $opts[:quiet]

			# Cover a custom version string that is not there in the local tree
			if metadata[:keywords].include? best_version
				already_stable = filter_unstable(metadata[:keywords][best_version]) - NOSTABLE_ARCHES
			else
				already_stable = []
			end

			need_stable = 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" % metadata[:stable_arches].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
	end
end

if __FILE__ == $0
	include GenSec::Target
	main(ARGV)
end