cloud_stack_client 0.0.1
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/.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
|