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.
@@ -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