cloud_stack_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.hgignore +19 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.md +70 -0
- data/Rakefile +1 -0
- data/assets/cacert.pem +3825 -0
- data/bin/cloud-stack.rb +6 -0
- data/cloud_stack_client.gemspec +23 -0
- data/lib/cloud_stack_client.rb +8 -0
- data/lib/cloud_stack_client/api.rb +325 -0
- data/lib/cloud_stack_client/cli.rb +239 -0
- data/lib/cloud_stack_client/connector.rb +155 -0
- data/lib/cloud_stack_client/definitions.rb +29 -0
- data/lib/cloud_stack_client/helpers.rb +7 -0
- data/lib/cloud_stack_client/version.rb +4 -0
- metadata +66 -0
data/bin/cloud-stack.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cloud_stack_client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "cloud_stack_client"
|
8
|
+
gem.version = CloudStackClient::VERSION
|
9
|
+
gem.authors = ["Roman Messer"]
|
10
|
+
gem.email = ["roman.messer@ict-cloud.com"]
|
11
|
+
gem.description = %q{Execute any command against a CloudStack Infrastructure. Also contains a commandline utility with auto paging and support to read credentials from a config file.}
|
12
|
+
gem.summary = %q{A simple library to work with the CloudStack API.}
|
13
|
+
# gem.homepage = ""
|
14
|
+
|
15
|
+
if File.exist? '.git'
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
elsif File.exist? '.hg'
|
18
|
+
gem.files = `hg manifest`.split($/)
|
19
|
+
end
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
# An API to interact with the Citrix CloudStack Platform
|
2
|
+
|
3
|
+
require "cloud_stack_client/helpers"
|
4
|
+
require "cloud_stack_client/definitions"
|
5
|
+
require "digest/md5"
|
6
|
+
autoload :URI, "uri"
|
7
|
+
autoload :OpenSSL, "openssl"
|
8
|
+
autoload :Base64, "base64"
|
9
|
+
autoload :JSON, "json"
|
10
|
+
# Autoloading Net submodules
|
11
|
+
# @private
|
12
|
+
module Net
|
13
|
+
autoload :HTTP, "net/http"
|
14
|
+
end
|
15
|
+
# Autoloading REXML submodules
|
16
|
+
# @private
|
17
|
+
module REXML
|
18
|
+
autoload :Document, "rexml/document"
|
19
|
+
autoload :QuickPath, "rexml/quickpath"
|
20
|
+
end
|
21
|
+
|
22
|
+
module CloudStackClient
|
23
|
+
# Collection of functions to work with the API
|
24
|
+
module API
|
25
|
+
|
26
|
+
# Login with username, password hash and domain to get session credentials
|
27
|
+
#
|
28
|
+
# @param api_uri [String] HTTP URL of the CloudStack API
|
29
|
+
# @param username [String] CloudStack login user
|
30
|
+
# @param hashed_password [String] CloudStack login password (use {CloudStackClient::Helpers::hash_password})
|
31
|
+
# @param domain [String] CloudStack login domain
|
32
|
+
# @param http_method [Symbol] The HTTP method to submit the query (:post or :get)
|
33
|
+
# @param ssl_check [Boolean] Check Server SSL certificate?
|
34
|
+
# @return [Hash] Credentials for subsequent queries
|
35
|
+
# @see CloudStackClient::Helpers::hash_password
|
36
|
+
def self.get_credentials(api_uri, username, hashed_password, domain = "", http_method = nil , ssl_check = nil)
|
37
|
+
http_method ||= API_DEFAULTS[:http_method]
|
38
|
+
ssl_check ||= API_DEFAULTS[:ssl_check]
|
39
|
+
parameters = {:command => "login", :response => "json", :username => username, :password => hashed_password, :domain => parse_domain(domain)}
|
40
|
+
headers, body = execute_query(api_uri, generate_query(parameters), [], http_method, ssl_check)
|
41
|
+
credentials = {}
|
42
|
+
if headers.has_key? "set-cookie" # Expect lowercase names from Net::HTTPHeader
|
43
|
+
credentials[:session_cookies] = headers["set-cookie"].map {|raw| raw.split(";").first}
|
44
|
+
end
|
45
|
+
json = JSON.parse body
|
46
|
+
return nil unless CloudStackClient::API::response_is_success? json
|
47
|
+
credentials[:session_key] = json["loginresponse"]["sessionkey"]
|
48
|
+
credentials[:user_id] = json["loginresponse"]["userid"]
|
49
|
+
credentials
|
50
|
+
end
|
51
|
+
|
52
|
+
# Test connection with supplied Secret key and API key
|
53
|
+
#
|
54
|
+
# @param api_uri [String] HTTP URL of the CloudStack API
|
55
|
+
# @param api_key [String] CloudStack API Key
|
56
|
+
# @param secret_key [String] Cloudstack Secret Key
|
57
|
+
# @param http_method [Symbol] The HTTP method to submit the query (:post or :get)
|
58
|
+
# @param ssl_check [Boolean] Check Server SSL certificate?
|
59
|
+
# @return [Boolean]
|
60
|
+
def self.verify_keys(api_uri, api_key, secret_key, http_method = nil, ssl_check = nil)
|
61
|
+
http_method ||= API_DEFAULTS[:http_method]
|
62
|
+
ssl_check ||= API_DEFAULTS[:ssl_check]
|
63
|
+
CloudStackClient::API::response_is_success? query(api_uri, {:command => "listCapabilities"}, {:api_key => api_key, :secret_key => secret_key}, http_method, ssl_check)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Execute a query
|
67
|
+
#
|
68
|
+
# @param api_uri [String] HTTP URL of the CloudStack API
|
69
|
+
# @param parameters [Hash] A Hash containing all query parameters. The keys can be specified as Symbol or String
|
70
|
+
# @param credentials [Hash] Contains either :api_key and :secret_key or :session_key and :session_cookies as obtained from {get_credentials}
|
71
|
+
# @param http_method [Symbol] The HTTP method to submit the query (:post or :get)
|
72
|
+
# @param ssl_check [Boolean] Check Server SSL certificate?
|
73
|
+
# @return [REXML::Document, Hash] An XML Document or the parsed JSON (commonly a Hash)
|
74
|
+
def self.query(api_uri, parameters, credentials, http_method = nil, ssl_check = nil)
|
75
|
+
http_method ||= API_DEFAULTS[:http_method]
|
76
|
+
ssl_check ||= API_DEFAULTS[:ssl_check]
|
77
|
+
if credentials.is_a?(Hash) && credentials.has_key?(:api_key) && credentials.has_key?(:secret_key)
|
78
|
+
parameters[:apiKey] = credentials[:api_key]
|
79
|
+
parameters[:secret] = credentials[:secret_key]
|
80
|
+
parameters[:timestamp] = Time.new.to_i
|
81
|
+
headers, body = self.execute_query(api_uri, generate_query(parameters), [], http_method, ssl_check)
|
82
|
+
elsif credentials.is_a? Hash
|
83
|
+
parameters[:sessionkey] = credentials[:session_key] if credentials.has_key? :session_key
|
84
|
+
headers, body = self.execute_query(api_uri, generate_query(parameters), credentials[:session_cookies], http_method, ssl_check)
|
85
|
+
end
|
86
|
+
|
87
|
+
format = nil
|
88
|
+
if headers["content-type"]
|
89
|
+
format = headers["content-type"][0].split(";").first
|
90
|
+
end
|
91
|
+
|
92
|
+
case format
|
93
|
+
when "text/javascript"
|
94
|
+
JSON.parse body
|
95
|
+
when "text/xml"
|
96
|
+
REXML::Document.new body
|
97
|
+
else
|
98
|
+
body
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Executes the query like query() but merges all result pages into one
|
103
|
+
#
|
104
|
+
# @param api_uri [String] HTTP URL of the CloudStack API
|
105
|
+
# @param parameters [Hash] A Hash containing all query parameters. The keys can be specified as Symbol or String
|
106
|
+
# @param credentials [Hash] Contains either :api_key and :secret_key or :session_key and :session_cookies as obtained from {get_credentials}
|
107
|
+
# @param http_method [Symbol] The HTTP method to submit the query (:post or :get)
|
108
|
+
# @param ssl_check [Boolean] Check Server SSL certificate?
|
109
|
+
# @return [REXML::Document, Hash] An XML Document or the parsed JSON (commonly a Hash)
|
110
|
+
def self.query_with_auto_paging(api_uri, parameters, credentials, page_size = nil, http_method = nil, ssl_check = nil)
|
111
|
+
page_size ||= API_DEFAULTS[:page_size]
|
112
|
+
http_method ||= API_DEFAULTS[:http_method]
|
113
|
+
ssl_check ||= API_DEFAULTS[:ssl_check]
|
114
|
+
output = nil
|
115
|
+
parameters[:page] = 0
|
116
|
+
parameters[:pageSize] = page_size
|
117
|
+
has_count = false
|
118
|
+
|
119
|
+
begin
|
120
|
+
parameters[:page] += 1
|
121
|
+
data = self.query api_uri, parameters, credentials, http_method, ssl_check
|
122
|
+
return nil unless data
|
123
|
+
format = :json
|
124
|
+
format = :xml if data.is_a? REXML::Document
|
125
|
+
|
126
|
+
# Remove count attribute from result data
|
127
|
+
if format == :xml
|
128
|
+
count_data = REXML::QuickPath.first(data.root, "//count")
|
129
|
+
if count_data
|
130
|
+
count = count_data.text.to_i
|
131
|
+
count_data.remove
|
132
|
+
has_count = true
|
133
|
+
else
|
134
|
+
count = 0
|
135
|
+
end
|
136
|
+
elsif format == :json
|
137
|
+
if data.first[1].has_key? "count"
|
138
|
+
count = data.first[1]["count"]
|
139
|
+
data.first[1].delete "count"
|
140
|
+
has_count = true
|
141
|
+
else
|
142
|
+
count = 0
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Copy first page in total
|
147
|
+
if !output
|
148
|
+
output = data
|
149
|
+
loop_done = count < parameters[:pageSize]
|
150
|
+
next
|
151
|
+
end
|
152
|
+
|
153
|
+
if count <= 0
|
154
|
+
# No elements
|
155
|
+
loop_done = true
|
156
|
+
elsif count >= parameters[:pageSize]
|
157
|
+
# Copy all elements if page is full
|
158
|
+
if format == :xml
|
159
|
+
data.root.elements.each {|record| output.root << record}
|
160
|
+
elsif format == :json
|
161
|
+
data.first[1].first[1].each {|record| output.first[1].first[1] << record}
|
162
|
+
end
|
163
|
+
else
|
164
|
+
# Handle last page
|
165
|
+
loop_done = true
|
166
|
+
# Detect and ignore duplicates that have already been extracted.
|
167
|
+
# E.g. guest-utilities during listIsos command
|
168
|
+
if format == :xml
|
169
|
+
pos_output = output.root.elements.size - count + 1
|
170
|
+
end_output = output.root.elements.size + 1
|
171
|
+
while pos_output < end_output
|
172
|
+
if output.root.elements[pos_output].to_s != data.root.elements[1].to_s
|
173
|
+
output.root << data.root.elements[1] # Move node to output tree
|
174
|
+
end
|
175
|
+
pos_output += 1
|
176
|
+
end
|
177
|
+
elsif format == :json
|
178
|
+
pos_data = 0
|
179
|
+
pos_output = output.first[1].first[1].size - count
|
180
|
+
end_output = output.first[1].first[1].size
|
181
|
+
while pos_output < end_output
|
182
|
+
if output.first[1].first[1][pos_output] != data.first[1].first[1][pos_data]
|
183
|
+
output.first[1].first[1] << data.first[1].first[1][pos_data]
|
184
|
+
end
|
185
|
+
pos_data += 1
|
186
|
+
pos_output += 1
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end until loop_done
|
191
|
+
|
192
|
+
# Recalculate count attribute
|
193
|
+
if has_count && CloudStackClient::API::response_is_success?(output)
|
194
|
+
if format == :xml && output.root && output.root.has_elements?
|
195
|
+
new_count = REXML::Element.new "count"
|
196
|
+
new_count.add_text output.root.elements.count.to_s
|
197
|
+
output.root << new_count
|
198
|
+
elsif format == :json && output.first && output.first[1].any?
|
199
|
+
output.first[1]["count"] = output.first[1].first[1].count
|
200
|
+
end
|
201
|
+
end
|
202
|
+
output
|
203
|
+
end
|
204
|
+
|
205
|
+
# Generate a single sign on link for the management console
|
206
|
+
#
|
207
|
+
# @param web_uri [String] The base URL of Webinterface
|
208
|
+
# @param username [String] CloudStack login user
|
209
|
+
# @param hashed_password [String] CloudStack login password (use {CloudStackClient::Helpers::hash_password})
|
210
|
+
# @param domain [String] CloudStack login domain
|
211
|
+
# @param login_parameter [String] The name of the CloudStack URL parameter that has been specified in scripts/cloud.core.callbacks.js to contain the login credentials
|
212
|
+
# @param timestamp_offset [Fixnum] Custom scripts/cloud.core.callbacks.js parameter to invalidate the login link after the specified number of seconds
|
213
|
+
# @return [String] A complete URL
|
214
|
+
# @see CloudStackClient::Helpers::hash_password
|
215
|
+
def self.generate_management_console_link(web_uri, username, hashed_password, domain = "", login_parameter = nil, timestamp_offset = nil)
|
216
|
+
login_parameter ||= API_DEFAULTS[:login_parameter]
|
217
|
+
timestamp_offset ||= API_DEFAULTS[:timestamp_offset]
|
218
|
+
uri = "#{web_uri}?#{login_parameter}="
|
219
|
+
uri += escape(generate_query({:command => "login", :response => "json", :username => username, :password => hashed_password, :domain => parse_domain(domain)}))
|
220
|
+
uri += "×tamp=#{Time.new.to_i + timestamp_offset}" if timestamp_offset
|
221
|
+
return uri
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
# Sanitize the domain
|
227
|
+
def self.parse_domain(domain = "")
|
228
|
+
domain = domain.to_s
|
229
|
+
if domain.start_with? "/"
|
230
|
+
domain
|
231
|
+
else
|
232
|
+
"/" + domain
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Make string URL safe
|
237
|
+
def self.escape(string)
|
238
|
+
URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
239
|
+
end
|
240
|
+
|
241
|
+
# Construct a query from a Hash of parameters or an Array of [key, value] Arrays
|
242
|
+
def self.generate_query(parameters = {})
|
243
|
+
if parameters.is_a? Enumerable
|
244
|
+
# Escape the key / value pairs
|
245
|
+
parameters = parameters.map {|param| [escape(param[0].to_s), escape(param[1].to_s)]}
|
246
|
+
else
|
247
|
+
parameters = [[escape(parameters.to_s), ""]]
|
248
|
+
end
|
249
|
+
secret = nil
|
250
|
+
parameters.delete_if {|key, value| secret = value if key == "secret"}
|
251
|
+
|
252
|
+
# Sign the query if secret is available
|
253
|
+
if secret
|
254
|
+
# Sort the parameters
|
255
|
+
parameters.sort!
|
256
|
+
# Generate query string
|
257
|
+
query = parameters.map {|param| param.join "="}.join("&")
|
258
|
+
# Generate signature by calculating SHA1 HMAC
|
259
|
+
hmac = OpenSSL::HMAC.digest("sha1", secret, query.downcase)
|
260
|
+
# Base64 encode 8 bit data and discard newline
|
261
|
+
base64 = Base64.encode64(hmac).strip
|
262
|
+
# Make signature URL safe
|
263
|
+
signature = escape(base64)
|
264
|
+
# Append signature to query
|
265
|
+
query += "&signature=" + signature
|
266
|
+
else
|
267
|
+
query = parameters.map {|param| param.join "="}.join("&")
|
268
|
+
end
|
269
|
+
query
|
270
|
+
end
|
271
|
+
|
272
|
+
# Submit the query to the server
|
273
|
+
def self.execute_query(api_uri, query, cookies = [], http_method = API_DEFAULTS[:http_method], ssl_check = API_DEFAULTS[:ssl_check])
|
274
|
+
uri = URI.parse api_uri
|
275
|
+
http = Net::HTTP.new uri.host, uri.port
|
276
|
+
if uri.scheme == "https"
|
277
|
+
http.use_ssl = true
|
278
|
+
# Verify server SSL certificate
|
279
|
+
if ssl_check
|
280
|
+
# On Ruby before 1.9 a local CA store is needed to verify SSL certificates.
|
281
|
+
ruby_version = RUBY_VERSION.split(".").map{|num| num.to_i}
|
282
|
+
if ruby_version[0] == 1 && ruby_version[1] < 9 || ruby_version[0] < 1
|
283
|
+
http.ca_file = File.join(File.dirname(__FILE__), "../../assets/cacert.pem")
|
284
|
+
end
|
285
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
286
|
+
end
|
287
|
+
end
|
288
|
+
request_cookies = nil
|
289
|
+
request_cookies = cookies.join(';') if cookies.is_a?(Array) && cookies.any?
|
290
|
+
if http_method == :post
|
291
|
+
request = Net::HTTP::Post.new uri.path
|
292
|
+
request["Cookie"] = request_cookies if request_cookies
|
293
|
+
response = http.request request, query
|
294
|
+
elsif http_method == :get
|
295
|
+
request = Net::HTTP::Get.new "#{uri.path}?#{query}"
|
296
|
+
request["Cookie"] = request_cookies if request_cookies
|
297
|
+
response = http.request request
|
298
|
+
else
|
299
|
+
raise "Only HTTP methods :post and :get are implemented."
|
300
|
+
end
|
301
|
+
return response.to_hash, response.body # Hash of HTTP Response header and HTTP Body
|
302
|
+
end
|
303
|
+
|
304
|
+
# Returns the format of a given reponse. Eigther :xml or :json
|
305
|
+
def self.response_format(response)
|
306
|
+
if response.is_a?(REXML::Document)
|
307
|
+
:xml
|
308
|
+
else
|
309
|
+
:json
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Checks the given response whether the command was successful
|
314
|
+
def self.response_is_success?(response)
|
315
|
+
if response.is_a? REXML::Document
|
316
|
+
!(response.root.elements["errorcode"] || response.root.name == "errorresponse")
|
317
|
+
elsif response.is_a?(Hash) && response.any?
|
318
|
+
!(response.first[1]["errorcode"] || response.first[0] == "errorresponse")
|
319
|
+
else
|
320
|
+
false
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "cloud_stack_client"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module CloudStackClient
|
5
|
+
# The commandline client 'cloud-stack.rb'
|
6
|
+
#
|
7
|
+
# Plesae run +cloud-stack.rb --help+ for additional info.
|
8
|
+
class CLI
|
9
|
+
# Default options for the command line. Can be overridden by parameters and a config file
|
10
|
+
DEFAULT_OPTIONS = {
|
11
|
+
:api_uri => nil,
|
12
|
+
:api_key => nil,
|
13
|
+
:secret_key => nil,
|
14
|
+
:username => nil,
|
15
|
+
:password => nil,
|
16
|
+
:domain => nil,
|
17
|
+
:response => "json",
|
18
|
+
:output => nil,
|
19
|
+
:debug => false,
|
20
|
+
:auto_paging => true,
|
21
|
+
:page_size => 500
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# Create new commandline instance
|
25
|
+
def initialize
|
26
|
+
@options = DEFAULT_OPTIONS.dup
|
27
|
+
@parameters = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Execute the command
|
31
|
+
#
|
32
|
+
# @param command [String] The name of the executable ($0)
|
33
|
+
# @param arguments [Array] Commandline parameters (ARGV)
|
34
|
+
# @return [Boolean] true if successful
|
35
|
+
def run(command, arguments)
|
36
|
+
# Parse commandline
|
37
|
+
pos = arguments.find_index("--config")
|
38
|
+
if pos
|
39
|
+
arguments.delete_at pos
|
40
|
+
raise "Path to config file missing" unless arguments[pos]
|
41
|
+
filename = arguments.delete_at pos
|
42
|
+
yaml = YAML.load_file filename
|
43
|
+
yaml["default"].each {|key, value| @options[key.to_sym] = value}
|
44
|
+
end
|
45
|
+
if arguments.empty?
|
46
|
+
print_help File.basename(command), DEFAULT_OPTIONS
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
while arguments.any? do
|
50
|
+
arg = arguments.shift
|
51
|
+
raise "Unknown parameter #{arg}" unless arg.start_with? '--'
|
52
|
+
key = arg.slice(2, arg.length - 2)
|
53
|
+
case key
|
54
|
+
when 'debug', 'ignore-ssl', 'auto-paging', 'no-auto-paging'
|
55
|
+
# Single parameter options
|
56
|
+
if key.start_with? 'no-'
|
57
|
+
key = key.slice(3, arg.length - 3)
|
58
|
+
@options[key.gsub('-', '_').to_sym] = false
|
59
|
+
else
|
60
|
+
@options[key.gsub('-', '_').to_sym] = true
|
61
|
+
end
|
62
|
+
when 'api-key', 'secret-key', 'username', 'password', 'domain', 'api-uri', 'response', 'output', 'pagesize', 'config'
|
63
|
+
# Second parameter options
|
64
|
+
raise "Missing value for #{arg}" if arguments.empty?
|
65
|
+
value = arguments.shift
|
66
|
+
if key == 'pagesize'
|
67
|
+
value = value.to_i
|
68
|
+
end
|
69
|
+
@options[key.gsub('-', '_').to_sym] = value
|
70
|
+
when 'help'
|
71
|
+
print_help File.basename(command), DEFAULT_OPTIONS
|
72
|
+
return false
|
73
|
+
when 'config-stub'
|
74
|
+
print_config DEFAULT_OPTIONS
|
75
|
+
return false
|
76
|
+
else
|
77
|
+
raise "Missing value for #{arg}" if arguments.empty?
|
78
|
+
@parameters[key] = arguments.shift
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check existence of base URL
|
83
|
+
unless @options[:api_uri] && !@options[:api_uri].empty?
|
84
|
+
raise "Server API URL is missing. Specify with --api-uri parameter."
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check existence of a command
|
88
|
+
unless @parameters['command']
|
89
|
+
warn "No command specified. Use --command COMMAND to specify a remote command."
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check that auto paging is only used on list* commands
|
93
|
+
unless @parameters['command'] && @parameters['command'].downcase.start_with?("list")
|
94
|
+
@options[:auto_paging] = false
|
95
|
+
warn "Auto Paging disabled because command does not start with 'list'." if @options[:debug]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set response format if specified
|
99
|
+
if @options[:response]
|
100
|
+
warn "Unknown response format #{@options[:response]}." unless ['xml', 'json'].include? @options[:response]
|
101
|
+
@parameters['response'] = @options[:response]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Login to CloudStack API
|
105
|
+
api = CloudStackClient::Connector.new({:api_uri => @options[:api_uri], :page_size => @options[:page_size], :ssl_check => !@options[:ignore_ssl]})
|
106
|
+
if @options[:api_key] && @options[:secret_key]
|
107
|
+
unless api.login_with_keys @options[:api_key], @options[:secret_key]
|
108
|
+
raise "Login using API key and secret key (--api-key KEY --secret-key SECRET) failed."
|
109
|
+
end
|
110
|
+
elsif @options[:username] && @options[:password]
|
111
|
+
unless api.login @options[:username], @options[:password], @options[:domain]
|
112
|
+
raise "Login using username and password failed."
|
113
|
+
end
|
114
|
+
else
|
115
|
+
warn "No credentials specified." if @options[:debug]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Execute query
|
119
|
+
warn "Parameters:\n#{@parameters.inspect}\n" if @options[:debug]
|
120
|
+
if @options[:auto_paging]
|
121
|
+
output = api.query_all_pages @parameters
|
122
|
+
else
|
123
|
+
output = api.query @parameters
|
124
|
+
end
|
125
|
+
|
126
|
+
# Verify result
|
127
|
+
errorcode = nil
|
128
|
+
message = "Unrecognizable Result"
|
129
|
+
unless CloudStackClient::API::response_is_success? output
|
130
|
+
case CloudStackClient::API::response_format(output)
|
131
|
+
when :xml
|
132
|
+
errorcode = REXML::XPath.first(output.root, "//errorcode").text.to_i
|
133
|
+
message = REXML::XPath.first(output.root, "//errortext").text
|
134
|
+
when :json
|
135
|
+
errorcode = output.first[1]["errorcode"]
|
136
|
+
message = output.first[1]["errortext"]
|
137
|
+
else
|
138
|
+
message = "Unknown format #{CloudStackClient::API::response_format(output)}"
|
139
|
+
end
|
140
|
+
warn "Error #{errorcode} occured: #{message}\n"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Output result
|
144
|
+
if not @options[:output]
|
145
|
+
# If no output is specified show formatted result on STDOUT
|
146
|
+
case CloudStackClient::API::response_format(output)
|
147
|
+
when :xml
|
148
|
+
puts "XML Response:"
|
149
|
+
puts output
|
150
|
+
when :json
|
151
|
+
puts "JSON Response:"
|
152
|
+
puts output.to_yaml
|
153
|
+
else
|
154
|
+
warn "Unknown format #{CloudStackClient::API::response_format(output)}" if @options[:debug]
|
155
|
+
end
|
156
|
+
elsif @options[:output] == '-'
|
157
|
+
# Pipe to STDOUT
|
158
|
+
STDOUT << output
|
159
|
+
puts # Fix missing newline
|
160
|
+
else
|
161
|
+
# Write to file
|
162
|
+
file = File.new(@options[:output], 'w')
|
163
|
+
file << output
|
164
|
+
file.close
|
165
|
+
end
|
166
|
+
|
167
|
+
!!errorcode
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
# Show help
|
173
|
+
def print_help(command, options)
|
174
|
+
puts "CloudStack API Commandline Interface (version #{CloudStackClient::VERSION})"
|
175
|
+
puts "Usage: #{command} [options]"
|
176
|
+
puts "Options:"
|
177
|
+
puts "\t--api-key api-key Your unique API key"
|
178
|
+
puts "\t--secret-key secret-key Your secret signature key"
|
179
|
+
puts "\t--username Your login username"
|
180
|
+
puts "\t--password Your login password"
|
181
|
+
puts "\t--domain Your login domain"
|
182
|
+
puts "\t--config yaml-file A YAML configuration file"
|
183
|
+
puts "\t--api-uri url The URL of the API. Example:"
|
184
|
+
puts "\t https://example.com/client/api"
|
185
|
+
puts "\t--response format Specify eighter json or xml (default: #{options[:response]})"
|
186
|
+
puts "\t--output filename Execute command and save the response"
|
187
|
+
puts "\t--[no-]auto-paging Aggregate all output pages (default: #{options[:auto_paging] ? "on" : "off"})"
|
188
|
+
puts "\t--pagesize num Server's maximum page size (default: #{options[:page_size]})"
|
189
|
+
puts "\t--ignore-ssl Ignore SSL certificate errors"
|
190
|
+
puts "\t--debug Additional output"
|
191
|
+
puts "\t--config-stub Prints a skeleton for the configuration file"
|
192
|
+
puts "\t--help This help"
|
193
|
+
puts
|
194
|
+
puts "Any API parameter can be passed as additional parameter:"
|
195
|
+
puts "\t--parameter value Adds 'parameter=value' to the query"
|
196
|
+
puts
|
197
|
+
puts "You can authenticate wit secret and API keys or using username, password and domain."
|
198
|
+
puts
|
199
|
+
puts "Examples:"
|
200
|
+
puts "#{command} --api-uri https://example.com/client/api \\"
|
201
|
+
puts "\t--api-key mYaPiKeY --secret-key MySeCrEt --command listTemplates \\"
|
202
|
+
puts "\t--templatefilter executable"
|
203
|
+
puts "#{command} --api-uri http://example.com/api --username Me --password \\"
|
204
|
+
puts "\tMyPass --domain My/Domain --command deployVirtualMachine --zoneid 123 \\"
|
205
|
+
puts "\t--serviceofferingid 234 --templateid 456"
|
206
|
+
puts "#{command} --config credentials.yml --command queryAsyncJobResult \\"
|
207
|
+
puts "\t--jobid 567"
|
208
|
+
puts "#{command} --config credentials.yml --command createPortForwardingRule \\"
|
209
|
+
puts "\t--ipaddressid 678 --protocol TCP --publicport 443 --privateport 443 \\"
|
210
|
+
puts "\t--virtualmachineid 789"
|
211
|
+
end
|
212
|
+
|
213
|
+
# Print config file skeleton
|
214
|
+
def print_config(options)
|
215
|
+
puts <<-YAML
|
216
|
+
# Specify default options for cloud-stack.rb.
|
217
|
+
#
|
218
|
+
# CloudStack Authentiction can eighter be API key and Secret key or username,
|
219
|
+
# password and domain.
|
220
|
+
#
|
221
|
+
# api_uri is the URL of the API e.g. https://www.example.com/client/api
|
222
|
+
#
|
223
|
+
# The supported response formats are json and xml.
|
224
|
+
|
225
|
+
default:
|
226
|
+
api_uri:
|
227
|
+
api_key:
|
228
|
+
secret_key:
|
229
|
+
username:
|
230
|
+
password:
|
231
|
+
domain:
|
232
|
+
# response: xml
|
233
|
+
# debug: true
|
234
|
+
# auto_paging: false
|
235
|
+
# page_size: 100
|
236
|
+
YAML
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|