cf 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2012, Alex Suraci
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Alex Suraci nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/cf ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: ft=ruby
3
+
4
+ require "rubygems"
5
+ require "thor"
6
+
7
+ require "cf"
8
+ require "cf/plugin"
9
+
10
+ CF::Plugin.load_all
11
+
12
+ $exit_status = 0
13
+ CF::CLI.start(ARGV)
14
+ exit($exit_status)
@@ -0,0 +1,2 @@
1
+ require "cf/version"
2
+ require "cf/cli"
@@ -0,0 +1,249 @@
1
+ require "cf/cli/command"
2
+ require "cf/cli/app"
3
+ require "cf/cli/service"
4
+ require "cf/cli/user"
5
+
6
+ module CF
7
+ class CLI < App # subclass App since we operate on Apps by default
8
+ class_option :verbose,
9
+ :type => :boolean, :aliases => "-v", :desc => "Verbose"
10
+
11
+ class_option :force,
12
+ :type => :boolean, :aliases => "-f", :desc => "Force (no interaction)"
13
+
14
+ class_option :simple_output,
15
+ :type => :boolean, :desc => "Simplified output format."
16
+
17
+ class_option :script, :type => :boolean, :aliases => "-s",
18
+ :desc => "--simple-output and --force"
19
+
20
+ class_option :color, :type => :boolean, :desc => "Colored output"
21
+
22
+ desc "service SUBCOMMAND ...ARGS", "Manage your services"
23
+ subcommand "service", Service
24
+
25
+ desc "user SUBCOMMAND ...ARGS", "User management"
26
+ subcommand "user", User
27
+
28
+ desc "info", "Display information on the current target, user, et."
29
+ flag(:runtimes)
30
+ flag(:services)
31
+ def info
32
+ info =
33
+ with_progress("Getting target information") do
34
+ client.info
35
+ end
36
+
37
+ if input(:runtimes)
38
+ runtimes = {}
39
+ info["frameworks"].each do |_, f|
40
+ f["runtimes"].each do |r|
41
+ runtimes[r["name"]] = r
42
+ end
43
+ end
44
+
45
+ runtimes = runtimes.values.sort_by { |x| x["name"] }
46
+
47
+ if simple_output?
48
+ runtimes.each do |r|
49
+ puts r["name"]
50
+ end
51
+ return
52
+ end
53
+
54
+ runtimes.each do |r|
55
+ puts ""
56
+ puts "#{c(r["name"], :blue)}:"
57
+ puts " version: #{b(r["version"])}"
58
+ puts " description: #{b(r["description"])}"
59
+ end
60
+
61
+ return
62
+ end
63
+
64
+ if input(:services)
65
+ services = {}
66
+ client.system_services.each do |_, svcs|
67
+ svcs.each do |name, versions|
68
+ services[name] = versions.values
69
+ end
70
+ end
71
+
72
+ if simple_output?
73
+ services.each do |name, _|
74
+ puts name
75
+ end
76
+
77
+ return
78
+ end
79
+
80
+ services.each do |name, versions|
81
+ puts ""
82
+ puts "#{c(name, :blue)}:"
83
+ puts " versions: #{versions.collect { |v| v["version"] }.join ", "}"
84
+ puts " description: #{versions[0]["description"]}"
85
+ puts " type: #{versions[0]["type"]}"
86
+ end
87
+
88
+ return
89
+ end
90
+
91
+ puts ""
92
+
93
+ puts info["description"]
94
+ puts ""
95
+ puts "target: #{b(client.target)}"
96
+ puts " version: #{info["version"]}"
97
+ puts " support: #{info["support"]}"
98
+ puts ""
99
+ puts "user: #{b(info["user"])}"
100
+ puts " usage:"
101
+
102
+ limits = info["limits"]
103
+ info["usage"].each do |k, v|
104
+ m = limits[k]
105
+ if k == "memory"
106
+ puts " #{k}: #{usage(v * 1024 * 1024, m * 1024 * 1024)}"
107
+ else
108
+ puts " #{k}: #{b(v)} of #{b(m)} limit"
109
+ end
110
+ end
111
+ end
112
+
113
+ desc "target URL", "Set target cloud"
114
+ def target(url)
115
+ target = sane_target_url(url)
116
+ display = c(target.sub(/https?:\/\//, ""), :blue)
117
+ with_progress("Setting target to #{display}") do
118
+ unless force?
119
+ # check that the target is valid
120
+ Conduit::Client.new(target).info
121
+ end
122
+
123
+ set_target(target)
124
+ end
125
+ end
126
+
127
+ desc "login [EMAIL]", "Authenticate with the target"
128
+ flag(:email) {
129
+ ask("Email")
130
+ }
131
+ flag(:password)
132
+ # TODO: implement new authentication scheme
133
+ def login(email = nil)
134
+ unless simple_output?
135
+ puts "Target: #{c(client_target, :blue)}"
136
+ puts ""
137
+ end
138
+
139
+ email ||= input(:email)
140
+ password = input(:password)
141
+
142
+ authenticated = false
143
+ failed = false
144
+ until authenticated
145
+ unless force?
146
+ if failed || !password
147
+ password = ask("Password", :echo => "*", :forget => true)
148
+ end
149
+ end
150
+
151
+ with_progress("Authenticating") do |s|
152
+ begin
153
+ save_token(client.login(email, password))
154
+ authenticated = true
155
+ rescue Conduit::Denied
156
+ return if force?
157
+
158
+ s.fail do
159
+ failed = true
160
+ end
161
+ end
162
+ end
163
+ end
164
+ ensure
165
+ $exit_status = 1 if not authenticated
166
+ end
167
+
168
+ desc "logout", "Log out from the target"
169
+ def logout
170
+ with_progress("Logging out") do
171
+ remove_token
172
+ end
173
+ end
174
+
175
+ desc "register [EMAIL]", "Create a user and log in"
176
+ flag(:email) {
177
+ ask("Email")
178
+ }
179
+ flag(:password) {
180
+ ask("Password", :echo => "*", :forget => true)
181
+ }
182
+ flag(:no_login, :type => :boolean)
183
+ def register(email = nil)
184
+ unless simple_output?
185
+ puts "Target: #{c(client_target, :blue)}"
186
+ puts ""
187
+ end
188
+
189
+ email ||= input(:email)
190
+ password = input(:password)
191
+
192
+ with_progress("Creating user") do
193
+ client.register(email, password)
194
+ end
195
+
196
+ unless input(:skip_login)
197
+ with_progress("Logging in") do
198
+ save_token(client.login(email, password))
199
+ end
200
+ end
201
+ end
202
+
203
+ desc "services", "List your services"
204
+ def services
205
+ services =
206
+ with_progress("Getting services") do
207
+ client.services
208
+ end
209
+
210
+ puts "" unless simple_output?
211
+
212
+ services.each do |s|
213
+ display_service(s)
214
+ end
215
+ end
216
+
217
+ desc "users", "List all users"
218
+ def users
219
+ users =
220
+ with_progress("Getting users") do
221
+ client.users
222
+ end
223
+
224
+ users.each do |u|
225
+ display_user(u)
226
+ end
227
+ end
228
+
229
+ private
230
+
231
+ def display_service(s)
232
+ if simple_output?
233
+ puts s.name
234
+ else
235
+ puts "#{c(s.name, :blue)}: #{s.vendor} v#{s.version}"
236
+ end
237
+ end
238
+
239
+ def display_user(u)
240
+ if simple_output?
241
+ puts u.email
242
+ else
243
+ puts ""
244
+ puts "#{c(u.email, :blue)}:"
245
+ puts " admin?: #{c(u.admin?, u.admin? ? :green : :red)}"
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,545 @@
1
+ require "cf/cli/command"
2
+ require "cf/detect"
3
+
4
+ module CF
5
+ class App < Command
6
+ MEM_CHOICES = ["64M", "128M", "256M", "512M"]
7
+
8
+ desc "apps", "List your applications"
9
+ def apps
10
+ apps =
11
+ with_progress("Getting applications") do
12
+ client.apps
13
+ end
14
+
15
+ apps.each.with_index do |a, num|
16
+ display_app(a)
17
+ end
18
+ end
19
+
20
+ desc "stop [APP]", "Stop an application"
21
+ def stop(name)
22
+ with_progress("Stopping #{c(name, :blue)}") do |s|
23
+ app = client.app(name)
24
+
25
+ unless app.exists?
26
+ s.fail do
27
+ err "Unknown application."
28
+ end
29
+ end
30
+
31
+ if app.stopped?
32
+ s.skip do
33
+ err "Application is not running."
34
+ end
35
+ end
36
+
37
+ app.stop!
38
+ end
39
+ end
40
+
41
+ desc "start [APP]", "Start an application"
42
+ flag(:debug_mode)
43
+ def start(name)
44
+ app = client.app(name)
45
+
46
+ unless app.exists?
47
+ err "Unknown application."
48
+ return
49
+ end
50
+
51
+ switch_mode(app, input(:debug_mode))
52
+
53
+ with_progress("Starting #{c(name, :blue)}") do |s|
54
+ if app.running?
55
+ s.skip do
56
+ err "Already started."
57
+ return
58
+ end
59
+ end
60
+
61
+ app.start!
62
+ end
63
+
64
+ check_application(app)
65
+
66
+ if app.debug_mode && !simple_output?
67
+ puts ""
68
+ instances(name)
69
+ end
70
+ end
71
+
72
+ desc "restart [APP]", "Stop and start an application"
73
+ flag(:debug_mode)
74
+ def restart(name)
75
+ stop(name)
76
+ start(name)
77
+ end
78
+
79
+ desc "delete [APP]", "Delete an application"
80
+ flag(:really) { |name|
81
+ force? || ask("Really delete #{c(name, :blue)}?", :default => false)
82
+ }
83
+ def delete(name)
84
+ return unless input(:really, name)
85
+
86
+ with_progress("Deleting #{c(name, :blue)}") do
87
+ client.app(name).delete!
88
+ end
89
+ ensure
90
+ forget(:really)
91
+ end
92
+
93
+ desc "instances [APP]", "List an app's instances"
94
+ def instances(name)
95
+ instances =
96
+ with_progress("Getting instances for #{c(name, :blue)}") do
97
+ client.app(name).instances
98
+ end
99
+
100
+ instances.each do |i|
101
+ if simple_output?
102
+ puts i.index
103
+ else
104
+ puts ""
105
+ display_instance(i)
106
+ end
107
+ end
108
+ end
109
+
110
+ desc "files [APP] [PATH]", "Examine an app's files"
111
+ def files(name, path = "/")
112
+ files =
113
+ with_progress("Getting file listing") do
114
+ client.app(name).files(*path.split("/"))
115
+ end
116
+
117
+ puts "" unless simple_output?
118
+
119
+ files.each do |file|
120
+ puts file
121
+ end
122
+ end
123
+
124
+ desc "file [APP] [PATH]", "Print out an app's file contents"
125
+ def file(name, path = "/")
126
+ file =
127
+ with_progress("Getting file contents") do
128
+ client.app(name).file(*path.split("/"))
129
+ end
130
+
131
+ puts "" unless simple_output?
132
+
133
+ print file
134
+ end
135
+
136
+ desc "logs [APP]", "Print out an app's logs"
137
+ flag(:instance, :type => :numeric, :default => 0)
138
+ def logs(name)
139
+ app = client.app(name)
140
+ unless app.exists?
141
+ err "Unknown application."
142
+ return
143
+ end
144
+
145
+ instances =
146
+ if input(:instance) == "all"
147
+ app.instances
148
+ else
149
+ app.instances.select { |i| i.index == input(:instance) }
150
+ end
151
+
152
+ if instances.empty?
153
+ if input(:instance) == "all"
154
+ err "No instances found."
155
+ else
156
+ err "Instance #{name} \##{input(:instance)} not found."
157
+ end
158
+
159
+ return
160
+ end
161
+
162
+ instances.each do |i|
163
+ logs =
164
+ with_progress(
165
+ "Getting logs for " +
166
+ c(name, :blue) + " " +
167
+ c("\##{i.index}", :yellow)) do
168
+ i.files("logs")
169
+ end
170
+
171
+ puts "" unless simple_output?
172
+
173
+ logs.each do |log|
174
+ body =
175
+ with_progress("Reading " + b(log.join("/"))) do
176
+ i.file(*log)
177
+ end
178
+
179
+ puts body
180
+ puts "" unless body.empty?
181
+ end
182
+ end
183
+ end
184
+
185
+ desc "push [NAME]", "Push an application, syncing changes if it exists"
186
+ flag(:name) { ask("Name") }
187
+ flag(:path) {
188
+ ask("Push from...", :default => ".")
189
+ }
190
+ flag(:url) { |name, target|
191
+ ask("URL", :default => "#{name}.#{target}")
192
+ }
193
+ flag(:memory) {
194
+ ask("Memory Limit",
195
+ :choices => MEM_CHOICES,
196
+
197
+ # TODO: base this on framework choice
198
+ :default => "64M")
199
+ }
200
+ flag(:instances) {
201
+ ask("Instances", :default => 1)
202
+ }
203
+ flag(:framework) { |choices, default|
204
+ ask("Framework", :choices => choices, :default => default)
205
+ }
206
+ flag(:runtime) { |choices|
207
+ ask("Runtime", :choices => choices)
208
+ }
209
+ flag(:start, :default => true)
210
+ flag(:restart, :default => true)
211
+ def push(name = nil)
212
+ path = File.expand_path(input(:path))
213
+
214
+ name ||= input(:name)
215
+
216
+ detector = Detector.new(client, path)
217
+ frameworks = detector.all_frameworks
218
+ detected, default = detector.frameworks
219
+
220
+ app = client.app(name)
221
+
222
+ if app.exists?
223
+ upload_app(app, path)
224
+ restart(app.name) if input(:restart)
225
+ return
226
+ end
227
+
228
+ app.total_instances = input(:instances)
229
+
230
+ domain = client.target.sub(/^https?:\/\/api\.(.+)\/?/, '\1')
231
+ app.urls = [input(:url, name, domain)]
232
+
233
+ framework = input(:framework, ["other"] + detected.keys, default)
234
+ if framework == "other"
235
+ forget(:framework)
236
+ framework = input(:framework, frameworks.keys)
237
+ end
238
+
239
+ framework_runtimes =
240
+ frameworks[framework]["runtimes"].collect do |k|
241
+ "#{k["name"]} (#{k["description"]})"
242
+ end
243
+
244
+ # TODO: include descriptions
245
+ runtime = input(:runtime, framework_runtimes).split.first
246
+
247
+ app.framework = framework
248
+ app.runtime = runtime
249
+
250
+ app.memory = megabytes(input(:memory))
251
+
252
+ with_progress("Creating #{c(name, :blue)}") do
253
+ app.create!
254
+ end
255
+
256
+ begin
257
+ upload_app(app, path)
258
+ rescue
259
+ err "Upload failed. Try again with 'vmc push'."
260
+ raise
261
+ end
262
+
263
+ start(name) if input(:start)
264
+ end
265
+
266
+ desc "update", "DEPRECATED", :hide => true
267
+ def update(*args)
268
+ err "The 'update' command is no longer used; use 'push' instead."
269
+ end
270
+
271
+ desc "stats", "Display application instance status"
272
+ def stats(name)
273
+ stats =
274
+ with_progress("Getting stats") do
275
+ client.app(name).stats
276
+ end
277
+
278
+ stats.sort_by { |k, _| k }.each do |idx, info|
279
+ stats = info["stats"]
280
+ usage = stats["usage"]
281
+ puts ""
282
+ puts "instance #{c("#" + idx, :blue)}:"
283
+ print " cpu: #{percentage(usage["cpu"])} of"
284
+ puts " #{b(stats["cores"])} cores"
285
+ puts " memory: #{usage(usage["mem"] * 1024, stats["mem_quota"])}"
286
+ puts " disk: #{usage(usage["disk"], stats["disk_quota"])}"
287
+ end
288
+ end
289
+
290
+ desc "scale [APP]", "Update the instances/memory limit for an application"
291
+ flag(:instances, :type => :numeric) { |default|
292
+ ask("Instances", :default => default)
293
+ }
294
+ flag(:memory) { |default|
295
+ ask("Memory Limit",
296
+ :default => human_size(default * 1024 * 1024, 0),
297
+ :choices => MEM_CHOICES)
298
+ }
299
+ def scale(name)
300
+ app = client.app(name)
301
+
302
+ instances = passed_value(:instances)
303
+ memory = passed_value(:memory)
304
+
305
+ unless instances || memory
306
+ instances = input(:instances, app.total_instances)
307
+ memory = input(:memory, app.memory)
308
+ end
309
+
310
+ with_progress("Scaling #{c(name, :blue)}") do
311
+ app.total_instances = instances.to_i if instances
312
+ app.memory = megabytes(memory) if memory
313
+ app.update!
314
+ end
315
+ end
316
+
317
+ desc "map NAME URL", "Add a URL mapping for an app"
318
+ def map(name, url)
319
+ simple = url.sub(/^https?:\/\/(.*)\/?/i, '\1')
320
+
321
+ with_progress("Updating #{c(name, :blue)}") do
322
+ app = client.app(name)
323
+ app.urls << simple
324
+ app.update!
325
+ end
326
+ end
327
+
328
+ desc "unmap NAME URL", "Remove a URL mapping from an app"
329
+ def unmap(name, url)
330
+ simple = url.sub(/^https?:\/\/(.*)\/?/i, '\1')
331
+
332
+ with_progress("Updating #{c(name, :blue)}") do |s|
333
+ app = client.app(name)
334
+
335
+ unless app.urls.delete(simple)
336
+ s.fail do
337
+ err "URL #{url} is not mapped to this application."
338
+ return
339
+ end
340
+ end
341
+
342
+ app.update!
343
+ end
344
+ end
345
+
346
+ class Env < Command
347
+ VALID_NAME = /^[a-zA-Za-z_][[:alnum:]_]*$/
348
+
349
+ desc "set [APP] [NAME] [VALUE]", "Set an environment variable"
350
+ def set(appname, name, value)
351
+ app = client.app(appname)
352
+ unless name =~ VALID_NAME
353
+ err "Invalid variable name; must match #{VALID_NAME.inspect}"
354
+ return
355
+ end
356
+
357
+ unless app.exists?
358
+ err "Unknown application."
359
+ return
360
+ end
361
+
362
+ with_progress("Updating #{c(app.name, :blue)}") do
363
+ app.update!("env" =>
364
+ app.env.reject { |v|
365
+ v.start_with?("#{name}=")
366
+ }.push("#{name}=#{value}"))
367
+ end
368
+ end
369
+
370
+ desc "unset [APP] [NAME]", "Remove an environment variable"
371
+ def unset(appname, name)
372
+ app = client.app(appname)
373
+
374
+ unless app.exists?
375
+ err "Unknown application."
376
+ return
377
+ end
378
+
379
+ with_progress("Updating #{c(app.name, :blue)}") do
380
+ app.update!("env" =>
381
+ app.env.reject { |v|
382
+ v.start_with?("#{name}=")
383
+ })
384
+ end
385
+ end
386
+
387
+ desc "list [APP]", "Show all environment variables set for an app"
388
+ def list(appname)
389
+ vars =
390
+ with_progress("Getting variables") do |s|
391
+ app = client.app(appname)
392
+
393
+ unless app.exists?
394
+ s.fail do
395
+ err "Unknown application."
396
+ return
397
+ end
398
+ end
399
+
400
+ app.env
401
+ end
402
+
403
+ puts "" unless simple_output?
404
+
405
+ vars.each do |pair|
406
+ name, val = pair.split("=", 2)
407
+ puts "#{c(name, :blue)}: #{val}"
408
+ end
409
+ end
410
+ end
411
+
412
+ desc "env SUBCOMMAND ...ARGS", "Manage application environment variables"
413
+ subcommand "env", Env
414
+
415
+ private
416
+
417
+ def upload_app(app, path)
418
+ with_progress("Uploading #{c(app.name, :blue)}") do
419
+ app.upload(path)
420
+ end
421
+ end
422
+
423
+ # set app debug mode, ensuring it's valid, and shutting it down
424
+ def switch_mode(app, mode)
425
+ mode = nil if mode == "none"
426
+
427
+ return false if app.debug_mode == mode
428
+
429
+ if mode.nil?
430
+ with_progress("Removing debug mode") do
431
+ app.debug_mode = nil
432
+ app.stop! if app.running?
433
+ end
434
+
435
+ return true
436
+ end
437
+
438
+ with_progress("Switching mode to #{c(mode, :blue)}") do |s|
439
+ runtimes = client.system_runtimes
440
+ modes = runtimes[app.runtime]["debug_modes"] || []
441
+ if modes.include?(mode)
442
+ app.debug_mode = mode
443
+ app.stop! if app.running?
444
+ true
445
+ else
446
+ s.fail do
447
+ err "Unknown mode '#{mode}'; available: #{modes.inspect}"
448
+ false
449
+ end
450
+ end
451
+ end
452
+ end
453
+
454
+ APP_CHECK_LIMIT = 60
455
+
456
+ def check_application(app)
457
+ with_progress("Checking #{c(app.name, :blue)}") do |s|
458
+ if app.debug_mode == "suspend"
459
+ s.skip do
460
+ puts "Application is in suspended debugging mode."
461
+ puts "It will wait for you to attach to it before starting."
462
+ end
463
+ end
464
+
465
+ seconds = 0
466
+ until app.healthy?
467
+ sleep 1
468
+ seconds += 1
469
+ if seconds == APP_CHECK_LIMIT
470
+ s.give_up do
471
+ err "Application failed to start."
472
+ # TODO: print logs
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end
478
+
479
+ # choose the right color for app/instance state
480
+ def state_color(s)
481
+ case s
482
+ when "STARTING"
483
+ :blue
484
+ when "STARTED", "RUNNING"
485
+ :green
486
+ when "DOWN"
487
+ :red
488
+ when "FLAPPING"
489
+ :magenta
490
+ when "N/A"
491
+ :cyan
492
+ else
493
+ :yellow
494
+ end
495
+ end
496
+
497
+ def display_app(a)
498
+ if simple_output?
499
+ puts a.name
500
+ return
501
+ end
502
+
503
+ puts ""
504
+
505
+ health = a.health
506
+
507
+ if a.debug_mode == "suspend" && health == "0%"
508
+ status = c("suspended", :yellow)
509
+ else
510
+ status = c(health.downcase, state_color(health))
511
+ end
512
+
513
+ print "#{c(a.name, :blue)}: #{status}"
514
+
515
+ unless a.total_instances == 1
516
+ print ", #{b(a.total_instances)} instances"
517
+ end
518
+
519
+ puts ""
520
+
521
+ unless a.urls.empty?
522
+ puts " urls: #{a.urls.collect { |u| b(u) }.join(", ")}"
523
+ end
524
+
525
+ unless a.services.empty?
526
+ puts " services: #{a.services.collect { |s| b(s) }.join(", ")}"
527
+ end
528
+ end
529
+
530
+ def display_instance(i)
531
+ print "instance #{c("\##{i.index}", :blue)}: "
532
+ puts "#{b(c(i.state.downcase, state_color(i.state)))} "
533
+
534
+ puts " started: #{c(i.since.strftime("%F %r"), :cyan)}"
535
+
536
+ if d = i.debugger
537
+ puts " debugger: port #{c(d["port"], :blue)} at #{c(d["ip"], :blue)}"
538
+ end
539
+
540
+ if c = i.console
541
+ puts " console: port #{b(c["port"])} at #{b(c["ip"])}"
542
+ end
543
+ end
544
+ end
545
+ end