lhj-tools 0.1.25 → 0.1.26

Sign up to get free protection for your applications and to get access to all the features.
@@ -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