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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +23 -0
- data/.github/workflows/ci.yml +57 -0
- data/.github/workflows/release.yml +60 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +33 -0
- data/.rubocop_todo.yml +572 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +5 -0
- data/README.md +118 -23
- data/Rakefile +17 -2
- data/cloudstack_client.gemspec +23 -4
- data/lib/cloudstack_client/api.rb +39 -14
- data/lib/cloudstack_client/cli.rb +1 -1
- data/lib/cloudstack_client/client.rb +4 -1
- data/lib/cloudstack_client/configuration.rb +14 -2
- data/lib/cloudstack_client/connection.rb +26 -62
- data/lib/cloudstack_client/error.rb +1 -0
- data/lib/cloudstack_client/request_handling.rb +91 -0
- data/lib/cloudstack_client/version.rb +1 -1
- data/test/api_test.rb +23 -1
- data/test/client_test.rb +147 -13
- data/test/configuration_test.rb +84 -0
- data/test/connection_test.rb +364 -0
- data/test/data/cloudstack-empty.yml +3 -0
- data/test/data/cloudstack-incomplete.yml +5 -0
- data/test/data/cloudstack-malformed.yml +5 -0
- data/test/data/cloudstack-unsafe.yml +1 -0
- data/test/support/api_stubs.rb +85 -0
- data/test/test_helper.rb +38 -1
- data/test/utils_test.rb +62 -0
- metadata +131 -19
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -27
data/README.md
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
# cloudstack_client
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/cloudstack_client)
|
|
4
|
+
[](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
|
-
|
|
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:
|
|
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.
|
|
22
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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`.
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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]
|
data/cloudstack_client.gemspec
CHANGED
|
@@ -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
|
|
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 = '>=
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
!find_command(command).nil?
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def command_supports_param?(command, key)
|
|
32
|
-
command =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
112
|
+
@underscored_commands = nil
|
|
113
|
+
parsed = Zlib::GzipReader.open(@api_file) do |gz|
|
|
95
114
|
JSON.parse(gz.read)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
127
|
+
def max_tries(timeout, interval)
|
|
128
|
+
(timeout / interval).round
|
|
165
129
|
end
|
|
166
130
|
|
|
167
131
|
def escape(input)
|