vmc 0.0.8 → 0.2.4

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.
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