knife-cloudstack-fog 0.5.0 → 0.5.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.
@@ -1,501 +1,513 @@
1
- # Author:: Jeff Moody (<jmoody@datapipe.com>), Takashi Kanai (<anikundesu@gmail.com>)
2
- # Copyright:: Copyright (c) 2012 Datapipe, Copyright (c) 2012 IDC Frontier Inc.
3
- # License:: Apache License, Version 2.0
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
- #
17
-
18
- require 'chef/knife/bootstrap'
19
- Chef::Knife::Bootstrap.load_deps
20
- require 'socket'
21
- require 'net/ssh/multi'
22
- require 'chef/json_compat'
23
- require 'chef/knife/cloudstack_base'
24
- require 'chef/knife/ssh'
25
- Chef::Knife::Ssh.load_deps
26
-
27
- class Chef
28
- class Knife
29
- class CloudstackServerCreate < Knife
30
-
31
- include Knife::CloudstackBase
32
-
33
- banner "knife cloudstack server create -s SERVICEID -t TEMPLATEID -z ZONEID (options)"
34
-
35
- option :cloudstack_serviceid,
36
- :short => "-s SERVICEID",
37
- :long => "--serviceid SERVICEID",
38
- :description => "The CloudStack service offering ID."
39
-
40
- option :cloudstack_templateid,
41
- :short => "-t TEMPLATEID",
42
- :long => "--templateid TEMPLATEID",
43
- :description => "The CloudStack template ID for the server."
44
-
45
- option :cloudstack_zoneid,
46
- :short => "-z ZONEID",
47
- :long => "--zoneid ZONE",
48
- :description => "The CloudStack zone ID for the server."
49
-
50
- option :cloudstack_networkids,
51
- :short => "-w NETWORKIDS",
52
- :long => "--networkids NETWORKIDS",
53
- :description => "Comma separated list of CloudStack network IDs.",
54
- :proc => lambda { |n| n.split(/[\s,]+/) },
55
- :default => []
56
-
57
- option :cloudstack_groupids,
58
- :short => "-g SECURITYGROUPIDS",
59
- :long => "--groupids SECURITYGROUPIDS",
60
- :description => "Comma separated list of CloudStack Security Group IDs.",
61
- :proc => lambda { |n| n.split(/[\s,]+/) },
62
- :default => []
63
-
64
- option :cloudstack_groupnames,
65
- :short => "-G SECURITYGROUPNAMES",
66
- :long => "--groupnames SECURITYGROUPNAMES",
67
- :description => "Comma separated list of CloudStack Security Group names. Each group name must be encapuslated in quotes if it contains whitespace.",
68
- :proc => lambda { |n| n.split(/[\s,]+/) },
69
- :default => []
70
-
71
- option :distro,
72
- :short => "-d DISTRO",
73
- :long => "--distro DISTRO",
74
- :description => "Bootstrap a distro using a template; default is 'chef-full'",
75
- :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
76
- :default => "chef-full"
77
-
78
- option :template_file,
79
- :long => "--template-file TEMPLATE",
80
- :description => "Full path to location of template to use",
81
- :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
82
- :default => false
83
-
84
- option :run_list,
85
- :short => "-r RUN_LIST",
86
- :long => "--run-list RUN_LIST",
87
- :description => "Comma separated list of roles/recipes to apply",
88
- :proc => lambda { |o| o.split(/[\s,]+/) },
89
- :default => []
90
-
91
- option :ssh_user,
92
- :short => "-x USERNAME",
93
- :long => "--ssh-user USERNAME",
94
- :description => "The ssh username",
95
- :default => 'root'
96
-
97
- option :ssh_password,
98
- :short => "-P PASSWORD",
99
- :long => "--ssh-password PASSWORD",
100
- :description => "The ssh password"
101
-
102
- option :identity_file,
103
- :short => "-i PRIVATE_KEY_FILE",
104
- :long => "--identity-file PRIVATE_KEY_FILE",
105
- :description => "The Private key file for authenticating SSH session. --keypair option is also needed."
106
-
107
- option :ssh_port,
108
- :short => "-p PORT",
109
- :long => "--ssh-port PORT",
110
- :description => "The port which SSH should be listening on. If unspecified, will default to 22."
111
-
112
- option :server_display_name,
113
- :short => "-N NAME",
114
- :long => "--display-name NAME",
115
- :description => "The instance display name"
116
-
117
- option :host_name,
118
- :short => "-H NAME",
119
- :long => "--hostname NAME",
120
- :description => "The instance host name"
121
-
122
- option :keypair,
123
- :short => "-k KEYPAIR",
124
- :long => "--keypair KEYPAIR",
125
- :description => "The CloudStack Key Pair to use for SSH key authentication."
126
-
127
- option :diskoffering,
128
- :short => "-D DISKOFFERINGID",
129
- :long => "--diskoffering DISKOFFERINGID",
130
- :description => "Specifies either the Disk Offering ID for the ROOT disk for an ISO template, or a DATA disk."
131
-
132
- option :size,
133
- :short => "-Z SIZE",
134
- :long => "--size SIZE",
135
- :description => "Specifies the arbitrary Disk Size for DATADISK volume in GB. Must be passed with custom size Disk Offering ID."
136
-
137
- option :random_ssh_port,
138
- :long => "--random-ssh-port",
139
- :description => "Map a random, unused high-level port to 22 for SSH and creates a port forward for this mapping. For Isolated Networking and VPC only."
140
-
141
- option :ssh_gateway,
142
- :short => "-W GATEWAY",
143
- :long => "--ssh-gateway GATEWAY",
144
- :description => "The ssh gateway server. Connection is defined as USERNAME@HOST:PORT",
145
- :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
146
-
147
- # def bootstrap_for_node(host, user, password)
148
- def bootstrap_for_node(server, ssh_host)
149
- host = server["name"]
150
- user = config[:ssh_user]
151
- password = server["password"]
152
- Chef::Log.debug("Bootstrap host: #{host}")
153
- Chef::Log.debug("Bootstrap user: #{user}")
154
- Chef::Log.debug("Bootstrap pass: #{password}")
155
- bootstrap = Chef::Knife::Bootstrap.new
156
- bootstrap.name_args = [ssh_host]
157
- bootstrap.config[:run_list] = config[:run_list]
158
- bootstrap.config[:ssh_user] = user
159
- bootstrap.config[:ssh_password] = password
160
- bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
161
- bootstrap.config[:identity_file] = locate_config_value(:identity_file)
162
- bootstrap.config[:chef_node_name] = config[:server_display_name] if config[:server_display_name]
163
- bootstrap.config[:prerelease] = config[:prerelease]
164
- bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
165
- bootstrap.config[:distro] = locate_config_value(:distro)
166
- bootstrap.config[:use_sudo] = true
167
- bootstrap.config[:template_file] = locate_config_value(:template_file)
168
- bootstrap.config[:environment] = config[:environment]
169
- # may be needed for vpc_mode
170
- bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
171
- begin
172
- bootstrap
173
- rescue
174
- sleep @initial_sleep_delay
175
- retry
176
- end
177
- end
178
-
179
- def check_port_available(public_port, ipaddressid)
180
- Chef::Log.debug("Checking if port #{public_port} is available.")
181
- pubport = public_port.to_i
182
- port_forward_rules_query = connection.list_port_forwarding_rules({'ipaddressid' => ipaddressid })
183
- port_rules = port_forward_rules_query['listportforwardingrulesresponse']['portforwardingrule']
184
- is_available = true
185
- some_possible_rules = port_rules.select { |rule| rule['publicport'].to_i <= pubport }
186
- possible_rules = some_possible_rules.select { |rule| rule['publicendport'].to_i >= pubport }
187
- possible_rules.each do |rule|
188
- startport = rule['publicport'].to_i
189
- endport = rule['publicendport'].to_i
190
- Chef::Log.debug("Determining if #{pubport} is between #{startport} and #{endport}.")
191
- if (endport != startport)
192
- if pubport.between?(startport, endport)
193
- is_available = false
194
- else
195
- is_available = true
196
- end
197
- else
198
- if (pubport == startport)
199
- is_available = false
200
- else
201
- is_available = true
202
- end
203
- end
204
- end
205
- return is_available
206
- end
207
-
208
- def add_port_forward(public_start_port, public_end_port, server_id, ipaddressid, privateport)
209
- pfwdops = {}
210
- pfwdops['ipaddressid'] = ipaddressid
211
- pfwdops['privateport'] = privateport
212
- pfwdops['protocol'] = "TCP"
213
- pfwdops['virtualmachineid'] = server_id
214
- pfwdops['openfirewall'] = "true"
215
- pfwdops['publicport'] = public_start_port
216
- pfwdops['publicendport'] = public_end_port
217
- rule_create_job = connection.create_port_forwarding_rule(pfwdops)
218
- print "#{ui.color("Creating port forwarding rule.", :cyan)}"
219
- while (@connection.query_async_job_result({'jobid' => rule_create_job['createportforwardingruleresponse']['jobid']})['queryasyncjobresultresponse'].fetch('jobstatus') == 0)
220
- print("#{ui.color(".", :cyan)}")
221
- sleep 2
222
- end
223
- print("\n")
224
- end
225
-
226
- def create_server_def
227
- server_def = {
228
- "templateid" => locate_config_value(:cloudstack_templateid),
229
- "serviceofferingid" => locate_config_value(:cloudstack_serviceid),
230
- "zoneid" => locate_config_value(:cloudstack_zoneid)
231
- }
232
-
233
- if locate_config_value(:server_display_name) != nil
234
- server_def["displayname"] = locate_config_value(:server_display_name)
235
- end
236
-
237
- if locate_config_value(:host_name) != nil
238
- server_def["name"] = locate_config_value(:host_name)
239
- end
240
-
241
- network_ids = []
242
- if locate_config_value(:cloudstack_networkids) != []
243
- cs_networkids = locate_config_value(:cloudstack_networkids)
244
- cs_networkids.each do |id|
245
- network_ids.push(id)
246
- end
247
- server_def["networkids"] = network_ids
248
- end
249
-
250
- security_groups = []
251
- if locate_config_value(:cloudstack_groupids) != []
252
- cs_groupids = locate_config_value(:cloudstack_groupids)
253
- cs_groupids.each do |id|
254
- security_groups.push(id)
255
- end
256
- server_def["securitygroupids"] = security_groups
257
- elsif locate_config_value(:cloudstack_groupnames) != []
258
- cs_groupnames = locate_config_value(:cloudstack_groupnames)
259
- cs_groupnames.each do |name|
260
- security_groups.push(name)
261
- end
262
- server_def["securitygroupnames"] = security_groups
263
- end
264
-
265
- if locate_config_value(:keypair) != nil
266
- server_def["keypair"] = locate_config_value(:keypair)
267
- end
268
-
269
- if locate_config_value(:diskoffering) != nil
270
- server_def["diskofferingid"] = locate_config_value(:diskoffering)
271
- end
272
-
273
- if locate_config_value(:size) != nil
274
- server_def["size"] = locate_config_value(:size)
275
- end
276
-
277
- server_def
278
- end
279
-
280
- def knife_ssh
281
- ssh = Chef::Knife::Ssh.new
282
- ssh.ui = ui
283
- ssh.name_args = [ @primary_ip, ssh_command ]
284
- ssh.config[:ssh_user] = Chef::Config[:knife][:ssh_user] || config[:ssh_user]
285
- ssh.config[:ssh_password] = config[:ssh_password]
286
- ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
287
- ssh.config[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway]
288
- ssh.config[:forward_agent] = Chef::Config[:knife][:forward_agent] || config[:forward_agent]
289
- ssh.config[:identity_file] = Chef::Config[:knife][:identity_file] || config[:identity_file]
290
- ssh.config[:manual] = true
291
- ssh.config[:host_key_verify] = Chef::Config[:knife][:host_key_verify] || config[:host_key_verify]
292
- ssh.config[:on_error] = :raise
293
- ssh
294
- end
295
-
296
- def find_template(template=nil)
297
- # Are we bootstrapping using an already shipped template?
298
- if config[:template_file]
299
- bootstrap_files = config[:template_file]
300
- else
301
- bootstrap_files = []
302
- bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{config[:distro]}.erb")
303
- bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{config[:distro]}.erb") if Knife.chef_config_dir
304
- bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{config[:distro]}.erb") if ENV['HOME']
305
- bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{config[:distro]}.erb"))
306
- bootstrap_files.flatten!
307
- end
308
-
309
- template = Array(bootstrap_files).find do |bootstrap_template|
310
- Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
311
- File.exists?(bootstrap_template)
312
- end
313
-
314
- unless template
315
- ui.info("Can not find bootstrap definition for #{config[:distro]}")
316
- raise Errno::ENOENT
317
- end
318
-
319
- Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}")
320
-
321
- template
322
- end
323
-
324
- def render_template(template=nil)
325
- context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config)
326
- Erubis::Eruby.new(template).evaluate(context)
327
- end
328
-
329
- def read_template
330
- IO.read(@template_file).chomp
331
- end
332
-
333
- def knife_ssh_with_password_auth
334
- ssh = knife_ssh
335
- ssh.config[:identity_file] = nil
336
- ssh.config[:ssh_password] = ssh.get_password
337
- ssh
338
- end
339
-
340
- def ssh_command
341
- command = render_template(read_template)
342
-
343
- if config[:use_sudo]
344
- command = config[:use_sudo_password] ? "echo #{config[:ssh_password]} | sudo -S #{command}" : "sudo #{command}"
345
- end
346
-
347
- command
348
- end
349
-
350
- def run
351
- $stdout.sync = true
352
- options = create_server_def
353
- Chef::Log.debug("Options: #{options} \n")
354
-
355
- @initial_sleep_delay = 10
356
- @sshport = 22
357
-
358
- config[:host_key_verify] = false
359
-
360
- if locate_config_value(:ssh_port) != nil
361
- @sshport = locate_config_value(:ssh_port).to_i
362
- end
363
-
364
- serverdeploy = connection.deploy_virtual_machine(options)
365
- jobid = serverdeploy['deployvirtualmachineresponse'].fetch('jobid')
366
-
367
- server_start = connection.query_async_job_result('jobid'=>jobid)
368
-
369
- Chef::Log.debug("Job ID: #{jobid} \n")
370
-
371
- print "#{ui.color("Waiting for server", :magenta)}"
372
- while server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 0
373
- print "#{ui.color(".", :magenta)}"
374
- sleep @initial_sleep_delay
375
- server_start = connection.query_async_job_result('jobid'=>jobid)
376
- Chef::Log.debug("Server_Start: #{server_start} \n")
377
- end
378
- puts "\n\n"
379
-
380
- if server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 2
381
- errortext = server_start['queryasyncjobresultresponse'].fetch('jobresult').fetch('errortext')
382
- puts "#{ui.color("ERROR! Job failed with #{errortext}", :red)}"
383
- end
384
-
385
- if server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 1
386
-
387
- Chef::Log.debug("Job ID: #{jobid} \n")
388
- Chef::Log.debug("Options: #{options} \n")
389
- server_start = connection.query_async_job_result('jobid'=>jobid)
390
- Chef::Log.debug("Server_Start: #{server_start} \n")
391
-
392
- @server = server_start['queryasyncjobresultresponse']['jobresult']['virtualmachine']
393
-
394
- server_display_name = @server['displayname']
395
- server_id = @server['name']
396
- server_serviceoffering = @server['serviceofferingname']
397
- server_template = @server['templatename']
398
- if @server['password'] != nil
399
- config[:ssh_password] = @server['password']
400
- else
401
- config[:ssh_password] = locate_config_value(:ssh_password)
402
- end
403
-
404
- ssh_user = locate_config_value(:ssh_user)
405
-
406
- @primary_ip = nil
407
-
408
- if @server['nic'].size > 0
409
- @primary_ip = @server['nic'].first['ipaddress']
410
- end
411
-
412
- if locate_config_value(:random_ssh_port) != nil
413
- public_ips = connection.list_public_ip_addresses("associatednetworkid" => @server['nic'][0]['networkid'])
414
- primary_public_ip_id = public_ips['listpublicipaddressesresponse']['publicipaddress'][0]['id']
415
- @primary_ip = public_ips['listpublicipaddressesresponse']['publicipaddress'][0]['ipaddress']
416
- pubport = rand(49152..65535)
417
- while (check_port_available(pubport, primary_public_ip_id) == false)
418
- pubport = rand(49152..65535)
419
- end
420
- add_port_forward(pubport, pubport, server_id, primary_public_ip_id, @sshport)
421
- @sshport = pubport
422
- end
423
-
424
- Chef::Log.debug("Connecting over port #{@sshport}")
425
- config[:ssh_port] = @sshport
426
- config[:server_name] = @primary_ip
427
- @template_file = find_template(config[:bootstrap_template])
428
-
429
- puts "\n\n"
430
- puts "#{ui.color("Name", :cyan)}: #{server_display_name}"
431
- puts "#{ui.color("Primary IP", :cyan)}: #{@primary_ip}"
432
- puts "#{ui.color("Username", :cyan)}: #{ssh_user}"
433
- puts "#{ui.color("Password", :cyan)}: #{config[:ssh_password]}"
434
-
435
- print "#{ui.color("Waiting for SSH.", :magenta)}"
436
- if config[:ssh_gateway]
437
- Chef::Log.debug("Using SSH Gateway: #{config[:ssh_gateway]}")
438
- sleep @initial_sleep_delay
439
- print "#{ui.color(".", :magenta)}"
440
- sleep @initial_sleep_delay
441
- print "#{ui.color(".", :magenta)}"
442
- end
443
- begin
444
- knife_ssh.run
445
- rescue Net::SSH::AuthenticationFailed
446
- unless config[:ssh_password]
447
- ui.info("Failed to authenticate #{config[:ssh_user]} - trying password auth")
448
- knife_ssh_with_password_auth.run
449
- end
450
- rescue Errno::ECONNREFUSED
451
- sleep @initial_sleep_delay
452
- print "#{ui.color(".", :magenta)}"
453
- retry
454
- rescue SocketError
455
- sleep @initial_sleep_delay
456
- print "#{ui.color(".", :magenta)}"
457
- retry
458
- rescue Errno::ETIMEDOUT
459
- sleep @initial_sleep_delay
460
- print "#{ui.color(".", :magenta)}"
461
- retry
462
- rescue Errno::EPERM
463
- sleep @initial_sleep_delay
464
- print "#{ui.color(".", :magenta)}"
465
- retry
466
- rescue Errno::EHOSTUNREACH
467
- sleep @initial_sleep_delay
468
- print "#{ui.color(".", :magenta)}"
469
- retry
470
- rescue Errno::ENETUNREACH
471
- sleep @initial_sleep_delay
472
- print "#{ui.color(".", :magenta)}"
473
- retry
474
- rescue Net::SSH::Disconnect
475
- sleep @initial_sleep_delay
476
- print "#{ui.color(".", :magenta)}"
477
- retry
478
- rescue
479
- puts caller
480
- puts $!.inspect
481
- end
482
-
483
- Chef::Log.debug("#{@server}")
484
-
485
- puts "\n"
486
- puts "#{ui.color("Instance Name", :green)}: #{server_display_name}"
487
- puts "#{ui.color("Instance ID", :green)}: #{server_id}"
488
- puts "#{ui.color("Service Offering", :green)}: #{server_serviceoffering}"
489
- puts "#{ui.color("Template", :green)}: #{server_template}"
490
- puts "#{ui.color("Public IP Address", :green)}: #{@primary_ip}"
491
- puts "#{ui.color("Port", :green)}: #{@sshport}"
492
- puts "#{ui.color("User", :green)}: #{ssh_user}"
493
- puts "#{ui.color("Password", :green)}: #{config[:ssh_password]}"
494
- puts "#{ui.color("Environment", :green)}: #{config[:environment] || '_default'}"
495
- puts "#{ui.color("Run List", :green)}: #{config[:run_list].join(', ')}"
496
- end
497
-
498
- end
499
- end
500
- end
501
- end
1
+ # Author:: Jeff Moody (<jmoody@datapipe.com>), Takashi Kanai (<anikundesu@gmail.com>)
2
+ # Copyright:: Copyright (c) 2012 Datapipe, Copyright (c) 2012 IDC Frontier Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef/knife/bootstrap'
19
+ Chef::Knife::Bootstrap.load_deps
20
+ require 'socket'
21
+ require 'net/ssh/multi'
22
+ require 'chef/json_compat'
23
+ require 'chef/knife/cloudstack_base'
24
+ require 'chef/knife/ssh'
25
+ Chef::Knife::Ssh.load_deps
26
+
27
+ class Chef
28
+ class Knife
29
+ class CloudstackServerCreate < Knife
30
+
31
+ include Knife::CloudstackBase
32
+
33
+ banner "knife cloudstack server create -s SERVICEID -t TEMPLATEID -z ZONEID (options)"
34
+
35
+ option :cloudstack_serviceid,
36
+ :short => "-s SERVICEID",
37
+ :long => "--serviceid SERVICEID",
38
+ :description => "The CloudStack service offering ID."
39
+
40
+ option :cloudstack_templateid,
41
+ :short => "-t TEMPLATEID",
42
+ :long => "--templateid TEMPLATEID",
43
+ :description => "The CloudStack template ID for the server."
44
+
45
+ option :cloudstack_zoneid,
46
+ :short => "-Z ZONEID",
47
+ :long => "--zoneid ZONE",
48
+ :description => "The CloudStack zone ID for the server."
49
+
50
+ option :cloudstack_networkids,
51
+ :short => "-w NETWORKIDS",
52
+ :long => "--networkids NETWORKIDS",
53
+ :description => "Comma separated list of CloudStack network IDs.",
54
+ :proc => lambda { |n| n.split(/[\s,]+/) },
55
+ :default => []
56
+
57
+ option :cloudstack_groupids,
58
+ :short => "-g SECURITYGROUPIDS",
59
+ :long => "--groupids SECURITYGROUPIDS",
60
+ :description => "Comma separated list of CloudStack Security Group IDs.",
61
+ :proc => lambda { |n| n.split(/[\s,]+/) },
62
+ :default => []
63
+
64
+ option :cloudstack_groupnames,
65
+ :short => "-G SECURITYGROUPNAMES",
66
+ :long => "--groupnames SECURITYGROUPNAMES",
67
+ :description => "Comma separated list of CloudStack Security Group names. Each group name must be encapuslated in quotes if it contains whitespace.",
68
+ :proc => lambda { |n| n.split(/[\s,]+/) },
69
+ :default => []
70
+
71
+ option :distro,
72
+ :short => "-d DISTRO",
73
+ :long => "--distro DISTRO",
74
+ :description => "Bootstrap a distro using a template; default is 'chef-full'",
75
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
76
+ :default => "chef-full"
77
+
78
+ option :template_file,
79
+ :long => "--template-file TEMPLATE",
80
+ :description => "Full path to location of template to use",
81
+ :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
82
+ :default => false
83
+
84
+ option :run_list,
85
+ :short => "-r RUN_LIST",
86
+ :long => "--run-list RUN_LIST",
87
+ :description => "Comma separated list of roles/recipes to apply",
88
+ :proc => lambda { |o| o.split(/[\s,]+/) },
89
+ :default => []
90
+
91
+ option :ssh_user,
92
+ :short => "-x USERNAME",
93
+ :long => "--ssh-user USERNAME",
94
+ :description => "The ssh username",
95
+ :default => 'root'
96
+
97
+ option :ssh_password,
98
+ :short => "-P PASSWORD",
99
+ :long => "--ssh-password PASSWORD",
100
+ :description => "The ssh password"
101
+
102
+ option :identity_file,
103
+ :short => "-i PRIVATE_KEY_FILE",
104
+ :long => "--identity-file PRIVATE_KEY_FILE",
105
+ :description => "The Private key file for authenticating SSH session. --keypair option is also needed."
106
+
107
+ option :ssh_port,
108
+ :short => "-p PORT",
109
+ :long => "--ssh-port PORT",
110
+ :description => "The port which SSH should be listening on. If unspecified, will default to 22."
111
+
112
+ option :server_display_name,
113
+ :short => "-N NAME",
114
+ :long => "--display-name NAME",
115
+ :description => "The instance display name"
116
+
117
+ option :host_name,
118
+ :short => "-H NAME",
119
+ :long => "--hostname NAME",
120
+ :description => "The instance host name"
121
+
122
+ option :keypair,
123
+ :short => "-k KEYPAIR",
124
+ :long => "--keypair KEYPAIR",
125
+ :description => "The CloudStack Key Pair to use for SSH key authentication."
126
+
127
+ option :diskoffering,
128
+ :short => "-D DISKOFFERINGID",
129
+ :long => "--diskoffering DISKOFFERINGID",
130
+ :description => "Specifies either the Disk Offering ID for the ROOT disk for an ISO template, or a DATA disk."
131
+
132
+ option :size,
133
+ :long => "--size SIZE",
134
+ :description => "Specifies the arbitrary Disk Size for DATADISK volume in GB. Must be passed with custom size Disk Offering ID."
135
+
136
+ option :random_ssh_port,
137
+ :long => "--random-ssh-port",
138
+ :description => "Map a random, unused high-level port to 22 for SSH and creates a port forward for this mapping. For Isolated Networking and VPC only."
139
+
140
+ option :ssh_gateway,
141
+ :short => "-W GATEWAY",
142
+ :long => "--ssh-gateway GATEWAY",
143
+ :description => "The ssh gateway server. Connection is defined as USERNAME@HOST:PORT",
144
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
145
+
146
+ # def bootstrap_for_node(host, user, password)
147
+ def bootstrap_for_node(server, ssh_host)
148
+ host = server["name"]
149
+ user = config[:ssh_user]
150
+ password = server["password"]
151
+ Chef::Log.debug("Bootstrap host: #{host}")
152
+ Chef::Log.debug("Bootstrap user: #{user}")
153
+ Chef::Log.debug("Bootstrap pass: #{password}")
154
+ bootstrap = Chef::Knife::Bootstrap.new
155
+ bootstrap.name_args = [ssh_host]
156
+ bootstrap.config[:run_list] = config[:run_list]
157
+ bootstrap.config[:ssh_user] = user
158
+ bootstrap.config[:ssh_password] = password
159
+ bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
160
+ bootstrap.config[:identity_file] = locate_config_value(:identity_file)
161
+ bootstrap.config[:chef_node_name] = config[:server_display_name] if config[:server_display_name]
162
+ bootstrap.config[:prerelease] = config[:prerelease]
163
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
164
+ bootstrap.config[:distro] = locate_config_value(:distro)
165
+ bootstrap.config[:use_sudo] = true
166
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
167
+ bootstrap.config[:environment] = config[:environment]
168
+ # may be needed for vpc_mode
169
+ bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
170
+ begin
171
+ bootstrap
172
+ rescue
173
+ sleep @initial_sleep_delay
174
+ retry
175
+ end
176
+ end
177
+
178
+ def check_port_available(public_port, ipaddressid)
179
+ Chef::Log.debug("Checking if port #{public_port} is available.")
180
+ pubport = public_port.to_i
181
+ port_forward_rules_query = connection.list_port_forwarding_rules({'ipaddressid' => ipaddressid })
182
+ port_rules = port_forward_rules_query['listportforwardingrulesresponse']['portforwardingrule']
183
+ is_available = true
184
+ some_possible_rules = port_rules.select { |rule| rule['publicport'].to_i <= pubport }
185
+ possible_rules = some_possible_rules.select { |rule| rule['publicendport'].to_i >= pubport }
186
+ possible_rules.each do |rule|
187
+ startport = rule['publicport'].to_i
188
+ endport = rule['publicendport'].to_i
189
+ Chef::Log.debug("Determining if #{pubport} is between #{startport} and #{endport}.")
190
+ if (endport != startport)
191
+ if pubport.between?(startport, endport)
192
+ is_available = false
193
+ else
194
+ is_available = true
195
+ end
196
+ else
197
+ if (pubport == startport)
198
+ is_available = false
199
+ else
200
+ is_available = true
201
+ end
202
+ end
203
+ end
204
+ return is_available
205
+ end
206
+
207
+ def add_port_forward(public_start_port, public_end_port, server_id, ipaddressid, privateport)
208
+ pfwdops = {}
209
+ pfwdops['ipaddressid'] = ipaddressid
210
+ pfwdops['privateport'] = privateport
211
+ pfwdops['protocol'] = "TCP"
212
+ pfwdops['virtualmachineid'] = server_id
213
+ pfwdops['openfirewall'] = "true"
214
+ pfwdops['publicport'] = public_start_port
215
+ pfwdops['publicendport'] = public_end_port
216
+ rule_create_job = connection.create_port_forwarding_rule(pfwdops)
217
+ print "#{ui.color("Creating port forwarding rule.", :cyan)}"
218
+ while (@connection.query_async_job_result({'jobid' => rule_create_job['createportforwardingruleresponse']['jobid']})['queryasyncjobresultresponse'].fetch('jobstatus') == 0)
219
+ print("#{ui.color(".", :cyan)}")
220
+ sleep 2
221
+ end
222
+ print("\n")
223
+ end
224
+
225
+ def create_server_def
226
+ server_def = {
227
+ "templateid" => locate_config_value(:cloudstack_templateid),
228
+ "serviceofferingid" => locate_config_value(:cloudstack_serviceid),
229
+ "zoneid" => locate_config_value(:cloudstack_zoneid)
230
+ }
231
+
232
+ if locate_config_value(:server_display_name) != nil
233
+ server_def["displayname"] = locate_config_value(:server_display_name)
234
+ end
235
+
236
+ if locate_config_value(:host_name) != nil
237
+ server_def["name"] = locate_config_value(:host_name)
238
+ end
239
+
240
+ network_ids = []
241
+ if locate_config_value(:cloudstack_networkids) != []
242
+ cs_networkids = locate_config_value(:cloudstack_networkids)
243
+ cs_networkids.each do |id|
244
+ network_ids.push(id)
245
+ end
246
+ server_def["networkids"] = network_ids
247
+ end
248
+
249
+ security_groups = []
250
+ if locate_config_value(:cloudstack_groupids) != []
251
+ cs_groupids = locate_config_value(:cloudstack_groupids)
252
+ cs_groupids.each do |id|
253
+ security_groups.push(id)
254
+ end
255
+ server_def["securitygroupids"] = security_groups
256
+ elsif locate_config_value(:cloudstack_groupnames) != []
257
+ cs_groupnames = locate_config_value(:cloudstack_groupnames)
258
+ cs_groupnames.each do |name|
259
+ security_groups.push(name)
260
+ end
261
+ server_def["securitygroupnames"] = security_groups
262
+ end
263
+
264
+ if locate_config_value(:keypair) != nil
265
+ server_def["keypair"] = locate_config_value(:keypair)
266
+ end
267
+
268
+ if locate_config_value(:diskoffering) != nil
269
+ server_def["diskofferingid"] = locate_config_value(:diskoffering)
270
+ end
271
+
272
+ if locate_config_value(:size) != nil
273
+ server_def["size"] = locate_config_value(:size)
274
+ end
275
+
276
+ server_def
277
+ end
278
+
279
+ def knife_ssh
280
+ ssh = Chef::Knife::Ssh.new
281
+ ssh.ui = ui
282
+ ssh.name_args = [ @primary_ip, ssh_command ]
283
+ ssh.config[:ssh_user] = Chef::Config[:knife][:ssh_user] || config[:ssh_user]
284
+ ssh.config[:ssh_password] = config[:ssh_password]
285
+ ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
286
+ ssh.config[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway]
287
+ ssh.config[:forward_agent] = Chef::Config[:knife][:forward_agent] || config[:forward_agent]
288
+ ssh.config[:identity_file] = Chef::Config[:knife][:identity_file] || config[:identity_file]
289
+ ssh.config[:manual] = true
290
+ ssh.config[:host_key_verify] = Chef::Config[:knife][:host_key_verify] || config[:host_key_verify]
291
+ ssh.config[:on_error] = :raise
292
+ Chef::Log.debug("SSH User: #{ssh.config[:ssh_user]}")
293
+ Chef::Log.debug("SSH Password: #{ssh.config[:ssh_password]}")
294
+ Chef::Log.debug("SSH Port: #{ssh.config[:ssh_port]}")
295
+ Chef::Log.debug("SSH Gateway: #{ssh.config[:ssh_gateway]}")
296
+ Chef::Log.debug("SSH Identity File: #{ssh.config[:identity_file]}")
297
+ ssh
298
+ end
299
+
300
+ def find_template(template=nil)
301
+ # Are we bootstrapping using an already shipped template?
302
+ if config[:template_file]
303
+ bootstrap_files = config[:template_file]
304
+ else
305
+ bootstrap_files = []
306
+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{config[:distro]}.erb")
307
+ bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{config[:distro]}.erb") if Knife.chef_config_dir
308
+ bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{config[:distro]}.erb") if ENV['HOME']
309
+ bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{config[:distro]}.erb"))
310
+ bootstrap_files.flatten!
311
+ end
312
+
313
+ template = Array(bootstrap_files).find do |bootstrap_template|
314
+ Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
315
+ File.exists?(bootstrap_template)
316
+ end
317
+
318
+ unless template
319
+ ui.info("Can not find bootstrap definition for #{config[:distro]}")
320
+ raise Errno::ENOENT
321
+ end
322
+
323
+ Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}")
324
+
325
+ template
326
+ end
327
+
328
+ def render_template(template=nil)
329
+ context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config)
330
+ Erubis::Eruby.new(template).evaluate(context)
331
+ end
332
+
333
+ def read_template
334
+ IO.read(@template_file).chomp
335
+ end
336
+
337
+ def knife_ssh_with_password_auth
338
+ ssh = knife_ssh
339
+ ssh.config[:identity_file] = nil
340
+ Chef::Log.debug("Private Key failed or not specified. Trying password of #{ssh.get_password}")
341
+ ssh.config[:ssh_password] = ssh.get_password
342
+ ssh
343
+ end
344
+
345
+ def ssh_command
346
+ command = render_template(read_template)
347
+
348
+ if config[:use_sudo]
349
+ command = config[:use_sudo_password] ? "echo #{config[:ssh_password]} | sudo -S #{command}" : "sudo #{command}"
350
+ end
351
+
352
+ command
353
+ end
354
+
355
+ def run
356
+ $stdout.sync = true
357
+ options = create_server_def
358
+ Chef::Log.debug("Options: #{options} \n")
359
+
360
+ @initial_sleep_delay = 10
361
+ @sshport = 22
362
+
363
+ config[:host_key_verify] = false
364
+
365
+ if locate_config_value(:ssh_port) != nil
366
+ @sshport = locate_config_value(:ssh_port).to_i
367
+ end
368
+
369
+ serverdeploy = connection.deploy_virtual_machine(options)
370
+ jobid = serverdeploy['deployvirtualmachineresponse'].fetch('jobid')
371
+
372
+ server_start = connection.query_async_job_result('jobid'=>jobid)
373
+
374
+ Chef::Log.debug("Job ID: #{jobid} \n")
375
+
376
+ print "#{ui.color("Waiting for server", :magenta)}"
377
+ while server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 0
378
+ print "#{ui.color(".", :magenta)}"
379
+ sleep @initial_sleep_delay
380
+ server_start = connection.query_async_job_result('jobid'=>jobid)
381
+ Chef::Log.debug("Server_Start: #{server_start} \n")
382
+ end
383
+ puts "\n\n"
384
+
385
+ if server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 2
386
+ errortext = server_start['queryasyncjobresultresponse'].fetch('jobresult').fetch('errortext')
387
+ puts "#{ui.color("ERROR! Job failed with #{errortext}", :red)}"
388
+ end
389
+
390
+ if server_start['queryasyncjobresultresponse'].fetch('jobstatus') == 1
391
+
392
+ Chef::Log.debug("Job ID: #{jobid} \n")
393
+ Chef::Log.debug("Options: #{options} \n")
394
+ server_start = connection.query_async_job_result('jobid'=>jobid)
395
+ Chef::Log.debug("Server_Start: #{server_start} \n")
396
+
397
+ @server = server_start['queryasyncjobresultresponse']['jobresult']['virtualmachine']
398
+
399
+ server_display_name = @server['displayname']
400
+ server_id = @server['name']
401
+ server_serviceoffering = @server['serviceofferingname']
402
+ server_template = @server['templatename']
403
+ if @server['password'] != nil
404
+ config[:ssh_password] = @server['password']
405
+ else
406
+ config[:ssh_password] = locate_config_value(:ssh_password)
407
+ end
408
+
409
+ ssh_user = locate_config_value(:ssh_user)
410
+
411
+ @primary_ip = nil
412
+
413
+ if @server['nic'].size > 0
414
+ @primary_ip = @server['nic'].first['ipaddress']
415
+ end
416
+
417
+ if locate_config_value(:random_ssh_port) != nil
418
+ public_ips = connection.list_public_ip_addresses("associatednetworkid" => @server['nic'][0]['networkid'])
419
+ primary_public_ip_id = public_ips['listpublicipaddressesresponse']['publicipaddress'][0]['id']
420
+ @primary_ip = public_ips['listpublicipaddressesresponse']['publicipaddress'][0]['ipaddress']
421
+ pubport = rand(49152..65535)
422
+ while (check_port_available(pubport, primary_public_ip_id) == false)
423
+ pubport = rand(49152..65535)
424
+ end
425
+ add_port_forward(pubport, pubport, server_id, primary_public_ip_id, @sshport)
426
+ @sshport = pubport
427
+ end
428
+
429
+ Chef::Log.debug("Connecting over port #{@sshport}")
430
+ config[:ssh_port] = @sshport
431
+ config[:server_name] = @primary_ip
432
+ @template_file = find_template(config[:bootstrap_template])
433
+
434
+ puts "\n\n"
435
+ puts "#{ui.color("Name", :cyan)}: #{server_display_name}"
436
+ puts "#{ui.color("Primary IP", :cyan)}: #{@primary_ip}"
437
+ puts "#{ui.color("Username", :cyan)}: #{ssh_user}"
438
+ puts "#{ui.color("Password", :cyan)}: #{config[:ssh_password]}"
439
+
440
+ print "#{ui.color("Waiting for SSH.", :magenta)}"
441
+ if config[:ssh_gateway]
442
+ Chef::Log.debug("Using SSH Gateway: #{config[:ssh_gateway]}")
443
+ sleep @initial_sleep_delay
444
+ print "#{ui.color(".", :magenta)}"
445
+ sleep @initial_sleep_delay
446
+ print "#{ui.color(".", :magenta)}"
447
+ end
448
+ begin
449
+ knife_ssh.run
450
+ rescue Net::SSH::AuthenticationFailed
451
+ unless config[:ssh_password]
452
+ ui.info("Failed to authenticate #{config[:ssh_user]} - trying password auth")
453
+ knife_ssh_with_password_auth.run
454
+ end
455
+ sleep @initial_sleep_delay
456
+ print "#{ui.color(".", :magenta)}"
457
+ retry
458
+ rescue Errno::ECONNREFUSED
459
+ sleep @initial_sleep_delay
460
+ print "#{ui.color(".", :magenta)}"
461
+ retry
462
+ rescue SocketError
463
+ sleep @initial_sleep_delay
464
+ print "#{ui.color(".", :magenta)}"
465
+ retry
466
+ rescue Errno::ETIMEDOUT
467
+ sleep @initial_sleep_delay
468
+ print "#{ui.color(".", :magenta)}"
469
+ retry
470
+ rescue Errno::EPERM
471
+ sleep @initial_sleep_delay
472
+ print "#{ui.color(".", :magenta)}"
473
+ retry
474
+ rescue Errno::EHOSTUNREACH
475
+ sleep @initial_sleep_delay
476
+ print "#{ui.color(".", :magenta)}"
477
+ retry
478
+ rescue Errno::ENETUNREACH
479
+ sleep @initial_sleep_delay
480
+ print "#{ui.color(".", :magenta)}"
481
+ retry
482
+ rescue Net::SSH::Disconnect
483
+ sleep @initial_sleep_delay
484
+ print "#{ui.color(".", :magenta)}"
485
+ retry
486
+ rescue Net::SSH::AuthenticationFailed
487
+ sleep @initial_sleep_delay
488
+ print "#{ui.color(".", :magenta)}"
489
+ retry
490
+ rescue
491
+ puts caller
492
+ puts $!.inspect
493
+ end
494
+
495
+ Chef::Log.debug("#{@server}")
496
+
497
+ puts "\n"
498
+ puts "#{ui.color("Instance Name", :green)}: #{server_display_name}"
499
+ puts "#{ui.color("Instance ID", :green)}: #{server_id}"
500
+ puts "#{ui.color("Service Offering", :green)}: #{server_serviceoffering}"
501
+ puts "#{ui.color("Template", :green)}: #{server_template}"
502
+ puts "#{ui.color("Public IP Address", :green)}: #{@primary_ip}"
503
+ puts "#{ui.color("Port", :green)}: #{@sshport}"
504
+ puts "#{ui.color("User", :green)}: #{ssh_user}"
505
+ puts "#{ui.color("Password", :green)}: #{config[:ssh_password]}"
506
+ puts "#{ui.color("Environment", :green)}: #{config[:environment] || '_default'}"
507
+ puts "#{ui.color("Run List", :green)}: #{config[:run_list].join(', ')}"
508
+ end
509
+
510
+ end
511
+ end
512
+ end
513
+ end