improved_jenkins_client 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/jenkinscli +5 -0
- data/improved_jenkins_client.gemspec +34 -0
- data/java_deps/jenkins-cli.jar +0 -0
- data/lib/improved_jenkins_client/build_queue.rb +262 -0
- data/lib/improved_jenkins_client/cli/base.rb +84 -0
- data/lib/improved_jenkins_client/cli/helper.rb +61 -0
- data/lib/improved_jenkins_client/cli/job.rb +133 -0
- data/lib/improved_jenkins_client/cli/node.rb +97 -0
- data/lib/improved_jenkins_client/cli/system.rb +65 -0
- data/lib/improved_jenkins_client/client.rb +855 -0
- data/lib/improved_jenkins_client/exceptions.rb +246 -0
- data/lib/improved_jenkins_client/job.rb +1966 -0
- data/lib/improved_jenkins_client/node.rb +353 -0
- data/lib/improved_jenkins_client/plugin_manager.rb +460 -0
- data/lib/improved_jenkins_client/plugin_settings/base.rb +11 -0
- data/lib/improved_jenkins_client/plugin_settings/collection.rb +39 -0
- data/lib/improved_jenkins_client/plugin_settings/hipchat.rb +53 -0
- data/lib/improved_jenkins_client/plugin_settings/workspace_cleanup.rb +35 -0
- data/lib/improved_jenkins_client/root.rb +67 -0
- data/lib/improved_jenkins_client/system.rb +134 -0
- data/lib/improved_jenkins_client/urihelper.rb +18 -0
- data/lib/improved_jenkins_client/user.rb +131 -0
- data/lib/improved_jenkins_client/version.rb +36 -0
- data/lib/improved_jenkins_client/view.rb +313 -0
- data/lib/improved_jenkins_client.rb +52 -0
- metadata +172 -0
@@ -0,0 +1,855 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2012-2013 Kannan Manickam <arangamani.kannan@gmail.com>
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'json'
|
25
|
+
require 'net/http'
|
26
|
+
require 'net/https'
|
27
|
+
require 'nokogiri'
|
28
|
+
require 'base64'
|
29
|
+
require 'mixlib/shellout'
|
30
|
+
require 'uri'
|
31
|
+
require 'logger'
|
32
|
+
require 'socksify/http'
|
33
|
+
require 'open-uri'
|
34
|
+
|
35
|
+
# The main module that contains the Client class and all subclasses that
|
36
|
+
# communicate with the Jenkins's Remote Access API.
|
37
|
+
#
|
38
|
+
module JenkinsApi
|
39
|
+
# This is the client class that acts as the bridge between the subclasses and
|
40
|
+
# Jenkins. This class contains methods that performs GET and POST requests
|
41
|
+
# for various operations.
|
42
|
+
#
|
43
|
+
class Client
|
44
|
+
attr_accessor :timeout, :logger
|
45
|
+
# Default port to be used to connect to Jenkins
|
46
|
+
DEFAULT_SERVER_PORT = 8080
|
47
|
+
# Default timeout in seconds to be used while performing operations
|
48
|
+
DEFAULT_TIMEOUT = 120
|
49
|
+
DEFAULT_HTTP_OPEN_TIMEOUT = 10
|
50
|
+
DEFAULT_HTTP_READ_TIMEOUT = 120
|
51
|
+
# Parameters that are permitted as options while initializing the client
|
52
|
+
VALID_PARAMS = [
|
53
|
+
"server_url",
|
54
|
+
"server_ip",
|
55
|
+
"server_port",
|
56
|
+
"proxy_ip",
|
57
|
+
"proxy_port",
|
58
|
+
"proxy_protocol",
|
59
|
+
"jenkins_path",
|
60
|
+
"username",
|
61
|
+
"password",
|
62
|
+
"password_base64",
|
63
|
+
"logger",
|
64
|
+
"log_location",
|
65
|
+
"log_level",
|
66
|
+
"timeout",
|
67
|
+
"http_open_timeout",
|
68
|
+
"http_read_timeout",
|
69
|
+
"ssl",
|
70
|
+
"pkcs_file_path",
|
71
|
+
"pass_phrase",
|
72
|
+
"ca_file",
|
73
|
+
"follow_redirects",
|
74
|
+
"identity_file",
|
75
|
+
"cookies"
|
76
|
+
].freeze
|
77
|
+
|
78
|
+
# Initialize a Client object with Jenkins CI server credentials
|
79
|
+
#
|
80
|
+
# @param args [Hash] Arguments to connect to Jenkins server
|
81
|
+
#
|
82
|
+
# @option args [String] :server_ip the IP address of the Jenkins CI server
|
83
|
+
# @option args [String] :server_port the port on which the Jenkins listens
|
84
|
+
# @option args [String] :server_url the full URL address of the Jenkins CI server (http/https). This can include
|
85
|
+
# username/password. :username/:password options will override any user/pass value in the URL
|
86
|
+
# @option args [String] :username the username used for connecting to the server (optional)
|
87
|
+
# @option args [String] :password the password or API Key for connecting to the CI server (optional)
|
88
|
+
# @option args [String] :password_base64 the password with base64 encoded format for connecting to the CI
|
89
|
+
# server (optional)
|
90
|
+
# @option args [String] :identity_file the priviate key file for Jenkins CLI authentication,
|
91
|
+
# it is used only for executing CLI commands. Also remember to upload the public key to
|
92
|
+
# <Server IP>:<Server Port>/user/<Username>/configure
|
93
|
+
# @option args [String] :proxy_ip the proxy IP address
|
94
|
+
# @option args [String] :proxy_port the proxy port
|
95
|
+
# @option args [String] :proxy_protocol the proxy protocol ('socks' or 'http' (defaults to HTTP)
|
96
|
+
# @option args [String] :jenkins_path ("/") the optional context path for Jenkins
|
97
|
+
# @option args [Boolean] :ssl (false) indicates if Jenkins is accessible over HTTPS
|
98
|
+
# @option args [String] :pkcs_file_path ("/") the optional context path for pfx or p12 binary certificate file
|
99
|
+
# @option args [String] :pass_phrase password for pkcs_file_path certificate file
|
100
|
+
# @option args [String] :ca_file the path to a PEM encoded file containing trusted certificates used to verify peer certificate
|
101
|
+
# @option args [Boolean] :follow_redirects this argument causes the client to follow a redirect (jenkins can
|
102
|
+
# return a 30x when starting a build)
|
103
|
+
# @option args [Fixnum] :timeout (120) This argument sets the timeout for operations that take longer (in seconds)
|
104
|
+
# @option args [Logger] :logger a Logger object, used to override the default logger (optional)
|
105
|
+
# @option args [String] :log_location (STDOUT) the location for the log file
|
106
|
+
# @option args [Fixnum] :log_level (Logger::INFO) The level for messages to be logged. Should be one of:
|
107
|
+
# Logger::DEBUG (0), Logger::INFO (1), Logger::WARN (2), Logger::ERROR (2), Logger::FATAL (3)
|
108
|
+
# @option args [String] :cookies Cookies to be sent with all requests in the format: name=value; name2=value2
|
109
|
+
#
|
110
|
+
# @return [JenkinsApi::Client] a client object to Jenkins API
|
111
|
+
#
|
112
|
+
# @raise [ArgumentError] when required options are not provided.
|
113
|
+
#
|
114
|
+
def initialize(args)
|
115
|
+
args = symbolize_keys(args)
|
116
|
+
args.each do |key, value|
|
117
|
+
if value && VALID_PARAMS.include?(key.to_s)
|
118
|
+
instance_variable_set("@#{key}", value)
|
119
|
+
end
|
120
|
+
end if args.is_a? Hash
|
121
|
+
|
122
|
+
# Server IP or Server URL must be specific
|
123
|
+
unless @server_ip || @server_url
|
124
|
+
raise ArgumentError, "Server IP or Server URL is required to connect" +
|
125
|
+
" to Jenkins"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get info from the server_url, if we got one
|
129
|
+
if @server_url
|
130
|
+
server_uri = URI.parse(@server_url)
|
131
|
+
@server_ip = server_uri.host
|
132
|
+
@server_port = server_uri.port
|
133
|
+
@ssl = server_uri.scheme == "https"
|
134
|
+
@jenkins_path = server_uri.path
|
135
|
+
|
136
|
+
# read username and password from the URL
|
137
|
+
# only set if @username and @password are not already set via explicit options
|
138
|
+
@username ||= server_uri.user
|
139
|
+
@password ||= server_uri.password
|
140
|
+
end
|
141
|
+
|
142
|
+
# Username/password are optional as some jenkins servers do not require
|
143
|
+
# authentication
|
144
|
+
if @username && !(@password || @password_base64)
|
145
|
+
raise ArgumentError, "If username is provided, password is required"
|
146
|
+
end
|
147
|
+
if @proxy_ip.nil? ^ @proxy_port.nil?
|
148
|
+
raise ArgumentError, "Proxy IP and port must both be specified or" +
|
149
|
+
" both left nil"
|
150
|
+
end
|
151
|
+
|
152
|
+
@jenkins_path ||= ""
|
153
|
+
@jenkins_path.gsub!(/\/$/,"") # remove trailing slash if there is one
|
154
|
+
@server_port = DEFAULT_SERVER_PORT unless @server_port
|
155
|
+
@timeout = DEFAULT_TIMEOUT unless @timeout
|
156
|
+
@http_open_timeout = DEFAULT_HTTP_OPEN_TIMEOUT unless @http_open_timeout
|
157
|
+
@http_read_timeout = DEFAULT_HTTP_READ_TIMEOUT unless @http_read_timeout
|
158
|
+
@ssl ||= false
|
159
|
+
@proxy_protocol ||= 'http'
|
160
|
+
|
161
|
+
# Setting log options
|
162
|
+
if @logger
|
163
|
+
raise ArgumentError, "logger parameter must be a Logger object" unless @logger.is_a?(Logger)
|
164
|
+
raise ArgumentError, "log_level should not be set if using custom logger" if @log_level
|
165
|
+
raise ArgumentError, "log_location should not be set if using custom logger" if @log_location
|
166
|
+
else
|
167
|
+
@log_location = STDOUT unless @log_location
|
168
|
+
@log_level = Logger::INFO unless @log_level
|
169
|
+
@logger = Logger.new(@log_location)
|
170
|
+
@logger.level = @log_level
|
171
|
+
end
|
172
|
+
|
173
|
+
# Base64 decode inserts a newline character at the end. As a workaround
|
174
|
+
# added chomp to remove newline characters. I hope nobody uses newline
|
175
|
+
# characters at the end of their passwords :)
|
176
|
+
@password = Base64.decode64(@password_base64).chomp if @password_base64
|
177
|
+
|
178
|
+
# No connections are made to the Jenkins server during initialize to
|
179
|
+
# allow the unit tests to behave normally as mocking is simpler this way.
|
180
|
+
# If this variable is nil, the first POST request will query the API and
|
181
|
+
# populate this variable.
|
182
|
+
@crumbs_enabled = nil
|
183
|
+
# The crumbs hash. Store it so that we don't have to obtain the crumb for
|
184
|
+
# every POST request. It appears that the crumb doesn't change often.
|
185
|
+
@crumb = {}
|
186
|
+
# This is the number of times to refetch the crumb if it ever expires.
|
187
|
+
@crumb_max_retries = 3
|
188
|
+
end
|
189
|
+
|
190
|
+
# Creates an instance to the Job class by passing a reference to self
|
191
|
+
#
|
192
|
+
# @return [JenkinsApi::Client::Job] An object to Job subclass
|
193
|
+
#
|
194
|
+
def job
|
195
|
+
JenkinsApi::Client::Job.new(self)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Creates an instance to the System class by passing a reference to self
|
199
|
+
#
|
200
|
+
# @return [JenkinsApi::Client::System] An object to System subclass
|
201
|
+
#
|
202
|
+
def system
|
203
|
+
JenkinsApi::Client::System.new(self)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Creates an instance to the Node class by passing a reference to self
|
207
|
+
#
|
208
|
+
# @return [JenkinsApi::Client::Node] An object to Node subclass
|
209
|
+
#
|
210
|
+
def node
|
211
|
+
JenkinsApi::Client::Node.new(self)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Creates an instance to the View class by passing a reference to self
|
215
|
+
#
|
216
|
+
# @return [JenkinsApi::Client::View] An object to View subclass
|
217
|
+
#
|
218
|
+
def view
|
219
|
+
JenkinsApi::Client::View.new(self)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Creates an instance to the BuildQueue by passing a reference to self
|
223
|
+
#
|
224
|
+
# @return [JenkinsApi::Client::BuildQueue] An object to BuildQueue subclass
|
225
|
+
#
|
226
|
+
def queue
|
227
|
+
JenkinsApi::Client::BuildQueue.new(self)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Creates an instance to the PluginManager by passing a reference to self
|
231
|
+
#
|
232
|
+
# @return [JenkinsApi::Client::PluginManager] an object to PluginManager
|
233
|
+
# subclass
|
234
|
+
#
|
235
|
+
def plugin
|
236
|
+
JenkinsApi::Client::PluginManager.new(self)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Creates an instance of the User class by passing a reference to self
|
240
|
+
#
|
241
|
+
# @return [JenkinsApi::Client::User] An object of User subclass
|
242
|
+
#
|
243
|
+
def user
|
244
|
+
JenkinsApi::Client::User.new(self)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Creates an instance of the Root class by passing a reference to self
|
248
|
+
#
|
249
|
+
# @return [JenkinsApi::Client::Root] An object of Root subclass
|
250
|
+
#
|
251
|
+
def root
|
252
|
+
JenkinsApi::Client::Root.new(self)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns a string representing the class name
|
256
|
+
#
|
257
|
+
# @return [String] string representation of class name
|
258
|
+
#
|
259
|
+
def to_s
|
260
|
+
"#<JenkinsApi::Client>"
|
261
|
+
end
|
262
|
+
|
263
|
+
# Overrides the inspect method to get rid of the credentials being shown in
|
264
|
+
# the in interactive IRB sessions and also when the `inspect` method is
|
265
|
+
# called. Just print the important variables.
|
266
|
+
#
|
267
|
+
def inspect
|
268
|
+
"#<JenkinsApi::Client:0x#{(self.__id__ * 2).to_s(16)}" +
|
269
|
+
" @ssl=#{@ssl.inspect}," +
|
270
|
+
" @ca_file=#{@ca_file.inspect}," +
|
271
|
+
" @log_location=#{@log_location.inspect}," +
|
272
|
+
" @log_level=#{@log_level.inspect}," +
|
273
|
+
" @crumbs_enabled=#{@crumbs_enabled.inspect}," +
|
274
|
+
" @follow_redirects=#{@follow_redirects.inspect}," +
|
275
|
+
" @jenkins_path=#{@jenkins_path.inspect}," +
|
276
|
+
" @timeout=#{@timeout.inspect}>," +
|
277
|
+
" @http_open_timeout=#{@http_open_timeout.inspect}>," +
|
278
|
+
" @http_read_timeout=#{@http_read_timeout.inspect}>"
|
279
|
+
end
|
280
|
+
|
281
|
+
# Connects to the server and downloads artifacts to a specified location
|
282
|
+
#
|
283
|
+
# @param [String] job_name
|
284
|
+
# @param [String] filename location to save artifact
|
285
|
+
#
|
286
|
+
def get_artifact(job_name,filename)
|
287
|
+
@artifact = job.find_artifact(job_name)
|
288
|
+
response = make_http_request(Net::HTTP::Get.new(@artifact))
|
289
|
+
if response.code == "200"
|
290
|
+
File.write(File.expand_path(filename), response.body)
|
291
|
+
else
|
292
|
+
raise "Couldn't get the artifact"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Connects to the server and download all artifacts of a build to a specified location
|
297
|
+
#
|
298
|
+
# @param [String] job_name
|
299
|
+
# @param [String] dldir location to save artifacts
|
300
|
+
# @param [Integer] build_number optional, defaults to current build
|
301
|
+
# @returns [String, Array] list of retrieved artifacts
|
302
|
+
#
|
303
|
+
def get_artifacts(job_name, dldir, build_number = nil)
|
304
|
+
@artifacts = job.find_artifacts(job_name,build_number)
|
305
|
+
results = []
|
306
|
+
@artifacts.each do |artifact|
|
307
|
+
uri = URI.parse(artifact)
|
308
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
309
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
310
|
+
http.use_ssl = @ssl
|
311
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
312
|
+
request.basic_auth(@username, @password)
|
313
|
+
response = http.request(request)
|
314
|
+
# we want every thing after the last 'build' in the path to become the filename
|
315
|
+
if artifact.include?('/build/')
|
316
|
+
filename = artifact.split("/build/").last.gsub('/','-')
|
317
|
+
else
|
318
|
+
filename = File.basename(artifact)
|
319
|
+
end
|
320
|
+
filename = File.join(dldir, filename)
|
321
|
+
results << filename
|
322
|
+
if response.code == "200"
|
323
|
+
File.write(File.expand_path(filename), response.body)
|
324
|
+
else
|
325
|
+
raise "Couldn't get the artifact #{artifact} for job #{job}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
results
|
329
|
+
end
|
330
|
+
|
331
|
+
# Connects to the Jenkins server, sends the specified request and returns
|
332
|
+
# the response.
|
333
|
+
#
|
334
|
+
# @param [Net::HTTPRequest] request The request object to send
|
335
|
+
# @param [Boolean] follow_redirect whether to follow redirects or not
|
336
|
+
#
|
337
|
+
# @return [Net::HTTPResponse] Response from Jenkins
|
338
|
+
#
|
339
|
+
def make_http_request(request, follow_redirect = @follow_redirects)
|
340
|
+
request.basic_auth @username, @password if @username
|
341
|
+
request['Cookie'] = @cookies if @cookies
|
342
|
+
|
343
|
+
if @proxy_ip
|
344
|
+
case @proxy_protocol
|
345
|
+
when 'http'
|
346
|
+
http = Net::HTTP::Proxy(@proxy_ip, @proxy_port).new(@server_ip, @server_port)
|
347
|
+
when 'socks'
|
348
|
+
http = Net::HTTP::SOCKSProxy(@proxy_ip, @proxy_port).start(@server_ip, @server_port)
|
349
|
+
else
|
350
|
+
raise "unknown proxy protocol: '#{@proxy_protocol}'"
|
351
|
+
end
|
352
|
+
else
|
353
|
+
http = Net::HTTP.new(@server_ip, @server_port)
|
354
|
+
end
|
355
|
+
|
356
|
+
if @ssl && @pkcs_file_path
|
357
|
+
http.use_ssl = true
|
358
|
+
pkcs12 =OpenSSL::PKCS12.new(File.binread(@pkcs_file_path), @pass_phrase!=nil ? @pass_phrase : "")
|
359
|
+
http.cert = pkcs12.certificate
|
360
|
+
http.key = pkcs12.key
|
361
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
362
|
+
elsif @ssl
|
363
|
+
http.use_ssl = true
|
364
|
+
|
365
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
366
|
+
http.ca_file = @ca_file if @ca_file
|
367
|
+
end
|
368
|
+
http.open_timeout = @http_open_timeout
|
369
|
+
http.read_timeout = @http_read_timeout
|
370
|
+
|
371
|
+
response = http.request(request)
|
372
|
+
case response
|
373
|
+
when Net::HTTPRedirection then
|
374
|
+
# If we got a redirect request, follow it (if flag set), but don't
|
375
|
+
# go any deeper (only one redirect supported - don't want to follow
|
376
|
+
# our tail)
|
377
|
+
if follow_redirect
|
378
|
+
redir_uri = URI.parse(response['location'])
|
379
|
+
response = make_http_request(
|
380
|
+
Net::HTTP::Get.new(redir_uri.path, false)
|
381
|
+
)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# Pick out some useful header info before we return
|
386
|
+
@jenkins_version = response['X-Jenkins']
|
387
|
+
@hudson_version = response['X-Hudson']
|
388
|
+
|
389
|
+
return response
|
390
|
+
end
|
391
|
+
protected :make_http_request
|
392
|
+
|
393
|
+
# Obtains the root of Jenkins server. This function is used to see if
|
394
|
+
# Jenkins is running
|
395
|
+
#
|
396
|
+
# @return [Net::HTTP::Response] Response from Jenkins for "/"
|
397
|
+
#
|
398
|
+
def get_root
|
399
|
+
@logger.debug "GET #{@jenkins_path}/"
|
400
|
+
request = Net::HTTP::Get.new("#{@jenkins_path}/")
|
401
|
+
make_http_request(request)
|
402
|
+
end
|
403
|
+
|
404
|
+
# Sends a GET request to the Jenkins CI server with the specified URL
|
405
|
+
#
|
406
|
+
# @param [String] url_prefix The prefix to use in the URL
|
407
|
+
# @param [String] tree A specific JSON tree to optimize the API call
|
408
|
+
# @param [String] url_suffix The suffix to be used in the URL
|
409
|
+
# @param [Boolean] raw_response Return complete Response object instead of
|
410
|
+
# JSON body of response
|
411
|
+
#
|
412
|
+
# @return [String, Hash] JSON response from Jenkins
|
413
|
+
#
|
414
|
+
def api_get_request(url_prefix, tree = nil, url_suffix ="/api/json",
|
415
|
+
raw_response = false)
|
416
|
+
url_prefix = "#{@jenkins_path}#{url_prefix}"
|
417
|
+
to_get = ""
|
418
|
+
if tree
|
419
|
+
to_get = "#{url_prefix}#{url_suffix}?#{tree}"
|
420
|
+
else
|
421
|
+
to_get = "#{url_prefix}#{url_suffix}"
|
422
|
+
end
|
423
|
+
request = Net::HTTP::Get.new(to_get)
|
424
|
+
@logger.debug "GET #{to_get}"
|
425
|
+
response = make_http_request(request)
|
426
|
+
if raw_response
|
427
|
+
handle_exception(response, "raw")
|
428
|
+
else
|
429
|
+
handle_exception(response, "body", url_suffix =~ /json/)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# Sends a POST message to the Jenkins CI server with the specified URL
|
434
|
+
#
|
435
|
+
# @param [String] url_prefix The prefix to be used in the URL
|
436
|
+
# @param [Hash] form_data Form data to send with POST request
|
437
|
+
# @param [Boolean] raw_response Return complete Response object instead of
|
438
|
+
# JSON body of response
|
439
|
+
#
|
440
|
+
# @return [String] Response code form Jenkins Response
|
441
|
+
#
|
442
|
+
def api_post_request(url_prefix, form_data = {}, raw_response = false)
|
443
|
+
retries = @crumb_max_retries
|
444
|
+
begin
|
445
|
+
refresh_crumbs
|
446
|
+
|
447
|
+
# Added form_data default {} instead of nil to help with proxies
|
448
|
+
# that barf with empty post
|
449
|
+
request = Net::HTTP::Post.new("#{@jenkins_path}#{url_prefix}")
|
450
|
+
@logger.debug "POST #{url_prefix}"
|
451
|
+
if @crumbs_enabled
|
452
|
+
request[@crumb["crumbRequestField"]] = @crumb["crumb"]
|
453
|
+
end
|
454
|
+
request.set_form_data(form_data)
|
455
|
+
response = make_http_request(request)
|
456
|
+
if raw_response
|
457
|
+
handle_exception(response, "raw")
|
458
|
+
else
|
459
|
+
handle_exception(response)
|
460
|
+
end
|
461
|
+
rescue Exceptions::ForbiddenException => e
|
462
|
+
refresh_crumbs(true)
|
463
|
+
|
464
|
+
if @crumbs_enabled
|
465
|
+
@logger.info "Retrying: #{@crumb_max_retries - retries + 1} out of" +
|
466
|
+
" #{@crumb_max_retries} times..."
|
467
|
+
retries -= 1
|
468
|
+
|
469
|
+
if retries > 0
|
470
|
+
retry
|
471
|
+
else
|
472
|
+
raise Exceptions::ForbiddenWithCrumb.new(@logger, e.message)
|
473
|
+
end
|
474
|
+
else
|
475
|
+
raise
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Obtains the configuration of a component from the Jenkins CI server
|
481
|
+
#
|
482
|
+
# @param [String] url_prefix The prefix to be used in the URL
|
483
|
+
#
|
484
|
+
# @return [String] XML configuration obtained from Jenkins
|
485
|
+
#
|
486
|
+
def get_config(url_prefix)
|
487
|
+
request = Net::HTTP::Get.new("#{@jenkins_path}#{url_prefix}/config.xml")
|
488
|
+
@logger.debug "GET #{url_prefix}/config.xml"
|
489
|
+
response = make_http_request(request)
|
490
|
+
handle_exception(response, "body")
|
491
|
+
end
|
492
|
+
|
493
|
+
# Posts the given xml configuration to the url given
|
494
|
+
#
|
495
|
+
# @param [String] url_prefix The prefix to be used in the URL
|
496
|
+
# @param [String] xml The XML configuration to be sent to Jenkins
|
497
|
+
#
|
498
|
+
# @return [String] Response code returned from Jenkins
|
499
|
+
#
|
500
|
+
def post_config(url_prefix, xml)
|
501
|
+
post_data(url_prefix, xml, 'application/xml;charset=UTF-8')
|
502
|
+
end
|
503
|
+
|
504
|
+
def post_json(url_prefix, json)
|
505
|
+
post_data(url_prefix, json, 'application/json;charset=UTF-8')
|
506
|
+
end
|
507
|
+
|
508
|
+
def post_data(url_prefix, data, content_type)
|
509
|
+
retries = @crumb_max_retries
|
510
|
+
begin
|
511
|
+
refresh_crumbs
|
512
|
+
|
513
|
+
request = Net::HTTP::Post.new("#{@jenkins_path}#{url_prefix}")
|
514
|
+
@logger.debug "POST #{url_prefix}"
|
515
|
+
request.body = data
|
516
|
+
request.content_type = content_type
|
517
|
+
if @crumbs_enabled
|
518
|
+
request[@crumb["crumbRequestField"]] = @crumb["crumb"]
|
519
|
+
end
|
520
|
+
response = make_http_request(request)
|
521
|
+
handle_exception(response)
|
522
|
+
rescue Exceptions::ForbiddenException => e
|
523
|
+
refresh_crumbs(true)
|
524
|
+
|
525
|
+
if @crumbs_enabled
|
526
|
+
@logger.info "Retrying: #{@crumb_max_retries - retries + 1} out of" +
|
527
|
+
" #{@crumb_max_retries} times..."
|
528
|
+
retries -= 1
|
529
|
+
|
530
|
+
if retries > 0
|
531
|
+
retry
|
532
|
+
else
|
533
|
+
raise Exceptions::ForbiddenWithCrumb.new(@logger, e.message)
|
534
|
+
end
|
535
|
+
else
|
536
|
+
raise
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def init_update_center
|
542
|
+
@logger.info "Initializing Jenkins Update Center..."
|
543
|
+
@logger.debug "Obtaining the JSON data for Update Center..."
|
544
|
+
# TODO: Clean me up
|
545
|
+
update_center_data = open("https://updates.jenkins.io/current/update-center.json").read
|
546
|
+
# The Jenkins mirror returns the data in the following format
|
547
|
+
# updateCenter.post(
|
548
|
+
# {.. JSON data...}
|
549
|
+
# );
|
550
|
+
# which is used by the Javascript used by the Jenkins UI to send to Jenkins.
|
551
|
+
#
|
552
|
+
update_center_data.gsub!("updateCenter.post(\n", "")
|
553
|
+
update_center_data.gsub!("\n);", "")
|
554
|
+
|
555
|
+
@logger.debug "Posting the obtained JSON to Jenkins Update Center..."
|
556
|
+
post_json("/updateCenter/byId/default/postBack", update_center_data)
|
557
|
+
end
|
558
|
+
|
559
|
+
# Checks if Jenkins uses crumbs (i.e) the XSS disable option is checked in
|
560
|
+
# Jenkins' security settings
|
561
|
+
#
|
562
|
+
# @return [Boolean] whether Jenkins uses crumbs or not
|
563
|
+
#
|
564
|
+
def use_crumbs?
|
565
|
+
response = api_get_request("", "tree=useCrumbs")
|
566
|
+
response["useCrumbs"]
|
567
|
+
end
|
568
|
+
|
569
|
+
# Checks if Jenkins uses security
|
570
|
+
#
|
571
|
+
# @return [Boolean] whether Jenkins uses security or not
|
572
|
+
#
|
573
|
+
def use_security?
|
574
|
+
response = api_get_request("", "tree=useSecurity")
|
575
|
+
response["useSecurity"]
|
576
|
+
end
|
577
|
+
|
578
|
+
# Obtains the jenkins version from the API
|
579
|
+
# Only queries Jenkins if the version is not already stored.
|
580
|
+
# Note that the version is auto-updated after every request made to Jenkins
|
581
|
+
# since it is returned as a header in every response
|
582
|
+
#
|
583
|
+
# @return [String] Jenkins version
|
584
|
+
#
|
585
|
+
def get_jenkins_version
|
586
|
+
get_root if @jenkins_version.nil?
|
587
|
+
@jenkins_version
|
588
|
+
end
|
589
|
+
|
590
|
+
# Obtain the Hudson version of the CI server
|
591
|
+
# Only queries Hudson/Jenkins if the version is not already stored.
|
592
|
+
# Note that the version is auto-updated after every request made to Jenkins
|
593
|
+
# since it is returned as a header in every response
|
594
|
+
#
|
595
|
+
# @return [String] Version of Hudson on Jenkins server
|
596
|
+
#
|
597
|
+
def get_hudson_version
|
598
|
+
get_root if @hudson_version.nil?
|
599
|
+
@hudson_version
|
600
|
+
end
|
601
|
+
|
602
|
+
# Converts a version string to a list of integers
|
603
|
+
# This makes it easier to compare versions since in 'version-speak',
|
604
|
+
# v 1.2 is a lot older than v 1.102 - and simple < > on version
|
605
|
+
# strings doesn't work so well
|
606
|
+
def deconstruct_version_string(version)
|
607
|
+
match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?$/)
|
608
|
+
|
609
|
+
# Match should have 4 parts [0] = input string, [1] = major
|
610
|
+
# [2] = minor, [3] = patch (possibly blank)
|
611
|
+
if match && match.size == 4
|
612
|
+
return [match[1].to_i, match[2].to_i, match[3].to_i || 0]
|
613
|
+
else
|
614
|
+
return nil
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
# Compare two version strings (A and B)
|
619
|
+
# if A == B, returns 0
|
620
|
+
# if A > B, returns 1
|
621
|
+
# if A < B, returns -1
|
622
|
+
def compare_versions(version_a, version_b)
|
623
|
+
if version_a == version_b
|
624
|
+
return 0
|
625
|
+
else
|
626
|
+
version_a_d = deconstruct_version_string(version_a)
|
627
|
+
version_b_d = deconstruct_version_string(version_b)
|
628
|
+
|
629
|
+
if version_a_d[0] > version_b_d[0] ||
|
630
|
+
(version_a_d[0] == version_b_d[0] && version_a_d[1] > version_b_d[1]) ||
|
631
|
+
(version_a_d[0] == version_b_d[0] && version_a_d[1] == version_b_d[1] && version_a_d[2] > version_b_d[2])
|
632
|
+
return 1
|
633
|
+
else
|
634
|
+
return -1
|
635
|
+
end
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
# Obtain the date of the Jenkins server
|
640
|
+
#
|
641
|
+
# @return [String] Server date
|
642
|
+
#
|
643
|
+
def get_server_date
|
644
|
+
response = get_root
|
645
|
+
response["Date"]
|
646
|
+
end
|
647
|
+
|
648
|
+
# Executes the provided groovy script on the Jenkins CI server
|
649
|
+
#
|
650
|
+
# @param [String] script_text The text of the groovy script to execute
|
651
|
+
#
|
652
|
+
# @return [String] The output of the executed groovy script
|
653
|
+
#
|
654
|
+
def exec_script(script_text)
|
655
|
+
response = api_post_request('/scriptText', {'script' => script_text}, true)
|
656
|
+
response.body
|
657
|
+
end
|
658
|
+
|
659
|
+
# Execute the Jenkins CLI
|
660
|
+
#
|
661
|
+
# @param command [String] command name
|
662
|
+
# @param args [Array] the arguments for the command
|
663
|
+
#
|
664
|
+
# @return [String] command output from the CLI
|
665
|
+
#
|
666
|
+
# @raise [Exceptions::CLIException] if there are issues in running the
|
667
|
+
# commands using CLI
|
668
|
+
#
|
669
|
+
def exec_cli(command, args = [])
|
670
|
+
base_dir = File.dirname(__FILE__)
|
671
|
+
server_url = "http://#{@server_ip}:#{@server_port}/#{@jenkins_path}"
|
672
|
+
cmd = "java -jar #{base_dir}/../../java_deps/jenkins-cli.jar -s #{server_url}"
|
673
|
+
cmd << " -i #{@identity_file}" if @identity_file && !@identity_file.empty?
|
674
|
+
cmd << " #{command}"
|
675
|
+
cmd << " --username #{@username} --password #{@password}" if @identity_file.nil? || @identity_file.empty?
|
676
|
+
cmd << ' '
|
677
|
+
cmd << args.join(' ')
|
678
|
+
java_cmd = Mixlib::ShellOut.new(cmd)
|
679
|
+
|
680
|
+
# Run the command
|
681
|
+
java_cmd.run_command
|
682
|
+
if java_cmd.stderr.empty?
|
683
|
+
java_cmd.stdout.chomp
|
684
|
+
else
|
685
|
+
# The stderr has a stack trace of the Java program. We'll already have
|
686
|
+
# a stack trace for Ruby. So just display a descriptive message for the
|
687
|
+
# error thrown by the CLI.
|
688
|
+
raise Exceptions::CLIException.new(
|
689
|
+
@logger,
|
690
|
+
java_cmd.stderr.split("\n").first
|
691
|
+
)
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
private
|
696
|
+
|
697
|
+
# Obtains the crumb from Jenkins' crumb issuer
|
698
|
+
#
|
699
|
+
# @return [Hash<String, String>] the crumb response from Jenkins' crumb
|
700
|
+
# issuer
|
701
|
+
#
|
702
|
+
# @raise Exceptions::CrumbNotFoundException if the crumb is not provided
|
703
|
+
# (i.e) XSS disable option is not checked in Jenkins' security setting
|
704
|
+
#
|
705
|
+
def get_crumb
|
706
|
+
begin
|
707
|
+
@logger.debug "Obtaining crumb from the jenkins server"
|
708
|
+
api_get_request("/crumbIssuer")
|
709
|
+
rescue Exceptions::NotFoundException
|
710
|
+
raise Exceptions::CrumbNotFoundException.new(
|
711
|
+
@logger,
|
712
|
+
"CSRF protection is not enabled on the server at the moment." +
|
713
|
+
" Perhaps the client was initialized when the CSRF setting was" +
|
714
|
+
" enabled. Please re-initialize the client."
|
715
|
+
)
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
# Used to determine whether crumbs are enabled, and populate/clear our
|
720
|
+
# local crumb accordingly.
|
721
|
+
#
|
722
|
+
# @param force_refresh [Boolean] determines whether the check is
|
723
|
+
# cursory or deeper. The default is cursory - i.e. if crumbs
|
724
|
+
# enabled is 'nil' then figure out what to do, otherwise skip
|
725
|
+
# If 'true' the method will check to see if the crumbs require-
|
726
|
+
# ment has changed (by querying Jenkins), and updating crumb
|
727
|
+
# (refresh, delete, create) as appropriate.
|
728
|
+
#
|
729
|
+
def refresh_crumbs(force_refresh = false)
|
730
|
+
# Quick check to see if someone has changed XSS settings and not
|
731
|
+
# restarted us
|
732
|
+
if force_refresh || @crumbs_enabled.nil?
|
733
|
+
old_crumbs_setting = @crumbs_enabled
|
734
|
+
new_crumbs_setting = use_crumbs?
|
735
|
+
|
736
|
+
if old_crumbs_setting != new_crumbs_setting
|
737
|
+
@crumbs_enabled = new_crumbs_setting
|
738
|
+
end
|
739
|
+
|
740
|
+
# Get or clear crumbs setting appropriately
|
741
|
+
# Works as refresh if crumbs still enabled
|
742
|
+
if @crumbs_enabled
|
743
|
+
if old_crumbs_setting
|
744
|
+
@logger.info "Crumb expired. Refetching from the server."
|
745
|
+
else
|
746
|
+
@logger.info "Crumbs turned on. Fetching from the server."
|
747
|
+
end
|
748
|
+
|
749
|
+
@crumb = get_crumb if force_refresh || !old_crumbs_setting
|
750
|
+
else
|
751
|
+
if old_crumbs_setting
|
752
|
+
@logger.info "Crumbs turned off. Clearing crumb."
|
753
|
+
@crumb.clear
|
754
|
+
end
|
755
|
+
end
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
# Private method. Converts keys passed in as strings into symbols.
|
760
|
+
#
|
761
|
+
# @param hash [Hash] Hash containing arguments to login to jenkins.
|
762
|
+
#
|
763
|
+
def symbolize_keys(hash)
|
764
|
+
hash.inject({}){|result, (key, value)|
|
765
|
+
new_key = case key
|
766
|
+
when String then key.to_sym
|
767
|
+
else key
|
768
|
+
end
|
769
|
+
new_value = case value
|
770
|
+
when Hash then symbolize_keys(value)
|
771
|
+
else value
|
772
|
+
end
|
773
|
+
result[new_key] = new_value
|
774
|
+
result
|
775
|
+
}
|
776
|
+
end
|
777
|
+
|
778
|
+
# Private method that handles the exception and raises with proper error
|
779
|
+
# message with the type of exception and returns the required values if no
|
780
|
+
# exceptions are raised.
|
781
|
+
#
|
782
|
+
# @param [Net::HTTP::Response] response Response from Jenkins
|
783
|
+
# @param [String] to_send What should be returned as a response. Allowed
|
784
|
+
# values: "code", "body", and "raw".
|
785
|
+
# @param [Boolean] send_json Boolean value used to determine whether to
|
786
|
+
# load the JSON or send the response as is.
|
787
|
+
#
|
788
|
+
# @return [String, Hash] Response returned whether loaded JSON or raw
|
789
|
+
# string
|
790
|
+
#
|
791
|
+
# @raise [Exceptions::Unauthorized] When invalid credentials are
|
792
|
+
# provided to connect to Jenkins
|
793
|
+
# @raise [Exceptions::NotFound] When the requested page on Jenkins is not
|
794
|
+
# found
|
795
|
+
# @raise [Exceptions::InternalServerError] When Jenkins returns a 500
|
796
|
+
# Internal Server Error
|
797
|
+
# @raise [Exceptions::ApiException] Any other exception returned from
|
798
|
+
# Jenkins that are not categorized in the API Client.
|
799
|
+
#
|
800
|
+
def handle_exception(response, to_send = "code", send_json = false)
|
801
|
+
msg = "HTTP Code: #{response.code}, Response Body: #{response.body}"
|
802
|
+
@logger.debug msg
|
803
|
+
case response.code.to_i
|
804
|
+
# As of Jenkins version 1.519, the job builds return a 201 status code
|
805
|
+
# with a Location HTTP header with the pointing the URL of the item in
|
806
|
+
# the queue.
|
807
|
+
when 200, 201, 302
|
808
|
+
if to_send == "body" && send_json
|
809
|
+
return JSON.parse(response.body)
|
810
|
+
elsif to_send == "body"
|
811
|
+
return response.body
|
812
|
+
elsif to_send == "code"
|
813
|
+
return response.code
|
814
|
+
elsif to_send == "raw"
|
815
|
+
return response
|
816
|
+
end
|
817
|
+
when 400
|
818
|
+
matched = response.body.match(/<p>(.*)<\/p>/)
|
819
|
+
api_message = matched[1] unless matched.nil?
|
820
|
+
@logger.debug "API message: #{api_message}"
|
821
|
+
case api_message
|
822
|
+
when /A job already exists with the name/
|
823
|
+
raise Exceptions::JobAlreadyExists.new(@logger, api_message)
|
824
|
+
when /A view already exists with the name/
|
825
|
+
raise Exceptions::ViewAlreadyExists.new(@logger, api_message)
|
826
|
+
when /Slave called .* already exists/
|
827
|
+
raise Exceptions::NodeAlreadyExists.new(@logger, api_message)
|
828
|
+
when /Nothing is submitted/
|
829
|
+
raise Exceptions::NothingSubmitted.new(@logger, api_message)
|
830
|
+
else
|
831
|
+
raise Exceptions::ApiException.new(@logger, api_message)
|
832
|
+
end
|
833
|
+
when 401
|
834
|
+
raise Exceptions::Unauthorized.new @logger
|
835
|
+
when 403
|
836
|
+
raise Exceptions::Forbidden.new @logger
|
837
|
+
when 404
|
838
|
+
raise Exceptions::NotFound.new @logger
|
839
|
+
when 500
|
840
|
+
matched = response.body.match(/Exception: (.*)<br>/)
|
841
|
+
api_message = matched[1] unless matched.nil?
|
842
|
+
@logger.debug "API message: #{api_message}"
|
843
|
+
raise Exceptions::InternalServerError.new(@logger, api_message)
|
844
|
+
when 503
|
845
|
+
raise Exceptions::ServiceUnavailable.new @logger
|
846
|
+
else
|
847
|
+
raise Exceptions::ApiException.new(
|
848
|
+
@logger,
|
849
|
+
"Error code #{response.code}"
|
850
|
+
)
|
851
|
+
end
|
852
|
+
end
|
853
|
+
|
854
|
+
end
|
855
|
+
end
|