vmc 0.0.8 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/LICENSE +8 -3
  2. data/README.md +83 -0
  3. data/Rakefile +11 -65
  4. data/bin/vmc +3 -2
  5. data/lib/cli/commands/admin.rb +57 -0
  6. data/lib/cli/commands/apps.rb +828 -0
  7. data/lib/cli/commands/base.rb +56 -0
  8. data/lib/cli/commands/misc.rb +99 -0
  9. data/lib/cli/commands/services.rb +84 -0
  10. data/lib/cli/commands/user.rb +60 -0
  11. data/lib/cli/config.rb +109 -0
  12. data/lib/cli/core_ext.rb +119 -0
  13. data/lib/cli/errors.rb +19 -0
  14. data/lib/cli/frameworks.rb +97 -0
  15. data/lib/cli/runner.rb +437 -0
  16. data/lib/cli/services_helper.rb +74 -0
  17. data/lib/cli/usage.rb +94 -0
  18. data/lib/cli/version.rb +5 -0
  19. data/lib/cli/zip_util.rb +61 -0
  20. data/lib/cli.rb +30 -0
  21. data/lib/vmc/client.rb +415 -0
  22. data/lib/vmc/const.rb +19 -0
  23. data/lib/vmc.rb +2 -1589
  24. data/spec/assets/app_info.txt +9 -0
  25. data/spec/assets/app_listings.txt +9 -0
  26. data/spec/assets/bad_create_app.txt +9 -0
  27. data/spec/assets/delete_app.txt +9 -0
  28. data/spec/assets/global_service_listings.txt +9 -0
  29. data/spec/assets/good_create_app.txt +9 -0
  30. data/spec/assets/good_create_service.txt +9 -0
  31. data/spec/assets/info_authenticated.txt +27 -0
  32. data/spec/assets/info_return.txt +15 -0
  33. data/spec/assets/info_return_bad.txt +16 -0
  34. data/spec/assets/login_fail.txt +9 -0
  35. data/spec/assets/login_success.txt +9 -0
  36. data/spec/assets/sample_token.txt +1 -0
  37. data/spec/assets/service_already_exists.txt +9 -0
  38. data/spec/assets/service_listings.txt +9 -0
  39. data/spec/assets/service_not_found.txt +9 -0
  40. data/spec/assets/user_info.txt +9 -0
  41. data/spec/spec_helper.rb +11 -0
  42. data/spec/unit/cli_opts_spec.rb +73 -0
  43. data/spec/unit/client_spec.rb +284 -0
  44. metadata +114 -71
  45. data/README +0 -58
  46. data/lib/parse.rb +0 -719
  47. data/lib/vmc_base.rb +0 -205
  48. data/vendor/gems/httpclient/VERSION +0 -1
  49. data/vendor/gems/httpclient/lib/http-access2/cookie.rb +0 -1
  50. data/vendor/gems/httpclient/lib/http-access2/http.rb +0 -1
  51. data/vendor/gems/httpclient/lib/http-access2.rb +0 -53
  52. data/vendor/gems/httpclient/lib/httpclient/auth.rb +0 -522
  53. data/vendor/gems/httpclient/lib/httpclient/cacert.p7s +0 -1579
  54. data/vendor/gems/httpclient/lib/httpclient/cacert_sha1.p7s +0 -1579
  55. data/vendor/gems/httpclient/lib/httpclient/connection.rb +0 -84
  56. data/vendor/gems/httpclient/lib/httpclient/cookie.rb +0 -562
  57. data/vendor/gems/httpclient/lib/httpclient/http.rb +0 -867
  58. data/vendor/gems/httpclient/lib/httpclient/session.rb +0 -864
  59. data/vendor/gems/httpclient/lib/httpclient/ssl_config.rb +0 -417
  60. data/vendor/gems/httpclient/lib/httpclient/timeout.rb +0 -136
  61. data/vendor/gems/httpclient/lib/httpclient/util.rb +0 -86
  62. data/vendor/gems/httpclient/lib/httpclient.rb +0 -1020
  63. data/vendor/gems/httpclient/lib/tags +0 -908
@@ -0,0 +1,828 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'tmpdir'
5
+
6
+ module VMC::Cli::Command
7
+
8
+ class Apps < Base
9
+ include VMC::Cli::ServicesHelper
10
+
11
+ def list
12
+ apps = client.apps
13
+ return display JSON.pretty_generate(apps || []) if @options[:json]
14
+
15
+ display "\n"
16
+ return display "No Applications" if apps.nil? || apps.empty?
17
+
18
+ apps_table = table do |t|
19
+ t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
20
+ apps.each do |app|
21
+ t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
22
+ end
23
+ end
24
+ display apps_table
25
+ end
26
+
27
+ alias :apps :list
28
+
29
+ SLEEP_TIME = 0.5
30
+ LINE_LENGTH = 80
31
+
32
+ # Numerators are in secs
33
+ TICKER_TICKS = 15/SLEEP_TIME
34
+ HEALTH_TICKS = 5/SLEEP_TIME
35
+ TAIL_TICKS = 25/SLEEP_TIME
36
+ GIVEUP_TICKS = 120/SLEEP_TIME
37
+
38
+ def start(appname)
39
+ app = client.app_info(appname)
40
+ return display "Application '#{appname}' could not be found".red if app.nil?
41
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
42
+ app[:state] = 'STARTED'
43
+ client.update_app(appname, app)
44
+
45
+ count = log_lines_displayed = 0
46
+ failed = false
47
+ start_time = Time.now.to_i
48
+
49
+ banner = 'Starting Application: '
50
+ display banner, false
51
+
52
+ loop do
53
+ display '.', false unless count > TICKER_TICKS
54
+ sleep SLEEP_TIME
55
+ begin
56
+ break if app_started_properly(appname, count > HEALTH_TICKS)
57
+ if !crashes(appname, false, start_time).empty?
58
+ # Check for the existance of crashes
59
+ display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
60
+ grab_crash_logs(appname, '0', true)
61
+ if @push
62
+ display "\n"
63
+ should_delete = ask 'Should I delete the application? (Y/n)? ' unless no_prompt
64
+ delete_app(appname, false) unless no_prompt || should_delete.upcase == 'N'
65
+ end
66
+ failed = true
67
+ break
68
+ elsif count > TAIL_TICKS
69
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
70
+ end
71
+ rescue => e
72
+ err(e.message, '')
73
+ end
74
+ count += 1
75
+ if count > GIVEUP_TICKS # 2 minutes
76
+ display "\nApplication is taking too long to start, check your logs".yellow
77
+ break
78
+ end
79
+ end
80
+ exit(false) if failed
81
+ clear(LINE_LENGTH)
82
+ display "#{banner}#{'OK'.green}"
83
+ end
84
+
85
+ def stop(appname)
86
+ app = client.app_info(appname)
87
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
88
+ display 'Stopping Application: ', false
89
+ app[:state] = 'STOPPED'
90
+ client.update_app(appname, app)
91
+ display 'OK'.green
92
+ end
93
+
94
+ def restart(appname)
95
+ stop(appname)
96
+ start(appname)
97
+ end
98
+
99
+ def rename(appname, newname)
100
+ app = client.app_info(appname)
101
+ app[:name] = newname
102
+ display 'Renaming Appliction: '
103
+ client.update_app(newname, app)
104
+ display 'OK'.green
105
+ end
106
+
107
+ def mem(appname, memsize=nil)
108
+ app = client.app_info(appname)
109
+ mem = current_mem = mem_quota_to_choice(app[:resources][:memory])
110
+ memsize = normalize_mem(memsize) if memsize
111
+
112
+ unless memsize
113
+ choose do |menu|
114
+ menu.layout = :one_line
115
+ menu.prompt = "Update Memory Reservation? [Current:#{current_mem}] "
116
+ menu.default = current_mem
117
+ mem_choices.each { |choice| menu.choice(choice) { memsize = choice } }
118
+ end
119
+ end
120
+
121
+ mem = mem_choice_to_quota(mem)
122
+ memsize = mem_choice_to_quota(memsize)
123
+ current_mem = mem_choice_to_quota(current_mem)
124
+
125
+ display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
126
+
127
+ # check memsize here for capacity
128
+ check_has_capacity_for(memsize - mem)
129
+
130
+ mem = memsize
131
+
132
+ if (mem != current_mem)
133
+ app[:resources][:memory] = mem
134
+ client.update_app(appname, app)
135
+ display 'OK'.green
136
+ restart appname if app[:state] == 'STARTED'
137
+ else
138
+ display 'OK'.green
139
+ end
140
+ end
141
+
142
+ def map(appname, url)
143
+ app = client.app_info(appname)
144
+ uris = app[:uris] || []
145
+ uris << url
146
+ app[:uris] = uris
147
+ client.update_app(appname, app)
148
+ display "Succesfully mapped url".green
149
+ end
150
+
151
+ def unmap(appname, url)
152
+ app = client.app_info(appname)
153
+ uris = app[:uris] || []
154
+ url = url.gsub(/^http(s*):\/\//i, '')
155
+ deleted = uris.delete(url)
156
+ err "Invalid url" unless deleted
157
+ app[:uris] = uris
158
+ client.update_app(appname, app)
159
+ display "Succesfully unmapped url".green
160
+
161
+ end
162
+
163
+ def delete(appname=nil, force=false)
164
+ if @options[:all]
165
+ apps = client.apps
166
+ apps.each { |app| delete_app(app[:name], force) }
167
+ else
168
+ err 'No valid appname given' unless appname
169
+ delete_app(appname, force)
170
+ end
171
+ end
172
+
173
+ def delete_app(appname, force)
174
+ app = client.app_info(appname)
175
+ services_to_delete = []
176
+ app_services = app[:services]
177
+ app_services.each { |service|
178
+ del_service = 'Y' if force
179
+ del_service = 'N' if no_prompt
180
+ unless no_prompt || force
181
+ del_service = ask("Provisioned service [#{service}] detected, would you like to delete it? [Yn]: ")
182
+ end
183
+ services_to_delete << service unless del_service.upcase == 'N'
184
+ }
185
+ display "Deleting application [#{appname}]: ", false
186
+ client.delete_app(appname)
187
+ display 'OK'.green
188
+
189
+ services_to_delete.each do |s|
190
+ display "Deleting service [#{s}]: ", false
191
+ client.delete_service(s)
192
+ display 'OK'.green
193
+ end
194
+ end
195
+
196
+ def all_files(appname, path)
197
+ instances_info_envelope = client.app_instances(appname)
198
+ return if instances_info_envelope.is_a?(Array)
199
+ instances_info = instances_info_envelope[:instances] || []
200
+ instances_info.each do |entry|
201
+ content = client.app_files(appname, path, entry[:index])
202
+ display_logfile(path, content, entry[:index], "====> [#{entry[:index]}: #{path}] <====\n".bold)
203
+ end
204
+ end
205
+
206
+ def files(appname, path='/')
207
+ return all_files(appname, path) if @options[:all] && !@options[:instance]
208
+ instance = @options[:instance] || '0'
209
+ content = client.app_files(appname, path, instance)
210
+ display content
211
+ rescue VMC::Client::NotFound => e
212
+ err 'No such file or directory'
213
+ end
214
+
215
+ def logs(appname)
216
+ return grab_all_logs(appname) if @options[:all] && !@options[:instance]
217
+ instance = @options[:instance] || '0'
218
+ grab_logs(appname, instance)
219
+ end
220
+
221
+ def crashes(appname, print_results=true, since=0)
222
+ crashed = client.app_crashes(appname)[:crashes]
223
+ crashed.delete_if { |c| c[:since] < since }
224
+ instance_map = {}
225
+
226
+ # return display JSON.pretty_generate(apps) if @options[:json]
227
+
228
+
229
+ counter = 0
230
+ crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
231
+ crashed_table = table do |t|
232
+ t.headings = 'Name', 'Instance ID', 'Crashed Time'
233
+ crashed.each do |crash|
234
+ name = "#{appname}-#{counter += 1}"
235
+ instance_map[name] = crash[:instance]
236
+ t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
237
+ end
238
+ end
239
+
240
+ VMC::Cli::Config.store_instances(instance_map)
241
+
242
+ if @options[:json]
243
+ return display JSON.pretty_generate(crashed)
244
+ elsif print_results
245
+ display "\n"
246
+ if crashed.empty?
247
+ display "No crashed instances for [#{appname}]" if print_results
248
+ else
249
+ display crashed_table if print_results
250
+ end
251
+ end
252
+
253
+ crashed
254
+ end
255
+
256
+ def crashlogs(appname)
257
+ instance = @options[:instance] || '0'
258
+ grab_crash_logs(appname, instance)
259
+ end
260
+
261
+ def instances(appname, num=nil)
262
+ if (num)
263
+ change_instances(appname, num)
264
+ else
265
+ get_instances(appname)
266
+ end
267
+ end
268
+
269
+ def stats(appname)
270
+ stats = client.app_stats(appname)
271
+ return display JSON.pretty_generate(stats) if @options[:json]
272
+
273
+ stats_table = table do |t|
274
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
275
+ stats.each do |entry|
276
+ index = entry[:instance]
277
+ stat = entry[:stats]
278
+ hp = "#{stat[:host]}:#{stat[:port]}"
279
+ uptime = uptime_string(stat[:uptime])
280
+ usage = stat[:usage]
281
+ if usage
282
+ cpu = usage[:cpu]
283
+ mem = (usage[:mem] * 1024) # mem comes in K's
284
+ disk = usage[:disk]
285
+ end
286
+ mem_quota = stat[:mem_quota]
287
+ disk_quota = stat[:disk_quota]
288
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
289
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
290
+ cpu = cpu ? cpu.to_s : 'NA'
291
+ cpu = "#{cpu}% (#{stat[:cores]})"
292
+ t << [index, cpu, mem, disk, uptime]
293
+ end
294
+ end
295
+ display "\n"
296
+ if stats.empty?
297
+ display "No running instances for [#{appname}]".yellow
298
+ else
299
+ display stats_table
300
+ end
301
+ end
302
+
303
+ def update(appname)
304
+ path = @options[:path] || '.'
305
+ upload_app_bits(appname, path)
306
+ display "Successfully updated Application: '#{appname}'".green
307
+ if @options[:canary]
308
+ display "[--canary] is deprecated and will be removed in a future version".yellow
309
+ end
310
+ app = client.app_info(appname)
311
+ restart appname if app[:state] == 'STARTED'
312
+ end
313
+
314
+ def push(appname=nil)
315
+ instances = @options[:instances] || 1
316
+ exec = @options[:exec] || 'thin start'
317
+ ignore_framework = @options[:noframework]
318
+ no_start = @options[:nostart]
319
+
320
+ path = @options[:path] || '.'
321
+ appname = @options[:name] unless appname
322
+ url = @options[:url]
323
+ mem, memswitch = nil, @options[:mem]
324
+ memswitch = normalize_mem(memswitch) if memswitch
325
+
326
+ # Check app existing upfront if we have appname
327
+ app_checked = false
328
+ if appname
329
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
330
+ app_checked = true
331
+ end
332
+
333
+ # check if we have hit our app limit
334
+ check_app_limit
335
+
336
+ # check memsize here for capacity
337
+ check_has_capacity_for(mem_choice_to_quota(memswitch)) if memswitch
338
+
339
+ unless no_prompt || @options[:path]
340
+ proceed = ask('Would you like to deploy from the current directory? [Yn]: ')
341
+ if proceed.upcase == 'N'
342
+ path = ask('Please enter in the deployment path: ')
343
+ end
344
+ end
345
+
346
+ path = File.expand_path(path)
347
+ check_deploy_directory(path)
348
+
349
+ appname = ask("Application Name: ") unless no_prompt || appname
350
+ err "Application Name required." if appname.nil? || appname.empty?
351
+
352
+ unless app_checked
353
+ err "Application '#{appname}' already exists, use update or delete." if app_exists?(appname)
354
+ end
355
+
356
+ url = ask("Application Deployed URL: '#{appname}.#{VMC::Cli::Config.suggest_url}'? ") unless no_prompt || url
357
+ url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if url.nil? || url.empty?
358
+
359
+ # Detect the appropriate framework.
360
+ framework = nil
361
+ unless ignore_framework
362
+ framework = VMC::Cli::Framework.detect(path)
363
+ framework_correct = ask("Detected a #{framework}, is this correct? [Yn]: ") if prompt_ok && framework
364
+ framework_correct ||= 'y'
365
+ if prompt_ok && (framework.nil? || framework_correct.upcase == 'N')
366
+ display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
367
+ choose do |menu|
368
+ menu.layout = :one_line
369
+ menu.prompt = "Select Application Type: "
370
+ menu.default = framework
371
+ VMC::Cli::Framework.known_frameworks.each do |f|
372
+ menu.choice(f) { framework = VMC::Cli::Framework.lookup(f) }
373
+ end
374
+ end
375
+ display "Selected #{framework}"
376
+ end
377
+ # Framework override, deprecated
378
+ exec = framework.exec if framework && framework.exec
379
+ else
380
+ framework = VMC::Cli::Framework.new
381
+ end
382
+
383
+ err "Application Type undetermined for path '#{path}'" unless framework
384
+ unless memswitch
385
+ mem = framework.memory
386
+ if prompt_ok
387
+ choose do |menu|
388
+ menu.layout = :one_line
389
+ menu.prompt = "Memory Reservation [Default:#{mem}] "
390
+ menu.default = mem
391
+ mem_choices.each { |choice| menu.choice(choice) { mem = choice } }
392
+ end
393
+ end
394
+ else
395
+ mem = memswitch
396
+ end
397
+
398
+ # Set to MB number
399
+ mem_quota = mem_choice_to_quota(mem)
400
+
401
+ # check memsize here for capacity
402
+ check_has_capacity_for(mem_quota)
403
+
404
+ display 'Creating Application: ', false
405
+
406
+ manifest = {
407
+ :name => "#{appname}",
408
+ :staging => {
409
+ :model => framework.name,
410
+ :stack => exec
411
+ },
412
+ :uris => [url],
413
+ :instances => instances,
414
+ :resources => {
415
+ :memory => mem_quota
416
+ },
417
+ }
418
+
419
+ # Send the manifest to the cloud controller
420
+ client.create_app(appname, manifest)
421
+ display 'OK'.green
422
+
423
+ # Services check
424
+ services = client.services_info
425
+ unless no_prompt || @options[:noservices] || services.empty?
426
+ proceed = ask("Would you like to bind any services to '#{appname}'? [yN]: ")
427
+ bind_services(appname, services) if proceed.upcase == 'Y'
428
+ end
429
+
430
+ # Stage and upload the app bits.
431
+ upload_app_bits(appname, path)
432
+
433
+ @push = true
434
+ start(appname) unless no_start
435
+ end
436
+
437
+ private
438
+
439
+ def app_exists?(appname)
440
+ app_info = client.app_info(appname)
441
+ app_info != nil
442
+ rescue
443
+ false
444
+ end
445
+
446
+ def check_deploy_directory(path)
447
+ err 'Deployment path does not exist' unless File.exists? path
448
+ err 'Deployment path is not a directory' unless File.directory? path
449
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
450
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
451
+ end
452
+
453
+ def upload_app_bits(appname, path)
454
+ display 'Uploading Application:'
455
+
456
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
457
+ FileUtils.rm_f(upload_file)
458
+
459
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
460
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
461
+
462
+ Dir.chdir(path) do
463
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
464
+ if war_file = Dir.glob('*.war').first
465
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
466
+ else
467
+ FileUtils.mkdir(explode_dir)
468
+ files = Dir.glob('{*,.[^\.]*}')
469
+ FileUtils.cp_r(files, explode_dir)
470
+ end
471
+
472
+ # Send the resource list to the cloudcontroller, the response will tell us what it already has..
473
+ unless @options[:noresources]
474
+ display ' Checking for available resources: ', false
475
+ fingerprints = []
476
+ total_size = 0
477
+ resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
478
+ resource_files.each do |filename|
479
+ fingerprints << {
480
+ :size => File.size(filename),
481
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
482
+ :fn => filename
483
+ } unless (File.directory?(filename) || !File.exists?(filename))
484
+ total_size += File.size(filename)
485
+ end
486
+
487
+ # Check to see if the resource check is worth the round trip
488
+ if (total_size > (64*1024)) # 64k for now
489
+ # Send resource fingerprints to the cloud controller
490
+ appcloud_resources = client.check_resources(fingerprints)
491
+ end
492
+ display 'OK'.green
493
+
494
+ if appcloud_resources
495
+ display ' Processing resources: ', false
496
+ # We can then delete what we do not need to send.
497
+ appcloud_resources.each do |resource|
498
+ FileUtils.rm_f resource[:fn]
499
+ # adjust filenames sans the explode_dir prefix
500
+ resource[:fn].sub!("#{explode_dir}/", '')
501
+ end
502
+ display 'OK'.green
503
+ end
504
+
505
+ end
506
+
507
+ # Perform Packing of the upload bits here.
508
+ display ' Packing application: ', false
509
+ VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
510
+ display 'OK'.green
511
+
512
+ upload_size = File.size(upload_file);
513
+ if upload_size > 1024*1024
514
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
515
+ elsif upload_size > 0
516
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
517
+ end
518
+ upload_str = " Uploading (#{upload_size}): "
519
+ display upload_str, false
520
+ FileWithPercentOutput.display_str = upload_str
521
+ FileWithPercentOutput.upload_size = File.size(upload_file);
522
+ file = FileWithPercentOutput.open(upload_file, 'rb')
523
+ client.upload_app(appname, file, appcloud_resources)
524
+ display 'Push Status: ', false
525
+ display 'OK'.green
526
+ end
527
+
528
+ ensure
529
+ # Cleanup if we created an exploded directory.
530
+ FileUtils.rm_f(upload_file) if upload_file
531
+ FileUtils.rm_rf(explode_dir) if explode_dir
532
+ end
533
+
534
+ def choose_existing_service(appname, user_services)
535
+ return unless prompt_ok
536
+ selected = false
537
+ choose do |menu|
538
+ menu.header = "The following provisioned services are available:"
539
+ menu.prompt = 'Please select one you wish to provision: '
540
+ menu.select_by = :index_or_name
541
+ user_services.each do |s|
542
+ menu.choice(s[:name]) do
543
+ display "Binding Service: ", false
544
+ client.bind_service(s[:name], appname)
545
+ display 'OK'.green
546
+ selected = true
547
+ end
548
+ end
549
+ end
550
+ selected
551
+ end
552
+
553
+ def choose_new_service(appname, services)
554
+ return unless prompt_ok
555
+ choose do |menu|
556
+ menu.header = "The following system services are available:"
557
+ menu.prompt = 'Please select one you wish to provision: '
558
+ menu.select_by = :index_or_name
559
+ services.each do |service_type, value|
560
+ value.each do |vendor, version|
561
+ menu.choice(vendor) do
562
+ default_name = random_service_name(vendor)
563
+ service_name = ask("Specify the name of the service [#{default_name}]: ")
564
+ service_name = default_name if service_name.empty?
565
+ create_service_banner(vendor, service_name)
566
+ bind_service_banner(service_name, appname)
567
+ end
568
+ end
569
+ end
570
+ end
571
+ end
572
+
573
+ def bind_services(appname, services)
574
+ user_services = client.services
575
+ selected_existing = false
576
+ unless no_prompt || user_services.empty?
577
+ use_existing = ask "Would you like to use an existing provisioned service [yN]? "
578
+ if use_existing.upcase == 'Y'
579
+ selected_existing = choose_existing_service(appname, user_services)
580
+ end
581
+ end
582
+ # Create a new service and bind it here
583
+ unless selected_existing
584
+ choose_new_service(appname, services)
585
+ end
586
+ end
587
+
588
+ def check_app_limit
589
+ usage = client_info[:usage]
590
+ limits = client_info[:limits]
591
+ return unless usage and limits and limits[:apps]
592
+ if limits[:apps] == usage[:apps]
593
+ display "Not enough capacity for operation.".red
594
+ tapps = limits[:apps] || 0
595
+ apps = usage[:apps] || 0
596
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
597
+ end
598
+ end
599
+
600
+ def check_has_capacity_for(mem_wanted)
601
+ usage = client_info[:usage]
602
+ limits = client_info[:limits]
603
+ return unless usage and limits
604
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
605
+ if mem_wanted > available_for_use
606
+ tmem = pretty_size(limits[:memory]*1024*1024)
607
+ mem = pretty_size(usage[:memory]*1024*1024)
608
+ display "Not enough capacity for operation.".yellow
609
+ available = pretty_size(available_for_use * 1024 * 1024)
610
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
611
+ end
612
+ end
613
+
614
+ def mem_choices
615
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
616
+
617
+ return default unless client_info
618
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
619
+
620
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
621
+ check_has_capacity_for(64) if available_for_use < 64
622
+ return ['64M'] if available_for_use < 128
623
+ return ['64M', '128M'] if available_for_use < 256
624
+ return ['64M', '128M', '256M'] if available_for_use < 512
625
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
626
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
627
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
628
+ end
629
+
630
+ def normalize_mem(mem)
631
+ return mem if /K|G|M/i =~ mem
632
+ "#{mem}M"
633
+ end
634
+
635
+ def mem_choice_to_quota(mem_choice)
636
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
637
+ mem_quota
638
+ end
639
+
640
+ def mem_quota_to_choice(mem)
641
+ if mem < 1024
642
+ mem_choice = "#{mem}M"
643
+ else
644
+ mem_choice = "#{(mem/1024).to_i}G"
645
+ end
646
+ mem_choice
647
+ end
648
+
649
+ def get_instances(appname)
650
+ instances_info_envelope = client.app_instances(appname)
651
+ # Empty array is returned if there are no instances running.
652
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
653
+
654
+ instances_info = instances_info_envelope[:instances] || []
655
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
656
+
657
+ return display JSON.pretty_generate(instances_info) if @options[:json]
658
+
659
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
660
+
661
+ instances_table = table do |t|
662
+ t.headings = 'Index', 'State', 'Start Time'
663
+ instances_info.each do |entry|
664
+ t << [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
665
+ end
666
+ end
667
+ display "\n"
668
+ display instances_table
669
+ end
670
+
671
+ def change_instances(appname, instances)
672
+ app = client.app_info(appname)
673
+
674
+ match = instances.match(/([+-])?\d+/)
675
+ err "Invalid number of instances '#{instances}'" unless match
676
+
677
+ instances = instances.to_i
678
+ current_instances = app[:instances]
679
+ new_instances = match.captures[0] ? current_instances + instances : instances
680
+ err "There must be at least 1 instance." if new_instances < 1
681
+
682
+ if current_instances == new_instances
683
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
684
+ return
685
+ end
686
+
687
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
688
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
689
+ app[:instances] = new_instances
690
+ client.update_app(appname, app)
691
+ display 'OK'.green
692
+ end
693
+
694
+ def health(d)
695
+ return 'N/A' unless (d and d[:state])
696
+ return 'STOPPED' if d[:state] == 'STOPPED'
697
+
698
+ healthy_instances = d[:runningInstances]
699
+ expected_instance = d[:instances]
700
+ health = nil
701
+
702
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
703
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
704
+ end
705
+
706
+ return 'RUNNING' if health && health == 1.0
707
+ return "#{(health * 100).round}%" if health
708
+ return 'N/A'
709
+ end
710
+
711
+ def app_started_properly(appname, error_on_health)
712
+ app = client.app_info(appname)
713
+ case health(app)
714
+ when 'N/A'
715
+ # Health manager not running.
716
+ err "\Application '#{appname}'s state is undetermined, not enough information available." if error_on_health
717
+ return false
718
+ when 'RUNNING'
719
+ return true
720
+ else
721
+ return false
722
+ end
723
+ end
724
+
725
+ def display_logfile(path, content, instance='0', banner=nil)
726
+ banner ||= "====> #{path} <====\n\n"
727
+ if content && !content.empty?
728
+ display banner
729
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
730
+ unless prefix
731
+ display content
732
+ else
733
+ lines = content.split("\n")
734
+ lines.each { |line| display "#{prefix} #{line}"}
735
+ end
736
+ display ''
737
+ end
738
+ end
739
+
740
+ def log_file_paths
741
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
742
+ end
743
+
744
+ def grab_all_logs(appname)
745
+ instances_info_envelope = client.app_instances(appname)
746
+ return if instances_info_envelope.is_a?(Array)
747
+ instances_info = instances_info_envelope[:instances] || []
748
+ instances_info.each do |entry|
749
+ grab_logs(appname, entry[:index])
750
+ end
751
+ end
752
+
753
+ def grab_logs(appname, instance)
754
+ log_file_paths.each do |path|
755
+ begin
756
+ content = client.app_files(appname, path, instance)
757
+ rescue
758
+ end
759
+ display_logfile(path, content, instance)
760
+ end
761
+ end
762
+
763
+ def grab_crash_logs(appname, instance, was_staged=false)
764
+ # stage crash info
765
+ crashes(appname, false) unless was_staged
766
+
767
+ instance ||= '0'
768
+ map = VMC::Cli::Config.instances
769
+ instance = map[instance] if map[instance]
770
+
771
+ ['/logs/err.log', 'logs/stderr.log', 'logs/stdout.log', 'logs/startup.log'].each do |path|
772
+ begin
773
+ content = client.app_files(appname, path, instance)
774
+ rescue
775
+ end
776
+ display_logfile(path, content, instance)
777
+ end
778
+ end
779
+
780
+ def grab_startup_tail(appname, since = 0)
781
+ new_lines = 0
782
+ path = "logs/startup.log"
783
+ content = client.app_files(appname, path)
784
+ if content && !content.empty?
785
+ display "\n==== displaying startup log ====\n\n" if since == 0
786
+ response_lines = content.split("\n")
787
+ lines = response_lines.size
788
+ tail = response_lines[since, lines] || []
789
+ new_lines = tail.size
790
+ display tail.join("\n") if new_lines > 0
791
+ end
792
+ since + new_lines
793
+ end
794
+ rescue
795
+ end
796
+
797
+ class FileWithPercentOutput < ::File
798
+ class << self
799
+ attr_accessor :display_str, :upload_size
800
+ end
801
+
802
+ def update_display(rsize)
803
+ @read ||= 0
804
+ @read += rsize
805
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
806
+ unless VMC::Cli::Config.output.nil?
807
+ clear(FileWithPercentOutput.display_str.size + 5)
808
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
809
+ VMC::Cli::Config.output.flush
810
+ end
811
+ end
812
+
813
+ def read(*args)
814
+ result = super(*args)
815
+ if result && result.size > 0
816
+ update_display(result.size)
817
+ else
818
+ unless VMC::Cli::Config.output.nil?
819
+ clear(FileWithPercentOutput.display_str.size + 5)
820
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
821
+ display('OK'.green)
822
+ end
823
+ end
824
+ result
825
+ end
826
+ end
827
+
828
+ end