knife-sce 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,415 @@
1
+ #
2
+ # Author:: Rad Gruchalski (<radek@gruchalski.com>)
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/sce_base'
19
+
20
+ class Chef
21
+ class Knife
22
+ class SceServerCreate < Knife
23
+
24
+ include Knife::SceBase
25
+
26
+ deps do
27
+ require 'fog'
28
+ require 'readline'
29
+ require 'chef/json_compat'
30
+ require 'chef/knife/bootstrap'
31
+ Chef::Knife::Bootstrap.load_deps
32
+ end
33
+
34
+ banner "knife sce server create (options)"
35
+
36
+ attr_accessor :initial_sleep_delay
37
+ attr_reader :server
38
+
39
+ option :sce_image,
40
+ :short => "-I IMAGE",
41
+ :long => "--image IMAGE",
42
+ :description => "The SCE image ID for the server",
43
+ :proc => Proc.new { |i| Chef::Config[:knife][:sce_image] = i }
44
+
45
+ option :sce_flavor,
46
+ :short => "-f FLAVOR",
47
+ :long => "--flavor FLAVOR",
48
+ :description => "The flavor of server (Copper, Bronze, Gold, Platinum)",
49
+ :proc => Proc.new { |f| Chef::Config[:knife][:sce_flavor] = f }
50
+
51
+ option :datacenter,
52
+ :short => "-Z LOCATION_ID",
53
+ :long => "--data-center LOCATION_ID",
54
+ :description => "Data center location ID, use knife sce location list to learn more about possible locations.",
55
+ :proc => Proc.new { |key| Chef::Config[:knife][:sce_location_id] = key }
56
+
57
+ option :identity_file,
58
+ :short => "-i IDENTITY_FILE",
59
+ :long => "--identity-file IDENTITY_FILE",
60
+ :description => "The SSH identity file used for authentication"
61
+
62
+ option :associate_ip,
63
+ :long => "--associate-ip IP_ADDRESS",
64
+ :description => "Associate existing IP address with instance after launch"
65
+
66
+ option :vlan_id,
67
+ :long => "--vlan-id VLAN_ID",
68
+ :description => "The VLAN to use for the instance",
69
+ :proc => Proc.new { |vlan_id| Chef::Config[:knife][:sce_vlan_id] = vlan_id }
70
+
71
+ option :ssh_key_name,
72
+ :long => "--ssh-key-name SCE_KEY_NAME",
73
+ :description => "The SCE_KEY to use for the instance",
74
+ :proc => Proc.new { |ssh_key_name| Chef::Config[:knife][:sce_key_name] = ssh_key_name}
75
+
76
+ option :is_mini_ephemeral,
77
+ :long => "--is-mini-ephemeral",
78
+ :boolean => true,
79
+ :default => false,
80
+ :description => "No additional storage"
81
+
82
+ option :anti_collocation_instance,
83
+ :long => "--anti-collocation-instance INSTANCE_ID",
84
+ :description => "No additional storage",
85
+ :default => nil
86
+
87
+ option :volume_id,
88
+ :long => "--volume-id VOLUME_ID",
89
+ :description => "Existing persistent volume to attach to the instances at launch",
90
+ :default => nil
91
+
92
+ option :secondary_ip,
93
+ :long => "--secondary-ip IP_ID[,IP_ID,IP_ID]",
94
+ :description => "Add a secondary IP address to this instance (i.e. multi-homed)",
95
+ :default => nil
96
+
97
+ option :chef_node_name,
98
+ :short => "-N NAME",
99
+ :long => "--node-name NAME",
100
+ :description => "The Chef node name for your new node",
101
+ :proc => Proc.new { |key| Chef::Config[:knife][:chef_node_name] = key }
102
+
103
+ option :sce_ssh_user,
104
+ :short => "-x USERNAME",
105
+ :long => "--ssh-user USERNAME",
106
+ :description => "The ssh username",
107
+ :default => "idcuser"
108
+
109
+ option :ssh_password,
110
+ :short => "-P PASSWORD",
111
+ :long => "--ssh-password PASSWORD",
112
+ :description => "The ssh password"
113
+
114
+ option :ssh_port,
115
+ :short => "-p PORT",
116
+ :long => "--ssh-port PORT",
117
+ :description => "The ssh port",
118
+ :default => "22",
119
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
120
+
121
+ option :ssh_gateway,
122
+ :short => "-w GATEWAY",
123
+ :long => "--ssh-gateway GATEWAY",
124
+ :description => "The ssh gateway server",
125
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
126
+
127
+ option :prerelease,
128
+ :long => "--prerelease",
129
+ :description => "Install the pre-release chef gems"
130
+
131
+ option :bootstrap_version,
132
+ :long => "--bootstrap-version VERSION",
133
+ :description => "The version of Chef to install",
134
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
135
+
136
+ option :distro,
137
+ :short => "-d DISTRO",
138
+ :long => "--distro DISTRO",
139
+ :description => "Bootstrap a distro using a template; default is 'chef-full'",
140
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d }
141
+
142
+ option :template_file,
143
+ :long => "--template-file TEMPLATE",
144
+ :description => "Full path to location of template to use",
145
+ :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
146
+ :default => false
147
+
148
+ option :run_list,
149
+ :short => "-r RUN_LIST",
150
+ :long => "--run-list RUN_LIST",
151
+ :description => "Comma separated list of roles/recipes to apply",
152
+ :proc => lambda { |o| o.split(/[\s,]+/) }
153
+
154
+ option :json_attributes,
155
+ :short => "-j JSON",
156
+ :long => "--json-attributes JSON",
157
+ :description => "A JSON string to be added to the first run of chef-client",
158
+ :proc => lambda { |o| JSON.parse(o) }
159
+
160
+ option :host_key_verify,
161
+ :long => "--[no-]host-key-verify",
162
+ :description => "Verify host key, enabled by default.",
163
+ :boolean => true,
164
+ :default => true
165
+
166
+ option :hint,
167
+ :long => "--hint HINT_NAME[=HINT_FILE]",
168
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
169
+ :proc => Proc.new { |h|
170
+ Chef::Config[:knife][:hints] ||= {}
171
+ name, path = h.split("=")
172
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new
173
+ }
174
+
175
+ option :server_connect_attribute,
176
+ :long => "--server-connect-attribute ATTRIBUTE",
177
+ :short => "-a ATTRIBUTE",
178
+ :description => "The EC2 server attribute to use for SSH connection",
179
+ :default => nil
180
+
181
+ option :no_bootstrap,
182
+ :long => "--no-bootstrap",
183
+ :description => "Don't bootstrap the instance, just launch it",
184
+ :default => false
185
+
186
+ def tcp_test_ssh(hostname, ssh_port)
187
+ tcp_socket = TCPSocket.new(hostname, ssh_port)
188
+ readable = IO.select([tcp_socket], nil, nil, 5)
189
+ if readable
190
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
191
+ yield
192
+ true
193
+ else
194
+ false
195
+ end
196
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
197
+ sleep 2
198
+ false
199
+ rescue Errno::EPERM, Errno::ETIMEDOUT
200
+ false
201
+ ensure
202
+ tcp_socket && tcp_socket.close
203
+ end
204
+
205
+ def wait_for_server_active(server)
206
+ server.wait_for { print "."; ready? }
207
+ end
208
+
209
+ def run
210
+
211
+ $stdout.sync = true
212
+
213
+ Fog.timeout = Chef::Config[:knife][:sce_max_timeout] || 6000
214
+
215
+ validate!
216
+
217
+ requested_ip = config[:associate_ip] if config[:associate_ip]
218
+
219
+ # For VPC EIP assignment we need the allocation ID so fetch full EIP details
220
+ # elastic_ip = connection.addresses.detect{|addr| addr if addr.public_ip == requested_elastic_ip}
221
+
222
+ begin
223
+ definition = create_server_def
224
+ # SCE library gives me an excon response object, we have to fetch the server object ourselves:
225
+ excon_response = connection.create_instance(definition[:name], definition[:image_id], definition[:instance_type], definition[:location], definition)
226
+ @server = connection.servers.get(excon_response.data[:body]["instances"][0]["id"])
227
+
228
+ raise "Creating a server failed." if @server.nil?
229
+
230
+ msg_pair("Instance ID", @server.id.to_s)
231
+ msg_pair("Name", @server.name.to_s)
232
+ msg_pair("Flavor", @server.instance_type.to_s)
233
+ msg_pair("Image", @server.image.name.to_s)
234
+ msg_pair("Region", @server.location.location)
235
+ msg_pair("SSH Key", @server.key_name.to_s)
236
+ msg_pair("Owner", @server.owner.to_s)
237
+ msg_pair("Environment", config[:environment] || '_default')
238
+ msg_pair("Run List", (config[:run_list] || []).join(', '))
239
+ msg_pair("JSON Attributes",config[:json_attributes]) unless !config[:json_attributes] || config[:json_attributes].empty?
240
+ msg_pair("VLAN ID", @server.primary_ip["vlan"]["name"].to_s) if @server.primary_ip["vlan"]
241
+ msg_pair("Volume IDs", @server.volume_ids.join(",").to_s) if @server.volume_ids
242
+
243
+ print "\n#{ui.color("Waiting for server", :magenta)}"
244
+ @server.wait_for { print "."; ready? }
245
+
246
+ msg_pair("\nPublic DNS Name", @server.primary_ip["hostname"].to_s)
247
+ msg_pair("Public IP Address", @server.primary_ip["ip"].to_s)
248
+ if @server.secondary_ip
249
+ ips = []
250
+ @server.secondary_ip.each {|item| ips << item['ip']}
251
+ msg_pair("Secondary IP Addresses", ips.join(","))
252
+ end
253
+
254
+ wait_for_sshd(ssh_connect_host)
255
+
256
+ Chef::Config[:knife][:hints] ||= {}
257
+ Chef::Config[:knife][:hints]["sce"] ||= {}
258
+ Chef::Config[:knife][:hints]["sce"].merge!({
259
+ 'server_id' => @server.id.to_s,
260
+ 'region' => @server.location.location,
261
+ 'flavor' => @server.instance_type.to_s,
262
+ 'image' => @server.image.name.to_s
263
+ })
264
+ if @server.primary_ip["vlan"]
265
+ Chef::Config[:knife][:hints]["sce"].merge!({
266
+ 'vlan' => @server.primary_ip["vlan"]["name"].to_s
267
+ })
268
+ end
269
+ if @server.volume_ids
270
+ Chef::Config[:knife][:hints]["sce"].merge!({
271
+ 'volumes' => @server.volume_ids
272
+ })
273
+ end
274
+
275
+ bootstrap_for_node(@server, ssh_connect_host).run unless locate_config_value(:no_bootstrap)
276
+
277
+ rescue Excon::Errors::PreconditionFailed => e
278
+ ui.error e.response.data[:body]
279
+ exit 1
280
+ end
281
+
282
+ end
283
+
284
+ def bootstrap_for_node(server,ssh_host)
285
+
286
+ # Chef::Knife:Ssh is going to use ssh_user setting from knife.rb
287
+ # over the one that we hand to it.
288
+ # To overrule this setting we have to override to Chef knife.rb setting.
289
+ Chef::Config[:knife][:ssh_user] = config[:sce_ssh_user]
290
+
291
+ bootstrap = Chef::Knife::Bootstrap.new
292
+ bootstrap.name_args = [ssh_host]
293
+ bootstrap.config[:run_list] = locate_config_value(:run_list) || []
294
+ bootstrap.config[:ssh_user] = config[:sce_ssh_user]
295
+ bootstrap.config[:ssh_port] = config[:ssh_port]
296
+ bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
297
+ bootstrap.config[:identity_file] = config[:identity_file]
298
+ bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server.id
299
+ bootstrap.config[:prerelease] = config[:prerelease]
300
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
301
+ bootstrap.config[:first_boot_attributes] = locate_config_value(:json_attributes) || {}
302
+ bootstrap.config[:distro] = locate_config_value(:distro) || "chef-full"
303
+ bootstrap.config[:use_sudo] = true unless config[:sce_ssh_user] == 'root'
304
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
305
+ bootstrap.config[:environment] = config[:environment]
306
+ bootstrap.config[:host_key_verify] = config[:host_key_verify]
307
+ bootstrap
308
+ end
309
+
310
+ def vlan_mode?
311
+ # Amazon Virtual Private Cloud requires a subnet_id. If
312
+ # present, do a few things differently
313
+ !!locate_config_value(:vlan_id)
314
+ end
315
+
316
+ def validate!
317
+
318
+ super([:ibm_username, :ibm_password])
319
+
320
+ if locate_config_value(:sce_flavor).nil?
321
+ ui.error("No flavor provided. Use knife sce image describe to list supported flavors for used image.")
322
+ exit 1
323
+ else
324
+
325
+ flavor_found = false
326
+ requested_image = connection.images.get(locate_config_value(:sce_image))
327
+ requested_image.supported_instance_types.each do |sit|
328
+ if sit.id.to_s.eql?( locate_config_value(:sce_flavor) )
329
+ flavor_found = true
330
+ end
331
+ end
332
+ if !flavor_found
333
+ ui.error("Flavor #{config[:sce_flavor]} is not supported for image #{locate_config_value(:sce_image)}. Use knife sce image describe to list supported flavors for used image.")
334
+ exit 1
335
+ end
336
+
337
+ end
338
+
339
+ end
340
+
341
+ def eip_scope
342
+ if vlan_mode?
343
+ "vpc"
344
+ else
345
+ "standard"
346
+ end
347
+ end
348
+
349
+ def create_server_def
350
+ server_def = {
351
+ :name => locate_config_value(:chef_node_name),
352
+ :image_id => locate_config_value(:sce_image),
353
+ :instance_type => locate_config_value(:sce_flavor),
354
+ :location => datacenter_id,
355
+ :key_name => locate_config_value(:sce_key_name)
356
+ }
357
+ if locate_config_value(:sce_vlan_id)
358
+ server_def[:vlan_id] = locate_config_value(:sce_vlan_id)
359
+ end
360
+ if locate_config_value(:secondary_ip)
361
+ server_def[:secondary_ip] = locate_config_value(:secondary_ip)
362
+ end
363
+
364
+ %w{ip is_mini_ephemeral configuration_data anti_collocation_instance volume_id}.each do |parm|
365
+ server_def[parm.to_sym] = locate_config_value(parm.to_sym) if locate_config_value(parm.to_sym)
366
+ end
367
+ server_def
368
+ end
369
+
370
+ def wait_for_sshd(hostname)
371
+ config[:ssh_gateway] ? wait_for_tunnelled_sshd(hostname) : wait_for_direct_sshd(hostname, config[:ssh_port])
372
+ end
373
+
374
+ def wait_for_tunnelled_sshd(hostname)
375
+ print(".")
376
+ print(".") until tunnel_test_ssh(ssh_connect_host) {
377
+ sleep @initial_sleep_delay ||= (vlan_mode? ? 40 : 10)
378
+ puts("done")
379
+ }
380
+ end
381
+
382
+ def tunnel_test_ssh(hostname, &block)
383
+ gw_host, gw_user = config[:ssh_gateway].split('@').reverse
384
+ gw_host, gw_port = gw_host.split(':')
385
+ gateway = Net::SSH::Gateway.new(gw_host, gw_user, :port => gw_port || 22)
386
+ status = false
387
+ gateway.open(hostname, config[:ssh_port]) do |local_tunnel_port|
388
+ status = tcp_test_ssh('localhost', local_tunnel_port, &block)
389
+ end
390
+ status
391
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
392
+ sleep 2
393
+ false
394
+ rescue Errno::EPERM, Errno::ETIMEDOUT
395
+ false
396
+ end
397
+
398
+ def wait_for_direct_sshd(hostname, ssh_port)
399
+ print(".") until tcp_test_ssh(ssh_connect_host, ssh_port) {
400
+ sleep @initial_sleep_delay ||= (vlan_mode? ? 40 : 10)
401
+ puts("done")
402
+ }
403
+ end
404
+
405
+ def ssh_connect_host
406
+ @ssh_connect_host ||= if config[:server_connect_attribute]
407
+ server.send(config[:server_connect_attribute])
408
+ else
409
+ server.ip.to_s
410
+ end
411
+ end
412
+
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,119 @@
1
+ #
2
+ # Author:: Rad Gruchalski (<radek@gruchalski.com>)
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/sce_base'
19
+
20
+ # These two are needed for the '--purge' deletion case
21
+ require 'chef/node'
22
+ require 'chef/api_client'
23
+
24
+ class Chef
25
+ class Knife
26
+ class SceServerDelete < Knife
27
+
28
+ include Knife::SceBase
29
+
30
+ banner "knife sce server delete SERVER [SERVER] (options)"
31
+
32
+ attr_reader :server
33
+
34
+ option :purge,
35
+ :short => "-P",
36
+ :long => "--purge",
37
+ :boolean => true,
38
+ :default => false,
39
+ :description => "Destroy corresponding node and client on the Chef Server, in addition to destroying the SCE node itself. Assumes node and client have the same name as the server (if not, add the '--node-name' option)."
40
+
41
+ option :chef_node_name,
42
+ :short => "-N NAME",
43
+ :long => "--node-name NAME",
44
+ :description => "The name of the node and client to delete, if it differs from the server name. Only has meaning when used with the '--purge' option."
45
+
46
+ # Extracted from Chef::Knife.delete_object, because it has a
47
+ # confirmation step built in... By specifying the '--purge'
48
+ # flag (and also explicitly confirming the server destruction!)
49
+ # the user is already making their intent known. It is not
50
+ # necessary to make them confirm two more times.
51
+ def destroy_item(klass, name, type_name)
52
+ begin
53
+ object = klass.load(name)
54
+ object.destroy
55
+ ui.warn("Deleted #{type_name} #{name}")
56
+ rescue Net::HTTPServerException
57
+ ui.warn("Could not find a #{type_name} named #{name} to delete!")
58
+ end
59
+ end
60
+
61
+ def run
62
+
63
+ validate!
64
+
65
+ @name_args.each do |instance_id|
66
+
67
+ begin
68
+
69
+ @server = connection.servers.get(instance_id)
70
+
71
+ if @server.nil?
72
+ connection.servers.all.each do |s|
73
+ if s.name.to_s == instance_id
74
+ @server = s
75
+ end
76
+ end
77
+ end
78
+
79
+ msg_pair("Instance ID", @server.id.to_s)
80
+ msg_pair("Name", @server.name.to_s)
81
+ msg_pair("Flavor", @server.instance_type.to_s)
82
+ msg_pair("Image", @server.image_id.to_s)
83
+ msg_pair("Region", connection.locations.get(@server.location_id.to_i).name.to_s)
84
+ msg_pair("SSH Key", @server.key_name.to_s)
85
+ msg_pair("Public DNS Name", @server.primary_ip["hostname"].to_s)
86
+ msg_pair("Public IP Address", @server.primary_ip["ip"].to_s)
87
+ msg_pair("Expires at", @server.expires_at.to_s)
88
+
89
+ puts "\n"
90
+ confirm("Do you really want to delete this server")
91
+
92
+ begin
93
+ @server.destroy
94
+ rescue Excon::Errors::PreconditionFailed => e
95
+ if e.data[:body].index("Active or Failed").nil?
96
+ ui.error e.data[:body].to_s
97
+ exit 1
98
+ end
99
+ end
100
+
101
+ ui.warn("Deleted server #{@server.id}")
102
+
103
+ if config[:purge]
104
+ thing_to_delete = config[:chef_node_name] || instance_id
105
+ destroy_item(Chef::Node, thing_to_delete, "node")
106
+ destroy_item(Chef::ApiClient, thing_to_delete, "client")
107
+ else
108
+ ui.warn("Corresponding node and client for the #{instance_id} server were not deleted and remain registered with the Chef Server")
109
+ end
110
+
111
+ rescue NoMethodError
112
+ ui.error("Could not locate server '#{instance_id}'. Please verify it was provisioned.")
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,103 @@
1
+ #
2
+ # Author:: Rad Gruchalski (<radek@gruchalski.com>)
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/sce_base'
19
+
20
+ class Chef
21
+ class Knife
22
+ class SceServerList < Knife
23
+
24
+ include Knife::SceBase
25
+
26
+ banner "knife sce server list (options)"
27
+
28
+ option :name,
29
+ :short => "-n",
30
+ :long => "--no-name",
31
+ :boolean => true,
32
+ :default => true,
33
+ :description => "Do not display name tag in output"
34
+
35
+ option :owner,
36
+ :short => "-o",
37
+ :long => "--no-owner",
38
+ :boolean => true,
39
+ :default => true,
40
+ :description => "Do not display owner in output"
41
+
42
+ def run!
43
+ connection.servers.all
44
+ end
45
+
46
+ def run
47
+ $stdout.sync = true
48
+
49
+ validate!
50
+
51
+ server_list = [
52
+ ui.color('Instance ID', :bold),
53
+ if config[:name]
54
+ ui.color("Name", :bold)
55
+ end,
56
+ if config[:owner]
57
+ ui.color("Owner", :bold)
58
+ end,
59
+ ui.color('Public IP', :bold),
60
+ ui.color('Secondary IPs', :bold),
61
+ ui.color('Flavor', :bold),
62
+ ui.color('Image', :bold),
63
+ ui.color('SSH Key', :bold),
64
+ ui.color('Expires', :bold),
65
+ ui.color('Request', :bold),
66
+ ui.color('State', :bold)
67
+
68
+ ].flatten.compact
69
+
70
+ output_column_count = server_list.length
71
+
72
+ servers = run!
73
+
74
+ servers.each do |server|
75
+ server_list << server.id.to_s
76
+ if config[:name]
77
+ server_list << server.name.to_s
78
+ end
79
+ if config[:owner]
80
+ server_list << server.owner.to_s
81
+ end
82
+ server_list << server.primary_ip['hostname'].to_s
83
+ if server.secondary_ip.empty?
84
+ server_list << "n/a"
85
+ else
86
+ ips = []
87
+ server.secondary_ip.each {|sip| ips << sip['ip'] }
88
+ server_list << ips.join(",")
89
+ end
90
+ server_list << server.instance_type.to_s
91
+ server_list << server.image_id.to_s
92
+ server_list << server.key_name.to_s
93
+ server_list << server.expires_at.to_s
94
+ server_list << server.request_id.to_s
95
+ server_list << server.state.to_s
96
+ end
97
+
98
+ puts ui.list(server_list, :uneven_columns_across, output_column_count)
99
+
100
+ end
101
+ end
102
+ end
103
+ end