cloudstack_client 1.5.12 → 2.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.
data/README.md CHANGED
@@ -1,25 +1,34 @@
1
1
  # cloudstack_client
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/cloudstack_client.png)](http://badge.fury.io/rb/cloudstack_client)
4
- [![Build Status](https://travis-ci.com/niwo/cloudstack_client.svg?branch=master)](https://travis-ci.com/niwo/cloudstack_client)
3
+ [![Gem Version](https://img.shields.io/gem/v/cloudstack_client.svg)](https://rubygems.org/gems/cloudstack_client)
4
+ [![CI](https://github.com/niwo/cloudstack_client/actions/workflows/ci.yml/badge.svg)](https://github.com/niwo/cloudstack_client/actions/workflows/ci.yml)
5
5
 
6
6
  A CloudStack API client written in Ruby.
7
7
 
8
+ ## Version 2.0.0
9
+
10
+ Version 2.0.0 requires Ruby 3.0 or newer. It also fixes request dispatch when
11
+ no custom host is configured and improves support for CloudStack commands with
12
+ acronyms, such as `create_ssh_key_pair`.
13
+
8
14
  ## Installation
9
15
 
10
16
  Install the cloudstack_client gem:
11
17
 
12
18
  ```bash
13
- $ gem install cloudstack_client
19
+ gem install cloudstack_client
14
20
  ```
15
21
 
16
22
  ## Features
17
23
 
18
24
  - Access to the whole CloudStack-API from Ruby
19
- - Interactive console for playing with the CloudStack API: ```cloudstack_client console```
25
+ - Interactive console for playing with the CloudStack API:
26
+ `cloudstack_client console`
20
27
  - Dynamically builds API methods based on the listApis function of CloudStack
21
- - Command names are converted to match Ruby naming conventions (i.e. ListVirtualMachines becomes list_virtual_machines)
22
- - Accepts Ruby Hash arguments passed to commands as options (i.e. list_all: true becomes listall=true)
28
+ - Command names are converted to match Ruby naming conventions (i.e.
29
+ ListVirtualMachines becomes list_virtual_machines)
30
+ - Accepts Ruby Hash arguments passed to commands as options (i.e. `list_all: true`
31
+ becomes `listall=true`)
23
32
  - Assure all required arguments are passed
24
33
  - Removes unsupported arguments and arguments with nil values from commands
25
34
 
@@ -70,11 +79,15 @@ cs = CloudstackClient::Client.new(
70
79
 
71
80
  ### Pagination and Response Options
72
81
 
73
- When working with paginated responses, you can include the total count in the API response:
82
+ When working with paginated responses, you can include the total count in the
83
+ API response:
74
84
 
75
85
  ```ruby
76
86
  # Get paginated results with count information
77
- vms = cs.list_virtual_machines({ page: 1, pagesize: 10 }, { include_count: true })
87
+ vms = cs.list_virtual_machines(
88
+ { page: 1, pagesize: 10 },
89
+ { include_count: true }
90
+ )
78
91
  total_count = vms[:count]
79
92
  items = vms[:virtualmachine]
80
93
 
@@ -85,9 +98,10 @@ vms = cs.list_virtual_machines(page: 1, pagesize: 10)
85
98
 
86
99
  ### Using the configuration module
87
100
 
88
- The configuration module of CloudstackClient makes it easy to load CloudStack API settings from configuration files.
101
+ The configuration module of CloudstackClient makes it easy to load CloudStack
102
+ API settings from configuration files.
89
103
 
90
- #### Example
104
+ #### Configuration Example
91
105
 
92
106
  ```ruby
93
107
  require "cloudstack_client"
@@ -95,7 +109,11 @@ require "cloudstack_client/configuration"
95
109
 
96
110
  # looks for ~/.cloudstack.yml per default
97
111
  config = CloudstackClient::Configuration.load
98
- cs = CloudstackClient::Client.new(config[:url], config[:api_key], config[:secret_key])
112
+ cs = CloudstackClient::Client.new(
113
+ config[:url],
114
+ config[:api_key],
115
+ config[:secret_key]
116
+ )
99
117
  ```
100
118
 
101
119
  #### Configuration files
@@ -121,43 +139,118 @@ test:
121
139
 
122
140
  ### Configuration options
123
141
 
124
- You can pass `options` as 4th argument in `CloudstackClient::Client.new`. All its keys are optional.
142
+ You can pass `options` as 4th argument in `CloudstackClient::Client.new`.
143
+ All its keys are optional.
125
144
 
126
145
  ```ruby
127
146
  options = {
128
- symbolize_keys: true, # pass symbolize_names: true in JSON#parse for Cloudstack responses, default: false
129
- host: 'localhost', # custom host header to be used in Net::Http. May be useful when Cloudstack is set up locally via docker (i.e. Cloudstack-simulator), default: parsed from config[:url] via Net::Http
130
- read_timeout: 10, # timeout in seconds of a connection to the Cloudstack, default: 60
131
- request_retries: 3 # number of attempts for HTTP requests before raising a ConnectionError, default: 1 (no retries). Uses incremental back-off between attempts.
147
+ # Pass symbolize_names: true in JSON#parse for CloudStack responses.
148
+ # Default: false.
149
+ symbolize_keys: true,
150
+ # Custom host header for Net::HTTP. Useful with CloudStack-simulator.
151
+ # Default: parsed from config[:url] via Net::HTTP.
152
+ host: 'localhost',
153
+ # Timeout in seconds for a connection to CloudStack. Default: 60.
154
+ read_timeout: 10,
155
+ # HTTP request attempts before raising ConnectionError. Default: 1.
156
+ # Uses incremental back-off between attempts.
157
+ request_retries: 3,
158
+ # Verify HTTPS certificates. Default: true.
159
+ verify_ssl: true,
160
+ # Optional path to a custom CA certificate bundle.
161
+ ca_file: "/path/to/ca-bundle.pem",
162
+ # Raise ParameterError for unsupported command parameters. Default: false.
163
+ strict_params: false
132
164
  }
133
- cs = CloudstackClient::Client.new(config[:url], config[:api_key], config[:secret_key], options)
165
+ cs = CloudstackClient::Client.new(
166
+ config[:url],
167
+ config[:api_key],
168
+ config[:secret_key],
169
+ options
170
+ )
171
+ ```
172
+
173
+ HTTPS certificate verification is enabled by default. Set `verify_ssl: false`
174
+ only when connecting to a trusted endpoint with a certificate that cannot be
175
+ validated, such as a local development server.
176
+
177
+ By default, unsupported command parameters are ignored. Set
178
+ `strict_params: true` to raise `ParameterError` instead.
179
+
180
+ For a single call you can override defaults on the **second** hash (client
181
+ options), without changing the client instance:
182
+
183
+ ```ruby
184
+ cs.deploy_virtual_machine(
185
+ { zoneid: "...", serviceofferingid: "...", templateid: "..." },
186
+ async_timeout: 600,
187
+ async_poll_interval: 5
188
+ )
134
189
  ```
135
190
 
136
191
  ### Interactive Console
137
192
 
138
193
  cloudstack_client comes with an interactive console.
139
194
 
140
- #### Example
195
+ #### Console Example
141
196
 
142
197
  ```bash
143
- $ cloudstack_client console -e prod
198
+ cloudstack_client console -e prod
144
199
  prod >> list_virtual_machines
145
200
  ```
146
201
 
147
202
  ## Development
148
203
 
204
+ ### Running the tests
205
+
206
+ ```bash
207
+ bundle install
208
+ bundle exec rake # tests and RuboCop
209
+ bundle exec rake test # tests only
210
+ bundle exec rake rubocop # RuboCop only
211
+ bundle exec rake benchmark
212
+ ```
213
+
214
+ Tests use [Minitest](https://github.com/minitest/minitest) in spec style and
215
+ [WebMock](https://github.com/bblimke/webmock). Outbound network access is
216
+ disabled in the test suite, so any request that is not stubbed fails the test
217
+ rather than reaching a real endpoint. Helpers for stubbing CloudStack responses
218
+ live in `test/support/api_stubs.rb`.
219
+
220
+ `Gemfile.lock` is deliberately not checked in, so each supported Ruby version
221
+ resolves its own compatible dependency set.
222
+
149
223
  ### Generate or update API definitions
150
224
 
151
225
  New API definitions can be generated using the `list_apis` command.
152
226
 
153
- #### Example
227
+ #### API Definition Example
154
228
 
155
229
  ```bash
156
230
  # running against a CloudStack 4.15 API endpoint:
157
- $ cloudstack_client list_apis > data/4.15.json
158
- $ gzip data/4.15.json
231
+ cloudstack_client list_apis > data/4.15.json
232
+ gzip data/4.15.json
159
233
  ```
160
234
 
235
+ ### GitHub Actions
236
+
237
+ This repository includes GitHub Actions workflows for:
238
+
239
+ - Running tests and gem build against every supported Ruby version, plus a
240
+ RuboCop lint job, on every push and pull request (`CI`)
241
+ - Publishing the gem to RubyGems when a GitHub Release is published (`Release`)
242
+
243
+ Dependency and GitHub Actions updates are proposed weekly by Dependabot.
244
+
245
+ To enable publishing, configure a RubyGems trusted publisher for this
246
+ repository, workflow, and `rubygems` environment. The release workflow grants
247
+ the publish job GitHub's OIDC `id-token` permission, so no RubyGems API key
248
+ secret is required.
249
+
250
+ The release workflow checks that `CloudstackClient::VERSION` is greater than
251
+ the latest version on RubyGems before building, then uses the `rubygems`
252
+ environment to publish.
253
+
161
254
  ## References
162
255
 
163
256
  - [Apache CloudStack API documentation](http://cloudstack.apache.org/api/apidocs-4.15/)
@@ -172,4 +265,6 @@ $ gzip data/4.15.json
172
265
 
173
266
  ## License
174
267
 
175
- Released under the MIT License. See the [LICENSE](https://raw.github.com/niwo/cloudstack_client/master/LICENSE.txt) file for further details.
268
+ Released under the MIT License. See the
269
+ [LICENSE](https://raw.github.com/niwo/cloudstack_client/master/LICENSE.txt)
270
+ file for further details.
data/Rakefile CHANGED
@@ -9,5 +9,20 @@ Rake::TestTask.new do |t|
9
9
  t.warning = false
10
10
  end
11
11
 
12
- desc "Run Tests"
13
- task default: :test
12
+ begin
13
+ require 'rubocop/rake_task'
14
+ RuboCop::RakeTask.new
15
+ rescue LoadError
16
+ desc "rubocop is not available"
17
+ task :rubocop do
18
+ abort "RuboCop is not available. Run `bundle install` first."
19
+ end
20
+ end
21
+
22
+ desc "Run the API benchmark"
23
+ task :benchmark do
24
+ ruby "-Ilib test/benchmark.rb"
25
+ end
26
+
27
+ desc "Run tests and RuboCop"
28
+ task default: %i[test rubocop]
@@ -8,20 +8,39 @@ Gem::Specification.new do |gem|
8
8
  gem.version = CloudstackClient::VERSION
9
9
  gem.authors = ["Nik Wolfgramm"]
10
10
  gem.email = ["nik.wolfgramm@gmail.com"]
11
- gem.description = %q{CloudStack API client written in Ruby}
11
+ gem.description = %q{A Ruby client for the Apache CloudStack API that builds its
12
+ methods dynamically from the CloudStack API definition,
13
+ including an interactive console.}.gsub(/\s+/, ' ')
12
14
  gem.summary = %q{CloudStack API client written in Ruby}
13
15
  gem.homepage = "https://github.com/niwo/cloudstack_client"
14
16
  gem.license = 'MIT'
15
17
 
16
- gem.required_ruby_version = '>= 1.9.3'
18
+ gem.required_ruby_version = '>= 3.0'
17
19
  gem.files = `git ls-files`.split($/)
18
20
  gem.executables = %w(cloudstack_client)
19
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
21
  gem.require_paths = ["lib"]
21
22
  gem.rdoc_options = %w[--line-numbers --inline-source]
22
23
 
24
+ gem.metadata = {
25
+ "homepage_uri" => gem.homepage,
26
+ "bug_tracker_uri" => "#{gem.homepage}/issues",
27
+ "changelog_uri" => "#{gem.homepage}/releases",
28
+ "rubygems_mfa_required" => "true"
29
+ }
30
+
31
+ # base64 stopped being a default gem in Ruby 3.4, so `require "base64"`
32
+ # in connection.rb fails under Bundler unless it is declared here.
33
+ gem.add_dependency('base64', '>= 0.1')
34
+
23
35
  gem.add_development_dependency('rake', '~> 13.0')
24
36
  gem.add_development_dependency('thor', '~> 1.1')
25
37
  gem.add_development_dependency('ripl', '~> 0.7')
26
- gem.add_development_dependency('minitest', '~> 5.14')
38
+ # Allow minitest 6 on Ruby >= 3.2 while staying installable on Ruby 3.0/3.1.
39
+ gem.add_development_dependency('minitest', '>= 5.14', '< 7')
40
+ gem.add_development_dependency('minitest-reporters', '~> 1.8')
41
+ gem.add_development_dependency('webmock', '~> 3.26')
42
+ gem.add_development_dependency('benchmark', '>= 0.3')
43
+ gem.add_development_dependency('rubocop', '~> 1.90')
44
+ gem.add_development_dependency('rubocop-minitest', '~> 0.40')
45
+ gem.add_development_dependency('rubocop-rake', '~> 0.7')
27
46
  end
@@ -1,5 +1,6 @@
1
1
  require "zlib"
2
2
  require "json"
3
+ require "cloudstack_client/error"
3
4
  require "cloudstack_client/utils"
4
5
 
5
6
  module CloudstackClient
@@ -25,14 +26,29 @@ module CloudstackClient
25
26
  end
26
27
 
27
28
  def command_supported?(command)
28
- @commands.has_key? underscore_to_camel_case(command)
29
+ !find_command(command).nil?
29
30
  end
30
31
 
31
32
  def command_supports_param?(command, key)
32
- command = underscore_to_camel_case(command)
33
- @commands[command]["params"].detect do |params|
34
- params["name"] == key.to_s
35
- end ? true : false
33
+ command = find_command(command)
34
+ return false if command.nil?
35
+
36
+ command["params"].any? { |param| param["name"] == key.to_s }
37
+ end
38
+
39
+ # Resolves a command given either its CloudStack name ("createSSHKeyPair")
40
+ # or its underscored Ruby name ("create_ssh_key_pair").
41
+ #
42
+ # underscore_to_camel_case cannot be relied on here: converting to
43
+ # underscores loses acronym casing, so "create_ssh_key_pair" converts back
44
+ # to "createSshKeyPair", which is not a CloudStack command. Underscored
45
+ # names are resolved through an index built in the lossy direction instead.
46
+ #
47
+ # The index is built on first use: most lookups arrive as CloudStack names
48
+ # and hit @commands directly, so instantiating an Api should not pay for it.
49
+ def find_command(command)
50
+ command = command.to_s
51
+ @commands[command] || underscored_commands[command]
36
52
  end
37
53
 
38
54
  def required_params(command)
@@ -68,11 +84,7 @@ module CloudstackClient
68
84
  end
69
85
 
70
86
  def set_api_path(options)
71
- @api_path = if options[:api_path]
72
- File.expand_path(options[:api_path])
73
- else
74
- API_PATH
75
- end
87
+ @api_path = options[:api_path] ? File.expand_path(options[:api_path]) : API_PATH
76
88
  end
77
89
 
78
90
  def set_api_version(options)
@@ -89,13 +101,26 @@ module CloudstackClient
89
101
  @api_version
90
102
  end
91
103
 
104
+ def underscored_commands
105
+ @underscored_commands ||= @commands.each_with_object({}) do |(name, command), index|
106
+ index[camel_case_to_underscore(name)] = command
107
+ end
108
+ end
109
+
92
110
  def load_commands
93
111
  @commands = {}
94
- Zlib::GzipReader.open(@api_file) do |gz|
112
+ @underscored_commands = nil
113
+ parsed = Zlib::GzipReader.open(@api_file) do |gz|
95
114
  JSON.parse(gz.read)
96
- end.each {|cmd| @commands[cmd["name"]] = cmd }
97
- rescue => e
98
- raise "Error: Unable to read file '#{@api_file}': #{e.message}"
115
+ end
116
+ unless parsed.is_a?(Array) && parsed.all?(Hash)
117
+ raise ApiDefinitionError,
118
+ "Unable to read API definition '#{@api_file}': unexpected format (expected an array of command hashes)"
119
+ end
120
+ parsed.each { |cmd| @commands[cmd["name"]] = cmd }
121
+ rescue Zlib::Error, JSON::ParserError, SystemCallError, EOFError => e
122
+ raise ApiDefinitionError,
123
+ "Unable to read API definition '#{@api_file}': #{e.message}"
99
124
  end
100
125
 
101
126
  end
@@ -98,7 +98,7 @@ module CloudstackClient
98
98
 
99
99
  ARGV.clear
100
100
  Ripl.config[:prompt] = "#{@config[:environment]} >> "
101
- Ripl.start binding: cs_client.instance_eval{ binding }
101
+ Ripl.start binding: cs_client.instance_eval { binding }
102
102
  end
103
103
 
104
104
  no_commands do
@@ -25,8 +25,11 @@ module CloudstackClient
25
25
 
26
26
  args.each do |k, v|
27
27
  k = k.to_s.gsub("_", "")
28
- if v && @api.command_supports_param?(command["name"], k)
28
+ if !v.nil? && @api.command_supports_param?(command["name"], k)
29
29
  params[k] = v
30
+ elsif !v.nil? && options.fetch(:strict_params, @options[:strict_params])
31
+ raise ParameterError,
32
+ "#{command['name']} does not support parameter #{k}"
30
33
  end
31
34
  end
32
35
 
@@ -10,7 +10,7 @@ module CloudstackClient
10
10
  end
11
11
 
12
12
  begin
13
- config = YAML::load(IO.read file)
13
+ config = YAML.safe_load(IO.read(file), permitted_classes: [Symbol])
14
14
  rescue => e
15
15
  message = "Can't load configuration from file '#{file}'."
16
16
  if configuration[:debug]
@@ -20,13 +20,25 @@ module CloudstackClient
20
20
  raise ConfigurationError, message
21
21
  end
22
22
 
23
+ unless config.is_a?(Hash)
24
+ raise ConfigurationError, "Configuration file '#{file}' must contain a hash."
25
+ end
26
+
23
27
  if env = configuration[:env] || config[:default]
24
28
  unless config = config[env]
25
29
  raise ConfigurationError, "Can't find environment #{env}."
26
30
  end
27
31
  end
28
32
 
29
- unless config.key?(:url) && config.key?(:api_key) && config.key?(:secret_key)
33
+ unless config.is_a?(Hash)
34
+ raise ConfigurationError, "Environment #{env} must contain a hash."
35
+ end
36
+
37
+ required_keys = %i[url api_key secret_key]
38
+ missing_keys = required_keys.reject do |key|
39
+ config.key?(key) && !config[key].to_s.strip.empty?
40
+ end
41
+ unless missing_keys.empty?
30
42
  message = "The environment #{env || '\'-\''} does not contain all required keys."
31
43
  raise ConfigurationError, message
32
44
  end
@@ -4,12 +4,16 @@ require "uri"
4
4
  require "cgi"
5
5
  require "net/http"
6
6
  require "json"
7
+ require "cloudstack_client/request_handling"
7
8
 
8
9
  module CloudstackClient
9
10
  class Connection
10
11
  include Utils
12
+ include RequestHandling
11
13
 
12
- attr_accessor :api_url, :api_key, :secret_key, :verbose, :debug, :symbolize_keys, :host, :read_timeout
14
+ attr_reader :api_key, :secret_key
15
+ attr_accessor :api_url, :verbose, :debug, :symbolize_keys, :host, :read_timeout
16
+ attr_accessor :verify_ssl, :ca_file
13
17
  attr_accessor :async_poll_interval, :async_timeout, :request_retries
14
18
 
15
19
  DEF_POLL_INTERVAL = 2.0
@@ -25,6 +29,8 @@ module CloudstackClient
25
29
  @debug = options[:debug] ? true : false
26
30
  @symbolize_keys = options[:symbolize_keys] ? true : false
27
31
  @host = options[:host]
32
+ @verify_ssl = options.fetch(:verify_ssl, true)
33
+ @ca_file = options[:ca_file]
28
34
  @read_timeout = options[:read_timeout] || DEF_REQ_TIMEOUT
29
35
  @async_poll_interval = options[:async_poll_interval] || DEF_POLL_INTERVAL
30
36
  @async_timeout = options[:async_timeout] || DEF_ASYNC_TIMEOUT
@@ -37,66 +43,16 @@ module CloudstackClient
37
43
  # Sends a synchronous request to the CloudStack API and returns the response as a Hash.
38
44
  #
39
45
 
40
- def send_request(params, opts = {})
41
- params['response'] = 'json'
42
- params['apiKey'] = @api_key
43
- print_debug_output JSON.pretty_generate(params) if @debug
44
-
45
- data = params_to_data(params)
46
- uri = URI.parse "#{@api_url}?#{data}&signature=#{create_signature(data)}"
47
-
48
- http = Net::HTTP.new(uri.host, uri.port)
49
- if uri.scheme == 'https'
50
- http.use_ssl = true
51
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
52
- end
53
- http.read_timeout = @read_timeout
54
-
55
- retries = 0
56
- begin
57
- req = Net::HTTP::Get.new(uri.request_uri)
58
- req['Host'] = host if host.present?
59
- response = http.request(req)
60
- rescue => e
61
- retries += 1
62
- if retries < @request_retries
63
- sleep(retries) # incremental back-off
64
- print "." if @verbose
65
- retry
66
- end
67
- raise ConnectionError, "API URL \'#{@api_url}\' is not reachable (after #{retries} attempt#{'s' if retries > 1}): #{e.message}"
68
- end
69
-
70
- begin
71
- body = JSON.parse(response.body, symbolize_names: @symbolize_keys).values.first
72
- rescue JSON::ParserError
73
- raise ParseError,
74
- "Response from server is not readable. Check if the API endpoint (#{@api_url}) is valid and accessible."
75
- end
76
-
77
- if response.is_a?(Net::HTTPOK)
78
- return body unless body.respond_to?(:keys)
79
- if body.size == 2 && body.key?(k('count'))
80
- return opts[:include_count] ? body : body.reject { |key, _| key == k('count') }.values.first
81
- elsif body.size == 1 && body.values.first.respond_to?(:keys)
82
- item = body.values.first
83
- return (item.is_a?(Array) || item.is_a?(Hash)) ? item : []
84
- else
85
- body.reject! { |key, _| key == k('count') } if body.key?(k('count')) && !opts[:include_count]
86
- body.size == 0 ? [] : body
87
- end
88
- else
89
- message = body[k('errortext')] rescue body
90
- raise ApiError, "Status #{response.code}: #{message}."
91
- end
92
- end
93
-
94
46
  ##
95
47
  # Sends an asynchronous request and waits for the response.
96
48
  #
97
49
  # The contents of the 'jobresult' element are returned upon completion of the command.
98
50
 
99
51
  def send_async_request(params, opts = {})
52
+ request_timeout = opts[:async_timeout] || @async_timeout
53
+ poll_interval = opts[:async_poll_interval] || @async_poll_interval
54
+ validate_async_timeouts!(request_timeout, poll_interval)
55
+
100
56
  data = send_request(params, opts)
101
57
 
102
58
  params = {
@@ -104,7 +60,7 @@ module CloudstackClient
104
60
  'jobid' => data[k('jobid')]
105
61
  }
106
62
 
107
- max_tries.times do
63
+ max_tries(request_timeout, poll_interval).times do
108
64
  data = send_request(params)
109
65
  print "." if @verbose
110
66
 
@@ -112,11 +68,15 @@ module CloudstackClient
112
68
  when 1
113
69
  return data[k('jobresult')]
114
70
  when 2
115
- raise JobError, "Request failed (#{data[k('jobresultcode')]}): #{data[k('jobresult')][k('errortext')]}."
71
+ result = data[k('jobresult')]
72
+ error_text = result.is_a?(Hash) ? result[k('errortext')] : nil
73
+ error_text ||= "Unknown error"
74
+ raise JobError,
75
+ "Request failed (#{data[k('jobresultcode')]}): #{error_text}."
116
76
  end
117
77
 
118
78
  STDOUT.flush if @verbose
119
- sleep @async_poll_interval
79
+ sleep poll_interval
120
80
  end
121
81
 
122
82
  raise TimeoutError, "Asynchronous request timed out."
@@ -128,11 +88,15 @@ module CloudstackClient
128
88
  raise InputError, "API URL not set." if @api_url == nil
129
89
  raise InputError, "API KEY not set." if @api_key == nil
130
90
  raise InputError, "API SECRET KEY not set." if @secret_key == nil
131
- raise InputError, "ASYNC POLL INTERVAL must be at least 1." if @async_poll_interval < 1.0
132
- raise InputError, "ASYNC TIMEOUT must be at least 60." if @async_timeout < 60
91
+ validate_async_timeouts!(@async_timeout, @async_poll_interval)
133
92
  raise InputError, "REQUEST RETRIES must be at least 1." if @request_retries < 1
134
93
  end
135
94
 
95
+ def validate_async_timeouts!(timeout, interval)
96
+ raise InputError, "ASYNC POLL INTERVAL must be at least 1." if interval < 1.0
97
+ raise InputError, "ASYNC TIMEOUT must be at least 60." if timeout < 60
98
+ end
99
+
136
100
  def params_to_data(params)
137
101
  params_arr = params.sort.map do |key, value|
138
102
  case value
@@ -160,8 +124,8 @@ module CloudstackClient
160
124
  CGI.escape(signature)
161
125
  end
162
126
 
163
- def max_tries
164
- (@async_timeout / @async_poll_interval).round
127
+ def max_tries(timeout, interval)
128
+ (timeout / interval).round
165
129
  end
166
130
 
167
131
  def escape(input)
@@ -4,6 +4,7 @@ module CloudstackClient
4
4
  class ConnectionError < Error; end
5
5
  class ConfigurationError < Error; end
6
6
  class ParseError < Error; end
7
+ class ApiDefinitionError < Error; end
7
8
  class ApiError < Error; end
8
9
  class JobError < Error; end
9
10
  class TimeoutError < Error; end