jenkins_api_client 0.14.1 → 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +5 -23
  4. data/CHANGELOG.md +104 -35
  5. data/CONTRIBUTORS.md +1 -11
  6. data/Gemfile +4 -2
  7. data/README.md +107 -7
  8. data/Rakefile +1 -0
  9. data/Vagrantfile +6 -8
  10. data/jenkins_api_client.gemspec +30 -12
  11. data/lib/jenkins_api_client/cli/helper.rb +1 -0
  12. data/lib/jenkins_api_client/client.rb +162 -69
  13. data/lib/jenkins_api_client/exceptions.rb +26 -17
  14. data/lib/jenkins_api_client/job.rb +321 -75
  15. data/lib/jenkins_api_client/node.rb +22 -10
  16. data/lib/jenkins_api_client/plugin_manager.rb +460 -0
  17. data/lib/jenkins_api_client/urihelper.rb +17 -0
  18. data/lib/jenkins_api_client/user.rb +4 -2
  19. data/lib/jenkins_api_client/version.rb +3 -3
  20. data/lib/jenkins_api_client/view.rb +10 -7
  21. data/lib/jenkins_api_client.rb +1 -0
  22. data/scripts/login_with_pry.rb +54 -0
  23. data/spec/func_tests/client_spec.rb +3 -3
  24. data/spec/func_tests/job_spec.rb +90 -7
  25. data/spec/func_tests/{node_spec.rb → node_spec.rb.pending} +9 -9
  26. data/spec/func_tests/plugin_spec.rb +148 -0
  27. data/spec/unit_tests/client_spec.rb +108 -27
  28. data/spec/unit_tests/fake_http_response.rb +9 -0
  29. data/spec/unit_tests/fixtures/files/available_plugins.json +1 -0
  30. data/spec/unit_tests/fixtures/files/installed_plugins.json +1 -0
  31. data/spec/unit_tests/fixtures/files/updatable_plugins.json +1 -0
  32. data/spec/unit_tests/job_spec.rb +109 -6
  33. data/spec/unit_tests/node_spec.rb +18 -6
  34. data/spec/unit_tests/plugin_spec.rb +165 -0
  35. data/spec/unit_tests/spec_helper.rb +11 -1
  36. data/spec/unit_tests/system_spec.rb +2 -1
  37. data/spec/unit_tests/user_spec.rb +1 -1
  38. data/travis/hudson.model.UpdateCenter.xml +7 -0
  39. data/travis/setup.sh +2 -1
  40. metadata +76 -64
@@ -35,7 +35,7 @@ require 'logger'
35
35
  #
36
36
  module JenkinsApi
37
37
  # This is the client class that acts as the bridge between the subclasses and
38
- # Jnekins. This class contains methods that performs GET and POST requests
38
+ # Jenkins. This class contains methods that performs GET and POST requests
39
39
  # for various operations.
40
40
  #
41
41
  class Client
@@ -44,6 +44,8 @@ module JenkinsApi
44
44
  DEFAULT_SERVER_PORT = 8080
45
45
  # Default timeout in seconds to be used while performing operations
46
46
  DEFAULT_TIMEOUT = 120
47
+ DEFAULT_HTTP_OPEN_TIMEOUT = 10
48
+ DEFAULT_HTTP_READ_TIMEOUT = 120
47
49
  # Parameters that are permitted as options while initializing the client
48
50
  VALID_PARAMS = [
49
51
  "server_url",
@@ -58,53 +60,39 @@ module JenkinsApi
58
60
  "log_location",
59
61
  "log_level",
60
62
  "timeout",
63
+ "http_open_timeout",
64
+ "http_read_timeout",
61
65
  "ssl",
62
66
  "follow_redirects",
63
- "identity_file"
67
+ "identity_file",
68
+ "cookies"
64
69
  ].freeze
65
70
 
66
71
  # Initialize a Client object with Jenkins CI server credentials
67
72
  #
68
73
  # @param args [Hash] Arguments to connect to Jenkins server
69
74
  #
70
- # @option args [String] :server_ip
71
- # the IP address of the Jenkins CI server
72
- # @option args [String] :server_port
73
- # the port on which the Jenkins listens
74
- # @option args [String] :server_url
75
- # the full URL address of the Jenkins CI server (http/https)
76
- # @option args [String] :username
77
- # the username used for connecting to the server (optional)
78
- # @option args [String] :password
79
- # the password for connecting to the CI server (optional)
80
- # @option args [String] :password_base64
81
- # the password with base64 encoded format for connecting to the CI
75
+ # @option args [String] :server_ip the IP address of the Jenkins CI server
76
+ # @option args [String] :server_port the port on which the Jenkins listens
77
+ # @option args [String] :server_url the full URL address of the Jenkins CI server (http/https). This can include username/password. :username/:password options will override any user/pass value in the URL
78
+ # @option args [String] :username the username used for connecting to the server (optional)
79
+ # @option args [String] :password the password or API Key for connecting to the CI server (optional)
80
+ # @option args [String] :password_base64 the password with base64 encoded format for connecting to the CI
82
81
  # server (optional)
83
- # @option args [String] :identity_file
84
- # the priviate key file for Jenkins CLI authentication,
85
- # it is used only for executing CLI commands.
86
- # also remember to upload the public key to
87
- # http://<Server IP>:<Server Port>/user/<Username>/configure
88
- # @option args [String] :proxy_ip
89
- # the proxy IP address
90
- # @option args [String] :proxy_port
91
- # the proxy port
92
- # @option args [String] :jenkins_path ("/")
93
- # the optional context path for Jenkins
94
- # @option args [Boolean] :ssl (false)
95
- # indicates if Jenkins is accessible over HTTPS
96
- # @option args [Boolean] :follow_redirects
97
- # This argument causes the client to follow a redirect (jenkins can
82
+ # @option args [String] :identity_file the priviate key file for Jenkins CLI authentication,
83
+ # it is used only for executing CLI commands. Also remember to upload the public key to
84
+ # <Server IP>:<Server Port>/user/<Username>/configure
85
+ # @option args [String] :proxy_ip the proxy IP address
86
+ # @option args [String] :proxy_port the proxy port
87
+ # @option args [String] :jenkins_path ("/") the optional context path for Jenkins
88
+ # @option args [Boolean] :ssl (false) indicates if Jenkins is accessible over HTTPS
89
+ # @option args [Boolean] :follow_redirects this argument causes the client to follow a redirect (jenkins can
98
90
  # return a 30x when starting a build)
99
- # @option args [Fixnum] :timeout (120)
100
- # This argument sets the timeout for operations that take longer (in
101
- # seconds)
102
- # @option args [String] :log_location (STDOUT)
103
- # The location for the log file
104
- # @option args [Fixnum] :log_level (Logger::INFO)
105
- # The level for messages to be logged. Should be one of:
106
- # Logger::DEBUG (0), Logger::INFO (1), Logger::WARN (2), Logger::ERROR
107
- # (2), Logger::FATAL (3) (Defaults to Logger::INFO)
91
+ # @option args [Fixnum] :timeout (120) This argument sets the timeout for operations that take longer (in seconds)
92
+ # @option args [String] :log_location (STDOUT) the location for the log file
93
+ # @option args [Fixnum] :log_level (Logger::INFO) The level for messages to be logged. Should be one of:
94
+ # Logger::DEBUG (0), Logger::INFO (1), Logger::WARN (2), Logger::ERROR (2), Logger::FATAL (3)
95
+ # @option args [String] :cookies Cookies to be sent with all requests in the format: name=value; name2=value2
108
96
  #
109
97
  # @return [JenkinsApi::Client] a client object to Jenkins API
110
98
  #
@@ -117,12 +105,26 @@ module JenkinsApi
117
105
  end
118
106
  end if args.is_a? Hash
119
107
 
120
- # Server IP or Server URL must be specifiec
108
+ # Server IP or Server URL must be specific
121
109
  unless @server_ip || @server_url
122
110
  raise ArgumentError, "Server IP or Server URL is required to connect" +
123
111
  " to Jenkins"
124
112
  end
125
113
 
114
+ # Get info from the server_url, if we got one
115
+ if @server_url
116
+ server_uri = URI.parse(@server_url)
117
+ @server_ip = server_uri.host
118
+ @server_port = server_uri.port
119
+ @ssl = server_uri.scheme == "https"
120
+ @jenkins_path = server_uri.path
121
+
122
+ # read username and password from the URL
123
+ # only set if @username and @password are not already set via explicit options
124
+ @username ||= server_uri.user
125
+ @password ||= server_uri.password
126
+ end
127
+
126
128
  # Username/password are optional as some jenkins servers do not require
127
129
  # authentication
128
130
  if @username && !(@password || @password_base64)
@@ -133,19 +135,12 @@ module JenkinsApi
133
135
  " both left nil"
134
136
  end
135
137
 
136
- # Get info from the server_url, if we got one
137
- if @server_url
138
- server_uri = URI.parse(@server_url)
139
- @server_ip = server_uri.host
140
- @server_port = server_uri.port
141
- @ssl = server_uri.scheme == "https"
142
- @jenkins_path = server_uri.path
143
- end
144
-
145
138
  @jenkins_path ||= ""
146
139
  @jenkins_path.gsub!(/\/$/,"") # remove trailing slash if there is one
147
140
  @server_port = DEFAULT_SERVER_PORT unless @server_port
148
141
  @timeout = DEFAULT_TIMEOUT unless @timeout
142
+ @http_open_timeout = DEFAULT_HTTP_OPEN_TIMEOUT unless @http_open_timeout
143
+ @http_read_timeout = DEFAULT_HTTP_READ_TIMEOUT unless @http_read_timeout
149
144
  @ssl ||= false
150
145
 
151
146
  # Setting log options
@@ -212,6 +207,15 @@ module JenkinsApi
212
207
  JenkinsApi::Client::BuildQueue.new(self)
213
208
  end
214
209
 
210
+ # Creates an instance to the PluginManager by passing a reference to self
211
+ #
212
+ # @return [JenkinsApi::Client::PluginManager] an object to PluginManager
213
+ # subclass
214
+ #
215
+ def plugin
216
+ JenkinsApi::Client::PluginManager.new(self)
217
+ end
218
+
215
219
  # Creates an instance of the User class by passing a reference to self
216
220
  #
217
221
  # @return [JenkinsApi::Client::User] An object of User subclass
@@ -240,7 +244,9 @@ module JenkinsApi
240
244
  " @crumbs_enabled=#{@crumbs_enabled.inspect}," +
241
245
  " @follow_redirects=#{@follow_redirects.inspect}," +
242
246
  " @jenkins_path=#{@jenkins_path.inspect}," +
243
- " @timeout=#{@timeout.inspect}>"
247
+ " @timeout=#{@timeout.inspect}>," +
248
+ " @http_open_timeout=#{@http_open_timeout.inspect}>," +
249
+ " @http_read_timeout=#{@http_read_timeout.inspect}>"
244
250
  end
245
251
 
246
252
  # Connects to the Jenkins server, sends the specified request and returns
@@ -253,6 +259,7 @@ module JenkinsApi
253
259
  #
254
260
  def make_http_request(request, follow_redirect = @follow_redirects)
255
261
  request.basic_auth @username, @password if @username
262
+ request['Cookie'] = @cookies if @cookies
256
263
 
257
264
  if @proxy_ip
258
265
  http = Net::HTTP::Proxy(@proxy_ip, @proxy_port).new(@server_ip, @server_port)
@@ -265,6 +272,9 @@ module JenkinsApi
265
272
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
266
273
  end
267
274
 
275
+ http.open_timeout = @http_open_timeout
276
+ http.read_timeout = @http_read_timeout
277
+
268
278
  response = http.request(request)
269
279
  case response
270
280
  when Net::HTTPRedirection then
@@ -278,6 +288,11 @@ module JenkinsApi
278
288
  )
279
289
  end
280
290
  end
291
+
292
+ # Pick out some useful header info before we return
293
+ @jenkins_version = response['X-Jenkins']
294
+ @hudson_version = response['X-Hudson']
295
+
281
296
  return response
282
297
  end
283
298
  protected :make_http_request
@@ -288,8 +303,8 @@ module JenkinsApi
288
303
  # @return [Net::HTTP::Response] Response from Jenkins for "/"
289
304
  #
290
305
  def get_root
291
- @logger.info "GET /"
292
- request = Net::HTTP::Get.new("/")
306
+ @logger.info "GET #{@jenkins_path}/"
307
+ request = Net::HTTP::Get.new("#{@jenkins_path}/")
293
308
  make_http_request(request)
294
309
  end
295
310
 
@@ -301,7 +316,7 @@ module JenkinsApi
301
316
  # @param [Boolean] raw_response Return complete Response object instead of
302
317
  # JSON body of response
303
318
  #
304
- # @return [String, JSON] JSON response from Jenkins
319
+ # @return [String, Hash] JSON response from Jenkins
305
320
  #
306
321
  def api_get_request(url_prefix, tree = nil, url_suffix ="/api/json",
307
322
  raw_response = false)
@@ -312,7 +327,6 @@ module JenkinsApi
312
327
  else
313
328
  to_get = "#{url_prefix}#{url_suffix}"
314
329
  end
315
- to_get = URI.escape(to_get)
316
330
  request = Net::HTTP::Get.new(to_get)
317
331
  @logger.info "GET #{to_get}"
318
332
  response = make_http_request(request)
@@ -337,10 +351,8 @@ module JenkinsApi
337
351
 
338
352
  # Added form_data default {} instead of nil to help with proxies
339
353
  # that barf with empty post
340
- url_prefix = URI.escape("#{@jenkins_path}#{url_prefix}")
341
- request = Net::HTTP::Post.new("#{url_prefix}")
354
+ request = Net::HTTP::Post.new("#{@jenkins_path}#{url_prefix}")
342
355
  @logger.info "POST #{url_prefix}"
343
- request.content_type = 'application/json'
344
356
  if @crumbs_enabled
345
357
  request[@crumb["crumbRequestField"]] = @crumb["crumb"]
346
358
  end
@@ -377,8 +389,7 @@ module JenkinsApi
377
389
  # @return [String] XML configuration obtained from Jenkins
378
390
  #
379
391
  def get_config(url_prefix)
380
- url_prefix = URI.escape("#{@jenkins_path}#{url_prefix}")
381
- request = Net::HTTP::Get.new("#{url_prefix}/config.xml")
392
+ request = Net::HTTP::Get.new("#{@jenkins_path}#{url_prefix}/config.xml")
382
393
  @logger.info "GET #{url_prefix}/config.xml"
383
394
  response = make_http_request(request)
384
395
  handle_exception(response, "body")
@@ -392,15 +403,22 @@ module JenkinsApi
392
403
  # @return [String] Response code returned from Jenkins
393
404
  #
394
405
  def post_config(url_prefix, xml)
406
+ post_data(url_prefix, xml, 'application/xml')
407
+ end
408
+
409
+ def post_json(url_prefix, json)
410
+ post_data(url_prefix, json, 'application/json')
411
+ end
412
+
413
+ def post_data(url_prefix, data, content_type)
395
414
  retries = @crumb_max_retries
396
415
  begin
397
416
  refresh_crumbs
398
417
 
399
- url_prefix = URI.escape("#{@jenkins_path}#{url_prefix}")
400
- request = Net::HTTP::Post.new("#{url_prefix}")
418
+ request = Net::HTTP::Post.new("#{@jenkins_path}#{url_prefix}")
401
419
  @logger.info "POST #{url_prefix}"
402
- request.body = xml
403
- request.content_type = 'application/xml'
420
+ request.body = data
421
+ request.content_type = content_type
404
422
  if @crumbs_enabled
405
423
  request[@crumb["crumbRequestField"]] = @crumb["crumb"]
406
424
  end
@@ -425,6 +443,24 @@ module JenkinsApi
425
443
  end
426
444
  end
427
445
 
446
+ def init_update_center
447
+ @logger.info "Initializing Jenkins Update Center..."
448
+ @logger.debug "Obtaining the JSON data for Update Center..."
449
+ # TODO: Clean me up
450
+ update_center_data = open("http://updates.jenkins-ci.org/update-center.json").read
451
+ # The Jenkins mirror returns the data in the following format
452
+ # updateCenter.post(
453
+ # {.. JSON data...}
454
+ # );
455
+ # which is used by the Javascript used by the Jenkins UI to send to Jenkins.
456
+ #
457
+ update_center_data.gsub!("updateCenter.post(\n", "")
458
+ update_center_data.gsub!("\n);", "")
459
+
460
+ @logger.debug "Posting the obtained JSON to Jenkins Update Center..."
461
+ post_json("/updateCenter/byId/default/postBack", update_center_data)
462
+ end
463
+
428
464
  # Checks if Jenkins uses crumbs (i.e) the XSS disable option is checked in
429
465
  # Jenkins' security settings
430
466
  #
@@ -445,21 +481,64 @@ module JenkinsApi
445
481
  end
446
482
 
447
483
  # Obtains the jenkins version from the API
484
+ # Only queries Jenkins if the version is not already stored.
485
+ # Note that the version is auto-updated after every request made to Jenkins
486
+ # since it is returned as a header in every response
448
487
  #
449
- # @return Jenkins version
488
+ # @return [String] Jenkins version
450
489
  #
451
490
  def get_jenkins_version
452
- response = get_root
453
- response["X-Jenkins"]
491
+ get_root if @jenkins_version.nil?
492
+ @jenkins_version
454
493
  end
455
494
 
456
495
  # Obtain the Hudson version of the CI server
496
+ # Only queries Hudson/Jenkins if the version is not already stored.
497
+ # Note that the version is auto-updated after every request made to Jenkins
498
+ # since it is returned as a header in every response
457
499
  #
458
500
  # @return [String] Version of Hudson on Jenkins server
459
501
  #
460
502
  def get_hudson_version
461
- response = get_root
462
- response["X-Hudson"]
503
+ get_root if @hudson_version.nil?
504
+ @hudson_version
505
+ end
506
+
507
+ # Converts a version string to a list of integers
508
+ # This makes it easier to compare versions since in 'version-speak',
509
+ # v 1.2 is a lot older than v 1.102 - and simple < > on version
510
+ # strings doesn't work so well
511
+ def deconstruct_version_string(version)
512
+ match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?$/)
513
+
514
+ # Match should have 4 parts [0] = input string, [1] = major
515
+ # [2] = minor, [3] = patch (possibly blank)
516
+ if match && match.size == 4
517
+ return [match[1].to_i, match[2].to_i, match[3].to_i || 0]
518
+ else
519
+ return nil
520
+ end
521
+ end
522
+
523
+ # Compare two version strings (A and B)
524
+ # if A == B, returns 0
525
+ # if A > B, returns 1
526
+ # if A < B, returns -1
527
+ def compare_versions(version_a, version_b)
528
+ if version_a == version_b
529
+ return 0
530
+ else
531
+ version_a_d = deconstruct_version_string(version_a)
532
+ version_b_d = deconstruct_version_string(version_b)
533
+
534
+ if version_a_d[0] > version_b_d[0] ||
535
+ (version_a_d[0] == version_b_d[0] && version_a_d[1] > version_b_d[1]) ||
536
+ (version_a_d[0] == version_b_d[0] && version_a_d[1] == version_b_d[1] && version_a_d[2] > version_b_d[2])
537
+ return 1
538
+ else
539
+ return -1
540
+ end
541
+ end
463
542
  end
464
543
 
465
544
  # Obtain the date of the Jenkins server
@@ -471,6 +550,17 @@ module JenkinsApi
471
550
  response["Date"]
472
551
  end
473
552
 
553
+ # Executes the provided groovy script on the Jenkins CI server
554
+ #
555
+ # @param [String] script_text The text of the groovy script to execute
556
+ #
557
+ # @return [String] The output of the executed groovy script
558
+ #
559
+ def exec_script(script_text)
560
+ response = api_post_request('/scriptText', {'script' => script_text}, true)
561
+ response.body
562
+ end
563
+
474
564
  # Execute the Jenkins CLI
475
565
  #
476
566
  # @param command [String] command name
@@ -581,7 +671,7 @@ module JenkinsApi
581
671
  # @param [Boolean] send_json Boolean value used to determine whether to
582
672
  # load the JSON or send the response as is.
583
673
  #
584
- # @return [String, JSON] Response returned whether loaded JSON or raw
674
+ # @return [String, Hash] Response returned whether loaded JSON or raw
585
675
  # string
586
676
  #
587
677
  # @raise [Exceptions::Unauthorized] When invalid credentials are
@@ -633,7 +723,10 @@ module JenkinsApi
633
723
  when 404
634
724
  raise Exceptions::NotFound.new @logger
635
725
  when 500
636
- raise Exceptions::InternalServerError.new @logger
726
+ matched = response.body.match(/Exception: (.*)<br>/)
727
+ api_message = matched[1] unless matched.nil?
728
+ @logger.debug "API message: #{api_message}"
729
+ raise Exceptions::InternalServerError.new(@logger, api_message)
637
730
  when 503
638
731
  raise Exceptions::ServiceUnavailable.new @logger
639
732
  else
@@ -41,8 +41,8 @@ module JenkinsApi
41
41
  #
42
42
  class NothingSubmitted < ApiException
43
43
  def initialize(logger, message = "", log_level = Logger::ERROR)
44
- msg = "Nothing is submitted. #{message}"
45
- super(logger, msg)
44
+ message = "Nothing is submitted." if message.empty?
45
+ super(logger, message)
46
46
  end
47
47
  end
48
48
 
@@ -80,8 +80,8 @@ module JenkinsApi
80
80
  #
81
81
  class Unauthorized < ApiException
82
82
  def initialize(logger, message = "", log_level = Logger::ERROR)
83
- msg = "Invalid credentials are provided. #{message}"
84
- super(logger, msg, Logger::FATAL)
83
+ message = "Invalid credentials are provided." if message.empty?
84
+ super(logger, message, Logger::FATAL)
85
85
  end
86
86
  end
87
87
  # Support for backward compatibility
@@ -158,8 +158,8 @@ module JenkinsApi
158
158
  #
159
159
  class CrumbNotFound < NotFound
160
160
  def initialize(logger, message = "", log_level = Logger::ERROR)
161
- msg = "No crumb available on the server. #{message}"
162
- super(logger, msg)
161
+ message = "No crumb available on the server." if message.empty?
162
+ super(logger, message)
163
163
  end
164
164
  end
165
165
  # Support for backward compatibility
@@ -196,14 +196,14 @@ module JenkinsApi
196
196
  end
197
197
 
198
198
  # This exception class handles cases where the Jenkins API returns with a
199
- # 500 Internel Server Error.
199
+ # 500 Internal Server Error.
200
200
  #
201
201
  class InternalServerError < ApiException
202
202
  def initialize(logger, message = "", log_level = Logger::ERROR)
203
- msg = "Internel Server Error. Perhaps the in-memory configuration of" +
204
- " Jenkins is different from the disk configuration." +
205
- " Please try to reload the configuration #{message}"
206
- super(logger, msg)
203
+ message = "Internal Server Error. Perhaps the in-memory configuration" +
204
+ " Jenkins is different from the disk configuration. Please try to" +
205
+ " reload the configuration" if message.nil? || message.empty?
206
+ super(logger, message)
207
207
  end
208
208
  end
209
209
  # Support for backward compatibility
@@ -214,10 +214,10 @@ module JenkinsApi
214
214
  #
215
215
  class ServiceUnavailable < ApiException
216
216
  def initialize(logger, message = "", log_level = Logger::ERROR)
217
- msg = "Jenkins is being reloaded or restarted. Please wait till" +
218
- " Jenkins is completely back online. This can be" +
219
- " programatically achieved by System#wait_for_ready #{message}"
220
- super(logger, msg)
217
+ message = "Jenkins is being reloaded or restarted. Please wait till" +
218
+ " Jenkins is completely back online. This can be" +
219
+ " programatically achieved by System#wait_for_ready" if message.empty?
220
+ super(logger, message)
221
221
  end
222
222
  end
223
223
  # Support for backward compatibility
@@ -227,11 +227,20 @@ module JenkinsApi
227
227
  #
228
228
  class CLIError < ApiException
229
229
  def initialize(logger, message = "", log_level = Logger::ERROR)
230
- msg = "Execute CLI Error. #{message}"
231
- super(logger, msg)
230
+ message = "Unable to execute the command." if message.empty?
231
+ super(logger, message)
232
232
  end
233
233
  end
234
234
  # Support for backward compatibility
235
235
  CLIException = CLIError
236
+
237
+ # Exception when a particular plugin is not found
238
+ #
239
+ class PluginNotFound < NotFound
240
+ def initialize(logger, message = "", log_level = Logger::ERROR)
241
+ message = "The specified plugin is not found" if message.empty?
242
+ super(logger, message)
243
+ end
244
+ end
236
245
  end
237
246
  end