knife-cloudstack 0.0.12 → 0.0.13
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/CHANGES.rdoc +5 -0
- data/README.rdoc +3 -1
- data/lib/chef/knife/cs_hosts.rb +1 -1
- data/lib/chef/knife/cs_server_create.rb +249 -52
- data/lib/chef/knife/cs_server_delete.rb +25 -19
- data/lib/chef/knife/cs_server_list.rb +19 -3
- data/lib/chef/knife/cs_server_reboot.rb +1 -1
- data/lib/chef/knife/cs_server_start.rb +1 -1
- data/lib/chef/knife/cs_server_stop.rb +1 -1
- data/lib/chef/knife/cs_stack_create.rb +1 -1
- data/lib/chef/knife/cs_template_list.rb +17 -2
- data/lib/knife-cloudstack/connection.rb +119 -19
- metadata +56 -56
data/CHANGES.rdoc
CHANGED
data/README.rdoc
CHANGED
@@ -39,6 +39,8 @@ Additionally the following options may be set in your <tt>knife.rb</tt>:
|
|
39
39
|
* knife[:distro]
|
40
40
|
* knife[:template_file]
|
41
41
|
|
42
|
+
== Public Clouds (Tata InstaCompute, Ninefold etc):
|
43
|
+
To get this plugin to work in public clouds, it is essential that the virtual network (and router) be allocated to the account. Cloudstack clouds automatically creates a virtual network when the first VM is requested to be created. Hence, it is essential to create the first VM (of a newly created account) manually(which can be terminated immediately if not required) to ensure the virtual network is created.
|
42
44
|
|
43
45
|
== SUBCOMMANDS:
|
44
46
|
|
@@ -195,7 +197,7 @@ Reboots the specified virtual machines(s).
|
|
195
197
|
== LICENSE:
|
196
198
|
|
197
199
|
Author:: Ryan Holmes <rholmes@edmunds.com>
|
198
|
-
Author:: KC Braunschweig <
|
200
|
+
Author:: KC Braunschweig <kcbraunschweig@gmail.com>
|
199
201
|
Author:: John E. Vincent <lusis.org+github.com@gmail.com>
|
200
202
|
|
201
203
|
Copyright:: Copyright (c) 2011 Edmunds, Inc.
|
data/lib/chef/knife/cs_hosts.rb
CHANGED
@@ -18,18 +18,31 @@
|
|
18
18
|
|
19
19
|
require 'chef/knife'
|
20
20
|
require 'json'
|
21
|
+
require 'chef/knife/winrm_base'
|
22
|
+
require 'winrm'
|
23
|
+
require 'httpclient'
|
24
|
+
require 'em-winrm'
|
25
|
+
|
21
26
|
|
22
27
|
module KnifeCloudstack
|
23
28
|
class CsServerCreate < Chef::Knife
|
24
29
|
|
30
|
+
include Chef::Knife::WinrmBase
|
31
|
+
|
25
32
|
# Seconds to delay between detecting ssh and initiating the bootstrap
|
26
|
-
BOOTSTRAP_DELAY =
|
33
|
+
BOOTSTRAP_DELAY = 20
|
34
|
+
#The machine will reboot once so we need to handle that
|
35
|
+
WINRM_BOOTSTRAP_DELAY = 200
|
27
36
|
|
28
37
|
# Seconds to wait between ssh pings
|
29
|
-
SSH_POLL_INTERVAL =
|
38
|
+
SSH_POLL_INTERVAL = 10
|
30
39
|
|
31
40
|
deps do
|
32
41
|
require 'chef/knife/bootstrap'
|
42
|
+
require 'chef/knife/bootstrap_windows_winrm'
|
43
|
+
require 'chef/knife/bootstrap_windows_ssh'
|
44
|
+
require 'chef/knife/core/windows_bootstrap_context'
|
45
|
+
require 'chef/knife/winrm'
|
33
46
|
Chef::Knife::Bootstrap.load_deps
|
34
47
|
require 'socket'
|
35
48
|
require 'net/ssh/multi'
|
@@ -86,6 +99,12 @@ module KnifeCloudstack
|
|
86
99
|
:long => "--ssh-password PASSWORD",
|
87
100
|
:description => "The ssh password"
|
88
101
|
|
102
|
+
|
103
|
+
option :ssh_port,
|
104
|
+
:long => "--ssh-port PORT",
|
105
|
+
:description => "The ssh port",
|
106
|
+
:default => "22"
|
107
|
+
|
89
108
|
option :identity_file,
|
90
109
|
:short => "-i IDENTITY_FILE",
|
91
110
|
:long => "--identity-file IDENTITY_FILE",
|
@@ -157,10 +176,47 @@ module KnifeCloudstack
|
|
157
176
|
:proc => lambda { |o| o.split(/[\s,]+/) },
|
158
177
|
:default => []
|
159
178
|
|
179
|
+
option :cloudstack_project,
|
180
|
+
:short => "-P PROJECT_NAME",
|
181
|
+
:long => '--cloudstack-project PROJECT_NAME',
|
182
|
+
:description => "Cloudstack Project in which to create server",
|
183
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:cloudstack_project] = v },
|
184
|
+
:default => nil
|
185
|
+
|
186
|
+
option :static_nat,
|
187
|
+
:long => '--static-nat',
|
188
|
+
:description => 'Support Static NAT',
|
189
|
+
:boolean => true,
|
190
|
+
:default => false
|
191
|
+
|
192
|
+
option :use_http_ssl,
|
193
|
+
:long => '--[no-]use-http-ssl',
|
194
|
+
:description => 'Support HTTPS',
|
195
|
+
:boolean => true,
|
196
|
+
:default => true
|
197
|
+
|
198
|
+
option :bootstrap_protocol,
|
199
|
+
:long => "--bootstrap-protocol protocol",
|
200
|
+
:description => "Protocol to bootstrap windows servers. options: winrm/ssh",
|
201
|
+
:default => "ssh"
|
202
|
+
|
203
|
+
option :fqdn,
|
204
|
+
:long => '--fqdn',
|
205
|
+
:description => "FQDN which Kerberos Understands (only for Windows Servers)"
|
206
|
+
|
207
|
+
|
208
|
+
def connection
|
209
|
+
@connection ||= CloudstackClient::Connection.new(
|
210
|
+
locate_config_value(:cloudstack_url),
|
211
|
+
locate_config_value(:cloudstack_api_key),
|
212
|
+
locate_config_value(:cloudstack_secret_key),
|
213
|
+
locate_config_value(:cloudstack_project),
|
214
|
+
locate_config_value(:use_http_ssl)
|
215
|
+
)
|
216
|
+
end
|
160
217
|
|
161
218
|
def run
|
162
|
-
|
163
|
-
# validate hostname and options
|
219
|
+
Chef::Log.debug("Validate hostname and options")
|
164
220
|
hostname = @name_args.first
|
165
221
|
unless /^[a-zA-Z0-9][a-zA-Z0-9-]*$/.match hostname then
|
166
222
|
ui.error "Invalid hostname. Please specify a short hostname, not an fqdn (e.g. 'myhost' instead of 'myhost.domain.com')."
|
@@ -168,15 +224,26 @@ module KnifeCloudstack
|
|
168
224
|
end
|
169
225
|
validate_options
|
170
226
|
|
227
|
+
if @windows_image and locate_config_value(:kerberos_realm)
|
228
|
+
Chef::Log.debug("Load additional gems for AD/Kerberos Authentication")
|
229
|
+
if @windows_platform
|
230
|
+
require 'em-winrs'
|
231
|
+
else
|
232
|
+
require 'gssapi'
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
171
236
|
$stdout.sync = true
|
172
237
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
238
|
+
Chef::Log.info("Creating instance with
|
239
|
+
service : #{locate_config_value(:cloudstack_service)}
|
240
|
+
template : #{locate_config_value(:cloudstack_template)}
|
241
|
+
zone : #{locate_config_value(:cloudstack_zone)}
|
242
|
+
project: #{locate_config_value(:cloudstack_project)}
|
243
|
+
network: #{locate_config_value(:cloudstack_networks)}")
|
244
|
+
|
245
|
+
print "\n#{ui.color("Waiting for Server to be created", :magenta)}"
|
178
246
|
|
179
|
-
print "#{ui.color("Waiting for server", :magenta)}"
|
180
247
|
server = connection.create_server(
|
181
248
|
hostname,
|
182
249
|
locate_config_value(:cloudstack_service),
|
@@ -188,82 +255,160 @@ module KnifeCloudstack
|
|
188
255
|
public_ip = find_or_create_public_ip(server, connection)
|
189
256
|
|
190
257
|
puts "\n\n"
|
191
|
-
puts "#{ui.color(
|
192
|
-
puts "#{ui.color(
|
258
|
+
puts "#{ui.color('Name', :cyan)}: #{server['name']}"
|
259
|
+
puts "#{ui.color('Public IP', :cyan)}: #{public_ip}"
|
193
260
|
|
194
261
|
return if config[:no_bootstrap]
|
195
262
|
|
196
|
-
|
263
|
+
if @bootstrap_protocol == 'ssh'
|
264
|
+
print "\n#{ui.color("Waiting for sshd", :magenta)}"
|
197
265
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
266
|
+
print(".") until is_ssh_open?(public_ip) {
|
267
|
+
sleep BOOTSTRAP_DELAY
|
268
|
+
puts "\n"
|
269
|
+
}
|
270
|
+
else
|
271
|
+
print "\n#{ui.color("Waiting for winrm to be active", :magenta)}"
|
272
|
+
print(".") until tcp_test_winrm(public_ip,locate_config_value(:winrm_port)) {
|
273
|
+
sleep WINRM_BOOTSTRAP_DELAY
|
274
|
+
puts("\n")
|
275
|
+
}
|
276
|
+
end
|
204
277
|
|
205
278
|
puts "\n"
|
206
279
|
puts "#{ui.color("Name", :cyan)}: #{server['name']}"
|
207
280
|
puts "#{ui.color("Public IP", :cyan)}: #{public_ip}"
|
208
281
|
puts "#{ui.color("Environment", :cyan)}: #{config[:environment] || '_default'}"
|
209
282
|
puts "#{ui.color("Run List", :cyan)}: #{config[:run_list].join(', ')}"
|
283
|
+
bootstrap(server, public_ip).run
|
284
|
+
end
|
210
285
|
|
286
|
+
def fetch_server_fqdn(ip_addr)
|
287
|
+
require 'resolv'
|
288
|
+
Resolv.getname(ip_addr)
|
211
289
|
end
|
212
290
|
|
213
|
-
def
|
291
|
+
def is_image_windows?
|
292
|
+
template = connection.get_template(locate_config_value(:cloudstack_template))
|
293
|
+
if !template
|
294
|
+
ui.error("Template: #{template} does not exist")
|
295
|
+
exit 1
|
296
|
+
end
|
297
|
+
return template['ostypename'].scan('Windows').length > 0
|
298
|
+
end
|
214
299
|
|
300
|
+
def validate_options
|
215
301
|
unless locate_config_value :cloudstack_template
|
216
302
|
ui.error "Cloudstack template not specified"
|
217
303
|
exit 1
|
218
304
|
end
|
305
|
+
@windows_image = is_image_windows?
|
306
|
+
@windows_platform = is_platform_windows?
|
219
307
|
|
220
308
|
unless locate_config_value :cloudstack_service
|
221
309
|
ui.error "Cloudstack service offering not specified"
|
222
310
|
exit 1
|
223
311
|
end
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
312
|
+
if locate_config_value(:bootstrap_protocol) == 'ssh'
|
313
|
+
identity_file = locate_config_value :identity_file
|
314
|
+
ssh_user = locate_config_value :ssh_user
|
315
|
+
ssh_password = locate_config_value :ssh_password
|
316
|
+
unless identity_file || (ssh_user && ssh_password)
|
317
|
+
ui.error("You must specify either an ssh identity file or an ssh user and password")
|
318
|
+
exit 1
|
319
|
+
end
|
320
|
+
@bootstrap_protocol = 'ssh'
|
321
|
+
elsif locate_config_value(:bootstrap_protocol) == 'winrm'
|
322
|
+
if not @windows_image
|
323
|
+
ui.error("Only Windows Images support WinRM protocol for bootstrapping.")
|
324
|
+
exit 1
|
325
|
+
end
|
326
|
+
winrm_user = locate_config_value :winrm_user
|
327
|
+
winrm_password = locate_config_value :winrm_password
|
328
|
+
winrm_transport = locate_config_value :winrm_transport
|
329
|
+
winrm_port = locate_config_value :winrm_port
|
330
|
+
unless winrm_user && winrm_password && winrm_transport && winrm_port
|
331
|
+
ui.error("WinRM User, Password, Transport and Port are compulsory parameters")
|
332
|
+
exit 1
|
333
|
+
end
|
334
|
+
@bootstrap_protocol = 'winrm'
|
231
335
|
end
|
232
336
|
end
|
233
337
|
|
234
|
-
|
235
338
|
def find_or_create_public_ip(server, connection)
|
236
339
|
nic = connection.get_server_default_nic(server) || {}
|
237
340
|
#puts "#{ui.color("Not allocating public IP for server", :red)}" unless config[:public_ip]
|
238
|
-
if (config[:public_ip] == false)
|
341
|
+
if (config[:public_ip] == false)
|
239
342
|
nic['ipaddress']
|
240
343
|
else
|
241
|
-
|
242
|
-
ip_address = connection.associate_ip_address(server['zoneid'])
|
243
|
-
|
244
|
-
|
245
|
-
|
344
|
+
puts("\nAllocate ip address, create forwarding rules")
|
345
|
+
ip_address = connection.associate_ip_address(server['zoneid'], locate_config_value(:cloudstack_networks))
|
346
|
+
#ip_address = connection.get_public_ip_address('202.2.94.158')
|
347
|
+
puts("\nAllocated IP Address: #{ip_address['ipaddress']}")
|
348
|
+
Chef::Log.debug("IP Address Info: #{ip_address}")
|
349
|
+
|
350
|
+
if locate_config_value :static_nat
|
351
|
+
Chef::Log.debug("Enabling static NAT for IP Address : #{ip_address['ipaddress']}")
|
352
|
+
connection.enable_static_nat(ip_address['id'], server['id'])
|
353
|
+
end
|
354
|
+
create_port_forwarding_rules(ip_address, server['id'], connection)
|
355
|
+
ip_address['ipaddress']
|
246
356
|
end
|
247
357
|
end
|
248
358
|
|
249
|
-
def create_port_forwarding_rules(
|
359
|
+
def create_port_forwarding_rules(ip_address, server_id, connection)
|
250
360
|
rules = locate_config_value(:port_rules)
|
251
|
-
|
252
|
-
|
361
|
+
if @bootstrap_protocol == 'ssh'
|
362
|
+
rules += ["#{locate_config_value(:ssh_port)}"] #SSH Port
|
363
|
+
elsif @bootstrap_protocol == 'winrm'
|
364
|
+
rules +=[locate_config_value(:winrm_port)]
|
365
|
+
else
|
366
|
+
puts("\nUnsupported bootstrap protocol : #{@bootstrap_protocol}")
|
367
|
+
exit 1
|
368
|
+
end
|
369
|
+
return unless rules
|
253
370
|
rules.each do |rule|
|
254
371
|
args = rule.split(':')
|
255
372
|
public_port = args[0]
|
256
373
|
private_port = args[1] || args[0]
|
257
374
|
protocol = args[2] || "TCP"
|
258
|
-
|
375
|
+
if locate_config_value :static_nat
|
376
|
+
Chef::Log.debug("Creating IP Forwarding Rule for
|
377
|
+
#{ip_address['ipaddress']} with protocol: #{protocol}, public port: #{public_port}")
|
378
|
+
connection.create_ip_fwd_rule(ip_address['id'], protocol, public_port, public_port)
|
379
|
+
else
|
380
|
+
Chef::Log.debug("Creating Port Forwarding Rule for #{ip_address['id']} with protocol: #{protocol},
|
381
|
+
public port: #{public_port} and private port: #{private_port} and server: #{server_id}")
|
382
|
+
connection.create_port_forwarding_rule(ip_address['id'], private_port, protocol, public_port, server_id)
|
383
|
+
end
|
259
384
|
end
|
385
|
+
end
|
260
386
|
|
387
|
+
def tcp_test_winrm(hostname, port)
|
388
|
+
TCPSocket.new(hostname, port)
|
389
|
+
return true
|
390
|
+
rescue SocketError
|
391
|
+
sleep 2
|
392
|
+
false
|
393
|
+
rescue Errno::ETIMEDOUT
|
394
|
+
false
|
395
|
+
rescue Errno::EPERM
|
396
|
+
false
|
397
|
+
rescue Errno::ECONNREFUSED
|
398
|
+
sleep 2
|
399
|
+
false
|
400
|
+
rescue Errno::EHOSTUNREACH
|
401
|
+
sleep 2
|
402
|
+
false
|
403
|
+
rescue Errno::ENETUNREACH
|
404
|
+
sleep 2
|
405
|
+
false
|
261
406
|
end
|
262
407
|
|
263
408
|
#noinspection RubyArgCount,RubyResolve
|
264
409
|
def is_ssh_open?(ip)
|
265
410
|
s = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
266
|
-
sa = Socket.sockaddr_in(
|
411
|
+
sa = Socket.sockaddr_in(locate_config_value(:ssh_port), ip)
|
267
412
|
|
268
413
|
begin
|
269
414
|
s.connect_nonblock(sa)
|
@@ -288,31 +433,83 @@ module KnifeCloudstack
|
|
288
433
|
s && s.close
|
289
434
|
end
|
290
435
|
end
|
436
|
+
def is_platform_windows?
|
437
|
+
return RUBY_PLATFORM.scan('w32').size > 0
|
438
|
+
end
|
291
439
|
|
440
|
+
def bootstrap(server, public_ip)
|
441
|
+
if @windows_image
|
442
|
+
Chef::Log.debug("Windows Bootstrapping")
|
443
|
+
bootstrap_for_windows_node(server, public_ip)
|
444
|
+
else
|
445
|
+
Chef::Log.debug("Linux Bootstrapping")
|
446
|
+
bootstrap_for_node(server, public_ip)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
def bootstrap_for_windows_node(server, fqdn)
|
450
|
+
if locate_config_value(:bootstrap_protocol) == 'winrm'
|
451
|
+
bootstrap = Chef::Knife::BootstrapWindowsWinrm.new
|
452
|
+
if locate_config_value(:kerberos_realm)
|
453
|
+
#Fetch AD/WINS based fqdn if any for Kerberos-based Auth
|
454
|
+
private_ip_address = connection.get_server_default_nic(server)["ipaddress"]
|
455
|
+
fqdn = locate_config_value(:fqdn) || fetch_server_fqdn(private_ip_address)
|
456
|
+
end
|
457
|
+
bootstrap.name_args = [fqdn]
|
458
|
+
bootstrap.config[:winrm_user] = locate_config_value(:winrm_user) || 'Administrator'
|
459
|
+
bootstrap.config[:winrm_password] = locate_config_value(:winrm_password)
|
460
|
+
bootstrap.config[:winrm_transport] = locate_config_value(:winrm_transport)
|
461
|
+
bootstrap.config[:winrm_port] = locate_config_value(:winrm_port)
|
462
|
+
|
463
|
+
elsif locate_config_value(:bootstrap_protocol) == 'ssh'
|
464
|
+
bootstrap = Chef::Knife::BootstrapWindowsSsh.new
|
465
|
+
bootstrap.config[:ssh_user] = locate_config_value(:ssh_user)
|
466
|
+
bootstrap.config[:ssh_password] = locate_config_value(:ssh_password)
|
467
|
+
bootstrap.config[:ssh_port] = locate_config_value(:ssh_port)
|
468
|
+
bootstrap.config[:identity_file] = locate_config_value(:identity_file)
|
469
|
+
bootstrap.config[:no_host_key_verify] = locate_config_value(:no_host_key_verify)
|
470
|
+
else
|
471
|
+
ui.error("Unsupported Bootstrapping Protocol. Supported : winrm, ssh")
|
472
|
+
exit 1
|
473
|
+
end
|
474
|
+
bootstrap.config[:chef_node_name] = config[:chef_node_name] || server['id']
|
475
|
+
bootstrap.config[:encrypted_data_bag_secret] = config[:encrypted_data_bag_secret]
|
476
|
+
bootstrap.config[:encrypted_data_bag_secret_file] = config[:encrypted_data_bag_secret_file]
|
477
|
+
bootstrap_common_params(bootstrap)
|
478
|
+
end
|
479
|
+
def bootstrap_common_params(bootstrap)
|
292
480
|
|
293
|
-
def bootstrap_for_node(host)
|
294
|
-
bootstrap = Chef::Knife::Bootstrap.new
|
295
|
-
bootstrap.name_args = [host]
|
296
481
|
bootstrap.config[:run_list] = config[:run_list]
|
297
|
-
bootstrap.config[:ssh_user] = config[:ssh_user]
|
298
|
-
bootstrap.config[:ssh_password] = config[:ssh_password]
|
299
|
-
bootstrap.config[:identity_file] = config[:identity_file]
|
300
|
-
bootstrap.config[:chef_node_name] = config[:chef_node_name] if config[:chef_node_name]
|
301
482
|
bootstrap.config[:prerelease] = config[:prerelease]
|
302
483
|
bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
|
303
484
|
bootstrap.config[:distro] = locate_config_value(:distro)
|
304
|
-
bootstrap.config[:use_sudo] = true
|
305
485
|
bootstrap.config[:template_file] = locate_config_value(:template_file)
|
306
|
-
bootstrap.config[:environment] = config[:environment]
|
307
|
-
# may be needed for vpc_mode
|
308
|
-
bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
|
309
486
|
bootstrap
|
310
487
|
end
|
311
488
|
|
489
|
+
|
490
|
+
def bootstrap_for_node(server,fqdn)
|
491
|
+
bootstrap = Chef::Knife::Bootstrap.new
|
492
|
+
bootstrap.name_args = [fqdn]
|
493
|
+
# bootstrap.config[:run_list] = config[:run_list]
|
494
|
+
bootstrap.config[:ssh_user] = locate_config_value(:ssh_user)
|
495
|
+
bootstrap.config[:ssh_password] = locate_config_value(:ssh_password)
|
496
|
+
bootstrap.config[:ssh_port] = locate_config_value(:ssh_port) || 22
|
497
|
+
bootstrap.config[:identity_file] = locate_config_value(:identity_file)
|
498
|
+
bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server.name
|
499
|
+
# bootstrap.config[:prerelease] = locate_config_value(:prerelease)
|
500
|
+
# bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
|
501
|
+
# bootstrap.config[:distro] = locate_config_value(:distro)
|
502
|
+
bootstrap.config[:use_sudo] = true unless locate_config_value(:ssh_user) == 'root'
|
503
|
+
# bootstrap.config[:template_file] = config[:template_file]
|
504
|
+
bootstrap.config[:environment] = locate_config_value(:environment)
|
505
|
+
# may be needed for vpc_mode
|
506
|
+
bootstrap.config[:host_key_verify] = config[:host_key_verify]
|
507
|
+
bootstrap_common_params(bootstrap)
|
508
|
+
end
|
509
|
+
|
312
510
|
def locate_config_value(key)
|
313
511
|
key = key.to_sym
|
314
512
|
Chef::Config[:knife][key] || config[key]
|
315
513
|
end
|
316
|
-
|
317
|
-
end # class
|
514
|
+
end
|
318
515
|
end
|
@@ -46,6 +46,19 @@ module KnifeCloudstack
|
|
46
46
|
:description => "Your CloudStack secret key",
|
47
47
|
:proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
|
48
48
|
|
49
|
+
option :cloudstack_project,
|
50
|
+
:short => "-P PROJECT_NAME",
|
51
|
+
:long => '--cloudstack-project PROJECT_NAME',
|
52
|
+
:description => "Cloudstack Project in which to create server",
|
53
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:cloudstack_project] = v },
|
54
|
+
:default => nil
|
55
|
+
|
56
|
+
option :use_http_ssl,
|
57
|
+
:long => '--[no-]use-http-ssl',
|
58
|
+
:description => 'Support HTTPS',
|
59
|
+
:boolean => true,
|
60
|
+
:default => true
|
61
|
+
|
49
62
|
def run
|
50
63
|
|
51
64
|
@name_args.each do |hostname|
|
@@ -89,24 +102,15 @@ module KnifeCloudstack
|
|
89
102
|
end
|
90
103
|
|
91
104
|
def disassociate_virtual_ip_address(server)
|
92
|
-
|
93
|
-
return unless
|
94
|
-
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
return unless rules
|
102
|
-
|
103
|
-
# ensure ip address has rules only for this server
|
104
|
-
rules.each { |r|
|
105
|
-
return if r['virtualmachineid'] != server['id']
|
106
|
-
}
|
107
|
-
|
108
|
-
# dissassociate the ip address if all tests passed
|
109
|
-
connection.disassociate_ip_address(ssh_rule['ipaddressid'])
|
105
|
+
ip_addr = connection.get_server_public_ip(server)
|
106
|
+
return unless ip_addr
|
107
|
+
ip_addr_info = connection.get_public_ip_address(ip_addr)
|
108
|
+
#Check if Public IP has been allocated and is not Source NAT
|
109
|
+
if ip_addr_info
|
110
|
+
if not ip_addr_info['issourcenat']
|
111
|
+
connection.disassociate_ip_address(ip_addr_info['id'])
|
112
|
+
end
|
113
|
+
end
|
110
114
|
end
|
111
115
|
|
112
116
|
def delete_client(name)
|
@@ -136,7 +140,9 @@ module KnifeCloudstack
|
|
136
140
|
@connection = CloudstackClient::Connection.new(
|
137
141
|
locate_config_value(:cloudstack_url),
|
138
142
|
locate_config_value(:cloudstack_api_key),
|
139
|
-
locate_config_value(:cloudstack_secret_key)
|
143
|
+
locate_config_value(:cloudstack_secret_key),
|
144
|
+
locate_config_value(:cloudstack_project),
|
145
|
+
locate_config_value(:use_http_ssl)
|
140
146
|
)
|
141
147
|
end
|
142
148
|
@connection
|
@@ -45,6 +45,18 @@ module KnifeCloudstack
|
|
45
45
|
:description => "Your CloudStack secret key",
|
46
46
|
:proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
|
47
47
|
|
48
|
+
option :cloudstack_project,
|
49
|
+
:short => "-P PROJECT_NAME",
|
50
|
+
:long => '--cloudstack-project PROJECT_NAME',
|
51
|
+
:description => "Cloudstack Project in which to create server",
|
52
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:cloudstack_project] = v },
|
53
|
+
:default => nil
|
54
|
+
|
55
|
+
option :use_http_ssl,
|
56
|
+
:long => '--[no-]use-http-ssl',
|
57
|
+
:description => 'Support HTTPS',
|
58
|
+
:boolean => true,
|
59
|
+
:default => true
|
48
60
|
def run
|
49
61
|
|
50
62
|
$stdout.sync = true
|
@@ -52,7 +64,9 @@ module KnifeCloudstack
|
|
52
64
|
connection = CloudstackClient::Connection.new(
|
53
65
|
locate_config_value(:cloudstack_url),
|
54
66
|
locate_config_value(:cloudstack_api_key),
|
55
|
-
locate_config_value(:cloudstack_secret_key)
|
67
|
+
locate_config_value(:cloudstack_secret_key),
|
68
|
+
locate_config_value(:cloudstack_project),
|
69
|
+
locate_config_value(:use_http_ssl)
|
56
70
|
)
|
57
71
|
|
58
72
|
server_list = [
|
@@ -60,7 +74,8 @@ module KnifeCloudstack
|
|
60
74
|
ui.color('Public IP', :bold),
|
61
75
|
ui.color('Service', :bold),
|
62
76
|
ui.color('Template', :bold),
|
63
|
-
ui.color('State', :bold)
|
77
|
+
ui.color('State', :bold),
|
78
|
+
ui.color('Hypervisor', :bold)
|
64
79
|
]
|
65
80
|
|
66
81
|
servers = connection.list_servers
|
@@ -78,8 +93,9 @@ module KnifeCloudstack
|
|
78
93
|
server_list << server['serviceofferingname']
|
79
94
|
server_list << server['templatename']
|
80
95
|
server_list << server['state']
|
96
|
+
server_list << (server['hostname'] || 'N/A')
|
81
97
|
end
|
82
|
-
puts ui.list(server_list, :columns_across,
|
98
|
+
puts ui.list(server_list, :columns_across, 6)
|
83
99
|
|
84
100
|
end
|
85
101
|
|
@@ -171,7 +171,7 @@ module KnifeCloudstack
|
|
171
171
|
query = "(#{query})" + " AND chef_environment:#{get_environment}"
|
172
172
|
end
|
173
173
|
|
174
|
-
Chef::Log.debug("Searching for nodes: #{query}")
|
174
|
+
Chef::Chef::Log.debug("Searching for nodes: #{query}")
|
175
175
|
|
176
176
|
q = Chef::Search::Query.new
|
177
177
|
nodes = Array(q.search(:node, query))
|
@@ -53,12 +53,27 @@ module KnifeCloudstack
|
|
53
53
|
:description => "Your CloudStack secret key",
|
54
54
|
:proc => Proc.new { |key| Chef::Config[:knife][:cloudstack_secret_key] = key }
|
55
55
|
|
56
|
+
option :cloudstack_project,
|
57
|
+
:short => "-P PROJECT_NAME",
|
58
|
+
:long => '--cloudstack-project PROJECT_NAME',
|
59
|
+
:description => "Cloudstack Project in which to create server",
|
60
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:cloudstack_project] = v },
|
61
|
+
:default => nil
|
62
|
+
|
63
|
+
option :use_http_ssl,
|
64
|
+
:long => '--[no-]use-http-ssl',
|
65
|
+
:description => 'Support HTTPS',
|
66
|
+
:boolean => true,
|
67
|
+
:default => true
|
68
|
+
|
56
69
|
def run
|
57
70
|
|
58
71
|
connection = CloudstackClient::Connection.new(
|
59
72
|
locate_config_value(:cloudstack_url),
|
60
73
|
locate_config_value(:cloudstack_api_key),
|
61
|
-
locate_config_value(:cloudstack_secret_key)
|
74
|
+
locate_config_value(:cloudstack_secret_key),
|
75
|
+
locate_config_value(:cloudstack_project),
|
76
|
+
locate_config_value(:use_http_ssl)
|
62
77
|
)
|
63
78
|
|
64
79
|
template_list = [
|
@@ -73,7 +88,7 @@ module KnifeCloudstack
|
|
73
88
|
templates = connection.list_templates(filter)
|
74
89
|
templates.each do |t|
|
75
90
|
template_list << t['name']
|
76
|
-
template_list << (human_file_size(t['size']) || 'Unknown')
|
91
|
+
#template_list << (human_file_size(t['size']) || 'Unknown')
|
77
92
|
template_list << t['zonename']
|
78
93
|
template_list << t['ispublic'].to_s
|
79
94
|
template_list << t['created']
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#
|
2
2
|
# Author:: Ryan Holmes (<rholmes@edmunds.com>)
|
3
|
-
# Author:: KC Braunschweig (<
|
3
|
+
# Author:: KC Braunschweig (<kcbraunschweig@gmail.com>)
|
4
4
|
# Copyright:: Copyright (c) 2011 Edmunds, Inc.
|
5
5
|
# License:: Apache License, Version 2.0
|
6
6
|
#
|
@@ -28,13 +28,24 @@ require 'json'
|
|
28
28
|
module CloudstackClient
|
29
29
|
class Connection
|
30
30
|
|
31
|
-
ASYNC_POLL_INTERVAL =
|
32
|
-
ASYNC_TIMEOUT =
|
31
|
+
ASYNC_POLL_INTERVAL = 5.0
|
32
|
+
ASYNC_TIMEOUT = 600
|
33
33
|
|
34
|
-
def initialize(api_url, api_key, secret_key)
|
34
|
+
def initialize(api_url, api_key, secret_key, project_name=nil, use_ssl=true)
|
35
35
|
@api_url = api_url
|
36
36
|
@api_key = api_key
|
37
37
|
@secret_key = secret_key
|
38
|
+
@project_id = nil
|
39
|
+
@use_ssl = use_ssl
|
40
|
+
if project_name
|
41
|
+
project = get_project(project_name)
|
42
|
+
if !project then
|
43
|
+
puts "Project #{project_name} does not exist"
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
@project_id = project['id']
|
47
|
+
end
|
48
|
+
|
38
49
|
end
|
39
50
|
|
40
51
|
##
|
@@ -45,6 +56,9 @@ module CloudstackClient
|
|
45
56
|
'command' => 'listVirtualMachines',
|
46
57
|
'name' => name
|
47
58
|
}
|
59
|
+
# if @project_id
|
60
|
+
# params['projectId'] = @project_id
|
61
|
+
# end
|
48
62
|
json = send_request(params)
|
49
63
|
machines = json['virtualmachine']
|
50
64
|
|
@@ -60,15 +74,18 @@ module CloudstackClient
|
|
60
74
|
|
61
75
|
def get_server_public_ip(server, cached_rules=nil)
|
62
76
|
return nil unless server
|
63
|
-
|
64
77
|
# find the public ip
|
65
|
-
nic = get_server_default_nic(server)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
78
|
+
nic = get_server_default_nic(server)
|
79
|
+
ssh_rule = get_ssh_port_forwarding_rule(server, cached_rules)
|
80
|
+
if ssh_rule
|
81
|
+
return ssh_rule['ipaddress']
|
82
|
+
end
|
83
|
+
#check for static NAT
|
84
|
+
ip_addr = list_public_ip_addresses.find {|v| v['virtualmachineid'] == server['id']}
|
85
|
+
if ip_addr
|
86
|
+
return ip_addr['ipaddress']
|
71
87
|
end
|
88
|
+
nic['ipaddress']
|
72
89
|
end
|
73
90
|
|
74
91
|
##
|
@@ -102,6 +119,9 @@ module CloudstackClient
|
|
102
119
|
params = {
|
103
120
|
'command' => 'listVirtualMachines'
|
104
121
|
}
|
122
|
+
# if @project_id
|
123
|
+
# params['projectId'] = @project_id
|
124
|
+
# end
|
105
125
|
json = send_request(params)
|
106
126
|
json['virtualmachine'] || []
|
107
127
|
end
|
@@ -147,7 +167,7 @@ module CloudstackClient
|
|
147
167
|
networks << get_network(name)
|
148
168
|
end
|
149
169
|
if networks.empty? then
|
150
|
-
networks << get_default_network
|
170
|
+
networks << get_default_network(zone['id'])
|
151
171
|
end
|
152
172
|
if networks.empty? then
|
153
173
|
puts "No default network found"
|
@@ -164,6 +184,10 @@ module CloudstackClient
|
|
164
184
|
'zoneId' => zone['id'],
|
165
185
|
'networkids' => network_ids.join(',')
|
166
186
|
}
|
187
|
+
# if @project_id
|
188
|
+
# params['projectId'] = @project_id
|
189
|
+
# end
|
190
|
+
|
167
191
|
params['name'] = host_name if host_name
|
168
192
|
|
169
193
|
json = send_async_request(params)
|
@@ -338,6 +362,25 @@ module CloudstackClient
|
|
338
362
|
json['template'] || []
|
339
363
|
end
|
340
364
|
|
365
|
+
#Fetch project with the specified name
|
366
|
+
def get_project(name)
|
367
|
+
params = {
|
368
|
+
'command' => 'listProjects'
|
369
|
+
}
|
370
|
+
|
371
|
+
json = send_request(params)
|
372
|
+
projects = json['project']
|
373
|
+
return nil unless projects
|
374
|
+
projects.each { |n|
|
375
|
+
if n['name'] == name then
|
376
|
+
return n
|
377
|
+
end
|
378
|
+
}
|
379
|
+
|
380
|
+
nil
|
381
|
+
end
|
382
|
+
|
383
|
+
|
341
384
|
##
|
342
385
|
# Finds the network with the specified name.
|
343
386
|
|
@@ -345,6 +388,9 @@ module CloudstackClient
|
|
345
388
|
params = {
|
346
389
|
'command' => 'listNetworks'
|
347
390
|
}
|
391
|
+
# if @project_id
|
392
|
+
# params['projectId'] = @project_id
|
393
|
+
# end
|
348
394
|
json = send_request(params)
|
349
395
|
|
350
396
|
networks = json['network']
|
@@ -362,11 +408,15 @@ module CloudstackClient
|
|
362
408
|
##
|
363
409
|
# Finds the default network.
|
364
410
|
|
365
|
-
def get_default_network
|
411
|
+
def get_default_network(zone)
|
366
412
|
params = {
|
367
413
|
'command' => 'listNetworks',
|
368
|
-
'isDefault' => true
|
414
|
+
'isDefault' => true,
|
415
|
+
'zoneid' => zone
|
369
416
|
}
|
417
|
+
# if @project_id
|
418
|
+
# params['projectId'] = @project_id
|
419
|
+
# end
|
370
420
|
json = send_request(params)
|
371
421
|
|
372
422
|
networks = json['network']
|
@@ -455,23 +505,66 @@ module CloudstackClient
|
|
455
505
|
'ipaddress' => ip_address
|
456
506
|
}
|
457
507
|
json = send_request(params)
|
508
|
+
return nil unless json['publicipaddress']
|
458
509
|
json['publicipaddress'].first
|
459
510
|
end
|
460
511
|
|
512
|
+
def list_public_ip_addresses()
|
513
|
+
params = { 'command' => 'listPublicIpAddresses'}
|
461
514
|
|
515
|
+
json = send_request(params)
|
516
|
+
return json['publicipaddress']
|
517
|
+
end
|
462
518
|
##
|
463
519
|
# Acquires and associates a public IP to an account.
|
464
520
|
|
465
|
-
def associate_ip_address(zone_id)
|
521
|
+
def associate_ip_address(zone_id, networks)
|
466
522
|
params = {
|
467
523
|
'command' => 'associateIpAddress',
|
468
524
|
'zoneId' => zone_id
|
469
525
|
}
|
470
|
-
|
526
|
+
#Choose the first network from the list
|
527
|
+
if networks.size > 0
|
528
|
+
params['networkId'] = get_network(networks.first)['id']
|
529
|
+
else
|
530
|
+
default_network = get_default_network(zone_id)
|
531
|
+
params['networkId'] = default_network['id']
|
532
|
+
end
|
533
|
+
print "params: #{params}"
|
471
534
|
json = send_async_request(params)
|
472
535
|
json['ipaddress']
|
473
536
|
end
|
474
537
|
|
538
|
+
def enable_static_nat(ipaddress_id, virtualmachine_id)
|
539
|
+
params = {
|
540
|
+
'command' => 'enableStaticNat',
|
541
|
+
'ipAddressId' => ipaddress_id,
|
542
|
+
'virtualmachineId' => virtualmachine_id
|
543
|
+
}
|
544
|
+
send_request(params)
|
545
|
+
end
|
546
|
+
|
547
|
+
def disable_static_nat(ipaddress)
|
548
|
+
params = {
|
549
|
+
'command' => 'disableStaticNat',
|
550
|
+
'ipAddressId' => ipaddress['id']
|
551
|
+
}
|
552
|
+
send_async_request(params)
|
553
|
+
end
|
554
|
+
|
555
|
+
def create_ip_fwd_rule(ipaddress_id, protocol, start_port, end_port)
|
556
|
+
params = {
|
557
|
+
'command' => 'createIpForwardingRule',
|
558
|
+
'ipaddressId' => ipaddress_id,
|
559
|
+
'protocol' => protocol,
|
560
|
+
'startport' => start_port,
|
561
|
+
'endport' => end_port
|
562
|
+
}
|
563
|
+
|
564
|
+
send_async_request(params)
|
565
|
+
end
|
566
|
+
|
567
|
+
|
475
568
|
##
|
476
569
|
# Disassociates an ip address from the account.
|
477
570
|
#
|
@@ -529,10 +622,13 @@ module CloudstackClient
|
|
529
622
|
##
|
530
623
|
# Sends a synchronous request to the CloudStack API and returns the response as a Hash.
|
531
624
|
#
|
532
|
-
# The wrapper element of the response (e.g. mycommandresponse) is discarded and the
|
625
|
+
# The wrapper element of the response (e.g. mycommandresponse) is discarded and the
|
533
626
|
# contents of that element are returned.
|
534
627
|
|
535
628
|
def send_request(params)
|
629
|
+
if @project_id
|
630
|
+
params['projectId'] = @project_id
|
631
|
+
end
|
536
632
|
params['response'] = 'json'
|
537
633
|
params['apiKey'] = @api_key
|
538
634
|
|
@@ -547,8 +643,12 @@ module CloudstackClient
|
|
547
643
|
signature = CGI.escape(signature)
|
548
644
|
|
549
645
|
url = "#{@api_url}?#{data}&signature=#{signature}"
|
550
|
-
|
551
|
-
|
646
|
+
uri = URI.parse(url)
|
647
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
648
|
+
http.use_ssl = @use_ssl
|
649
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
650
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
651
|
+
response = http.request(request)
|
552
652
|
|
553
653
|
if !response.is_a?(Net::HTTPOK) then
|
554
654
|
puts "Error #{response.code}: #{response.message}"
|
metadata
CHANGED
@@ -1,103 +1,103 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: knife-cloudstack
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.13
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 0
|
9
|
-
- 12
|
10
|
-
version: 0.0.12
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- Ryan Holmes
|
14
9
|
- KC Braunschweig
|
15
10
|
- John E. Vincent
|
11
|
+
- Chirag Jog
|
16
12
|
autorequire:
|
17
13
|
bindir: bin
|
18
14
|
cert_chain: []
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
- !ruby/object:Gem::Dependency
|
15
|
+
date: 2012-11-28 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
23
18
|
name: chef
|
19
|
+
requirement: !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ! '>='
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.10.0
|
25
|
+
type: :runtime
|
24
26
|
prerelease: false
|
25
|
-
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
28
|
none: false
|
27
|
-
requirements:
|
28
|
-
- -
|
29
|
-
- !ruby/object:Gem::Version
|
30
|
-
hash: 55
|
31
|
-
segments:
|
32
|
-
- 0
|
33
|
-
- 10
|
34
|
-
- 0
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
35
32
|
version: 0.10.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: knife-windows
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
36
41
|
type: :runtime
|
37
|
-
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
38
49
|
description: A Knife plugin to create, list and manage CloudStack servers
|
39
|
-
email:
|
50
|
+
email:
|
40
51
|
- rholmes@edmunds.com
|
41
|
-
-
|
52
|
+
- kcbraunschweig@gmail.com
|
42
53
|
- lusis.org+github.com@gmail.com
|
54
|
+
- chirag.jog@me.com
|
43
55
|
executables: []
|
44
|
-
|
45
56
|
extensions: []
|
46
|
-
|
47
|
-
extra_rdoc_files:
|
57
|
+
extra_rdoc_files:
|
48
58
|
- README.rdoc
|
49
59
|
- CHANGES.rdoc
|
50
60
|
- LICENSE
|
51
|
-
files:
|
61
|
+
files:
|
52
62
|
- CHANGES.rdoc
|
53
63
|
- README.rdoc
|
54
64
|
- LICENSE
|
55
65
|
- lib/knife-cloudstack/connection.rb
|
56
|
-
- lib/chef/knife/cs_stack_delete.rb
|
57
66
|
- lib/chef/knife/cs_server_list.rb
|
58
67
|
- lib/chef/knife/cs_network_list.rb
|
59
68
|
- lib/chef/knife/cs_server_delete.rb
|
60
69
|
- lib/chef/knife/cs_template_list.rb
|
61
|
-
- lib/chef/knife/cs_server_reboot.rb
|
62
|
-
- lib/chef/knife/cs_server_start.rb
|
63
|
-
- lib/chef/knife/cs_service_list.rb
|
64
|
-
- lib/chef/knife/cs_zone_list.rb
|
65
70
|
- lib/chef/knife/cs_server_stop.rb
|
71
|
+
- lib/chef/knife/cs_zone_list.rb
|
72
|
+
- lib/chef/knife/cs_stack_delete.rb
|
66
73
|
- lib/chef/knife/cs_hosts.rb
|
67
74
|
- lib/chef/knife/cs_stack_create.rb
|
75
|
+
- lib/chef/knife/cs_service_list.rb
|
76
|
+
- lib/chef/knife/cs_server_start.rb
|
68
77
|
- lib/chef/knife/cs_server_create.rb
|
78
|
+
- lib/chef/knife/cs_server_reboot.rb
|
69
79
|
homepage: http://cloudstack.org/
|
70
80
|
licenses: []
|
71
|
-
|
72
81
|
post_install_message:
|
73
82
|
rdoc_options: []
|
74
|
-
|
75
|
-
require_paths:
|
83
|
+
require_paths:
|
76
84
|
- lib
|
77
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
86
|
none: false
|
79
|
-
requirements:
|
80
|
-
- -
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
|
83
|
-
|
84
|
-
- 0
|
85
|
-
version: "0"
|
86
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
92
|
none: false
|
88
|
-
requirements:
|
89
|
-
- -
|
90
|
-
- !ruby/object:Gem::Version
|
91
|
-
|
92
|
-
segments:
|
93
|
-
- 0
|
94
|
-
version: "0"
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
95
97
|
requirements: []
|
96
|
-
|
97
98
|
rubyforge_project:
|
98
|
-
rubygems_version: 1.8.
|
99
|
+
rubygems_version: 1.8.24
|
99
100
|
signing_key:
|
100
101
|
specification_version: 3
|
101
102
|
summary: A knife plugin for the CloudStack API
|
102
103
|
test_files: []
|
103
|
-
|