lhj-tools 0.1.24 → 0.1.27

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