vmc_virgo 0.0.1.beta

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.
@@ -0,0 +1,1104 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'tempfile'
5
+ require 'tmpdir'
6
+ require 'set'
7
+ require "uuidtools"
8
+ require 'socket'
9
+
10
+ module VMC::Cli::Command
11
+
12
+ class Apps < Base
13
+ include VMC::Cli::ServicesHelper
14
+ include VMC::Cli::ManifestHelper
15
+ include VMC::Cli::TunnelHelper
16
+ include VMC::Cli::ConsoleHelper
17
+
18
+ def list
19
+ apps = client.apps
20
+ apps.sort! {|a, b| a[:name] <=> b[:name] }
21
+ return display JSON.pretty_generate(apps || []) if @options[:json]
22
+
23
+ display "\n"
24
+ return display "No Applications" if apps.nil? || apps.empty?
25
+
26
+ apps_table = table do |t|
27
+ t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
28
+ apps.each do |app|
29
+ t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
30
+ end
31
+ end
32
+ display apps_table
33
+ end
34
+
35
+ alias :apps :list
36
+
37
+ SLEEP_TIME = 1
38
+ LINE_LENGTH = 80
39
+
40
+ # Numerators are in secs
41
+ TICKER_TICKS = 25/SLEEP_TIME
42
+ HEALTH_TICKS = 5/SLEEP_TIME
43
+ TAIL_TICKS = 45/SLEEP_TIME
44
+ GIVEUP_TICKS = 120/SLEEP_TIME
45
+
46
+ def info(what, default=nil)
47
+ @options[what] || (@app_info && @app_info[what.to_s]) || default
48
+ end
49
+
50
+ def console(appname, interactive=true)
51
+ unless defined? Caldecott
52
+ display "To use `vmc rails-console', you must first install Caldecott:"
53
+ display ""
54
+ display "\tgem install caldecott"
55
+ display ""
56
+ display "Note that you'll need a C compiler. If you're on OS X, Xcode"
57
+ display "will provide one. If you're on Windows, try DevKit."
58
+ display ""
59
+ display "This manual step will be removed in the future."
60
+ display ""
61
+ err "Caldecott is not installed."
62
+ end
63
+
64
+ #Make sure there is a console we can connect to first
65
+ conn_info = console_connection_info appname
66
+
67
+ port = pick_tunnel_port(@options[:port] || 20000)
68
+
69
+ raise VMC::Client::AuthError unless client.logged_in?
70
+
71
+ if not tunnel_pushed?
72
+ display "Deploying tunnel application '#{tunnel_appname}'."
73
+ auth = UUIDTools::UUID.random_create.to_s
74
+ push_caldecott(auth)
75
+ start_caldecott
76
+ else
77
+ auth = tunnel_auth
78
+ end
79
+
80
+ if not tunnel_healthy?(auth)
81
+ display "Redeploying tunnel application '#{tunnel_appname}'."
82
+ # We don't expect caldecott not to be running, so take the
83
+ # most aggressive restart method.. delete/re-push
84
+ client.delete_app(tunnel_appname)
85
+ invalidate_tunnel_app_info
86
+ push_caldecott(auth)
87
+ start_caldecott
88
+ end
89
+
90
+ start_tunnel(port, conn_info, auth)
91
+ wait_for_tunnel_start(port)
92
+ start_local_console(port, appname) if interactive
93
+ port
94
+ end
95
+
96
+ def start(appname=nil, push=false)
97
+ if appname
98
+ do_start(appname, push)
99
+ else
100
+ each_app do |name|
101
+ do_start(name, push)
102
+ end
103
+ end
104
+ end
105
+
106
+ def stop(appname=nil)
107
+ if appname
108
+ do_stop(appname)
109
+ else
110
+ reversed = []
111
+ each_app do |name|
112
+ reversed.unshift name
113
+ end
114
+
115
+ reversed.each do |name|
116
+ do_stop(name)
117
+ end
118
+ end
119
+ end
120
+
121
+ def restart(appname=nil)
122
+ stop(appname)
123
+ start(appname)
124
+ end
125
+
126
+ def mem(appname, memsize=nil)
127
+ app = client.app_info(appname)
128
+ mem = current_mem = mem_quota_to_choice(app[:resources][:memory])
129
+ memsize = normalize_mem(memsize) if memsize
130
+
131
+ memsize ||= ask(
132
+ "Update Memory Reservation?",
133
+ :default => current_mem,
134
+ :choices => mem_choices
135
+ )
136
+
137
+ mem = mem_choice_to_quota(mem)
138
+ memsize = mem_choice_to_quota(memsize)
139
+ current_mem = mem_choice_to_quota(current_mem)
140
+
141
+ display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
142
+
143
+ # check memsize here for capacity
144
+ check_has_capacity_for((memsize - mem) * app[:instances])
145
+
146
+ mem = memsize
147
+
148
+ if (mem != current_mem)
149
+ app[:resources][:memory] = mem
150
+ client.update_app(appname, app)
151
+ display 'OK'.green
152
+ restart appname if app[:state] == 'STARTED'
153
+ else
154
+ display 'OK'.green
155
+ end
156
+ end
157
+
158
+ def map(appname, url)
159
+ app = client.app_info(appname)
160
+ uris = app[:uris] || []
161
+ uris << url
162
+ app[:uris] = uris
163
+ client.update_app(appname, app)
164
+ display "Successfully mapped url".green
165
+ end
166
+
167
+ def unmap(appname, url)
168
+ app = client.app_info(appname)
169
+ uris = app[:uris] || []
170
+ url = url.gsub(/^http(s*):\/\//i, '')
171
+ deleted = uris.delete(url)
172
+ err "Invalid url" unless deleted
173
+ app[:uris] = uris
174
+ client.update_app(appname, app)
175
+ display "Successfully unmapped url".green
176
+ end
177
+
178
+ def delete(appname=nil)
179
+ force = @options[:force]
180
+ if @options[:all]
181
+ if no_prompt || force || ask("Delete ALL applications?", :default => false)
182
+ apps = client.apps
183
+ apps.each { |app| delete_app(app[:name], force) }
184
+ end
185
+ else
186
+ err 'No valid appname given' unless appname
187
+ delete_app(appname, force)
188
+ end
189
+ end
190
+
191
+ def files(appname, path='/')
192
+ return all_files(appname, path) if @options[:all] && !@options[:instance]
193
+ instance = @options[:instance] || '0'
194
+ content = client.app_files(appname, path, instance)
195
+ display content
196
+ rescue VMC::Client::TargetError
197
+ err 'No such file or directory'
198
+ end
199
+
200
+ def logs(appname)
201
+ # Check if we have an app before progressing further
202
+ client.app_info(appname)
203
+ return grab_all_logs(appname) if @options[:all] && !@options[:instance]
204
+ instance = @options[:instance] || '0'
205
+ grab_logs(appname, instance)
206
+ end
207
+
208
+ def crashes(appname, print_results=true, since=0)
209
+ crashed = client.app_crashes(appname)[:crashes]
210
+ crashed.delete_if { |c| c[:since] < since }
211
+ instance_map = {}
212
+
213
+ # return display JSON.pretty_generate(apps) if @options[:json]
214
+
215
+
216
+ counter = 0
217
+ crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
218
+ crashed_table = table do |t|
219
+ t.headings = 'Name', 'Instance ID', 'Crashed Time'
220
+ crashed.each do |crash|
221
+ name = "#{appname}-#{counter += 1}"
222
+ instance_map[name] = crash[:instance]
223
+ t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
224
+ end
225
+ end
226
+
227
+ VMC::Cli::Config.store_instances(instance_map)
228
+
229
+ if @options[:json]
230
+ return display JSON.pretty_generate(crashed)
231
+ elsif print_results
232
+ display "\n"
233
+ if crashed.empty?
234
+ display "No crashed instances for [#{appname}]" if print_results
235
+ else
236
+ display crashed_table if print_results
237
+ end
238
+ end
239
+
240
+ crashed
241
+ end
242
+
243
+ def crashlogs(appname)
244
+ instance = @options[:instance] || '0'
245
+ grab_crash_logs(appname, instance)
246
+ end
247
+
248
+ def instances(appname, num=nil)
249
+ if num
250
+ change_instances(appname, num)
251
+ else
252
+ get_instances(appname)
253
+ end
254
+ end
255
+
256
+ def stats(appname=nil)
257
+ if appname
258
+ display "\n", false
259
+ do_stats(appname)
260
+ else
261
+ each_app do |n|
262
+ display "\n#{n}:"
263
+ do_stats(n)
264
+ end
265
+ end
266
+ end
267
+
268
+ def update(appname=nil)
269
+ if appname
270
+ app = client.app_info(appname)
271
+ if @options[:canary]
272
+ display "[--canary] is deprecated and will be removed in a future version".yellow
273
+ end
274
+ upload_app_bits(appname, @path)
275
+ restart appname if app[:state] == 'STARTED'
276
+ else
277
+ each_app do |name|
278
+ display "Updating application '#{name}'..."
279
+
280
+ app = client.app_info(name)
281
+ upload_app_bits(name, @application)
282
+ restart name if app[:state] == 'STARTED'
283
+ end
284
+ end
285
+ end
286
+
287
+ def push(appname=nil)
288
+ unless no_prompt || @options[:path]
289
+ proceed = ask(
290
+ 'Would you like to deploy from the current directory?',
291
+ :default => true
292
+ )
293
+
294
+ unless proceed
295
+ @path = ask('Deployment path')
296
+ end
297
+ end
298
+
299
+ pushed = false
300
+ each_app(false) do |name|
301
+ display "Pushing application '#{name}'..." if name
302
+ do_push(name)
303
+ pushed = true
304
+ end
305
+
306
+ unless pushed
307
+ @application = @path
308
+ do_push(appname)
309
+ end
310
+ end
311
+
312
+ def environment(appname)
313
+ app = client.app_info(appname)
314
+ env = app[:env] || []
315
+ return display JSON.pretty_generate(env) if @options[:json]
316
+ return display "No Environment Variables" if env.empty?
317
+ etable = table do |t|
318
+ t.headings = 'Variable', 'Value'
319
+ env.each do |e|
320
+ k,v = e.split('=', 2)
321
+ t << [k, v]
322
+ end
323
+ end
324
+ display "\n"
325
+ display etable
326
+ end
327
+
328
+ def environment_add(appname, k, v=nil)
329
+ app = client.app_info(appname)
330
+ env = app[:env] || []
331
+ k,v = k.split('=', 2) unless v
332
+ env << "#{k}=#{v}"
333
+ display "Adding Environment Variable [#{k}=#{v}]: ", false
334
+ app[:env] = env
335
+ client.update_app(appname, app)
336
+ display 'OK'.green
337
+ restart appname if app[:state] == 'STARTED'
338
+ end
339
+
340
+ def environment_del(appname, variable)
341
+ app = client.app_info(appname)
342
+ env = app[:env] || []
343
+ deleted_env = nil
344
+ env.each do |e|
345
+ k,v = e.split('=')
346
+ if (k == variable)
347
+ deleted_env = e
348
+ break;
349
+ end
350
+ end
351
+ display "Deleting Environment Variable [#{variable}]: ", false
352
+ if deleted_env
353
+ env.delete(deleted_env)
354
+ app[:env] = env
355
+ client.update_app(appname, app)
356
+ display 'OK'.green
357
+ restart appname if app[:state] == 'STARTED'
358
+ else
359
+ display 'OK'.green
360
+ end
361
+ end
362
+
363
+ private
364
+
365
+ def app_exists?(appname)
366
+ app_info = client.app_info(appname)
367
+ app_info != nil
368
+ rescue VMC::Client::NotFound
369
+ false
370
+ end
371
+
372
+ def check_deploy_directory(path)
373
+ err 'Deployment path does not exist' unless File.exists? path
374
+ err 'Deployment path is not a directory' unless File.directory? path
375
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
376
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
377
+ end
378
+
379
+ def check_unreachable_links(path)
380
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
381
+
382
+ pwd = Pathname.pwd
383
+
384
+ abspath = File.expand_path(path)
385
+ unreachable = []
386
+ files.each do |f|
387
+ file = Pathname.new(f)
388
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
389
+ unreachable << file.relative_path_from(pwd)
390
+ end
391
+ end
392
+
393
+ unless unreachable.empty?
394
+ root = Pathname.new(path).relative_path_from(pwd)
395
+ err "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
396
+ end
397
+ end
398
+
399
+ def find_sockets(path)
400
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
401
+ files && files.select { |f| File.socket? f }
402
+ end
403
+
404
+ def upload_app_bits(appname, path)
405
+ display 'Uploading Application:'
406
+
407
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
408
+ FileUtils.rm_f(upload_file)
409
+
410
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
411
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
412
+
413
+ Dir.chdir(path) do
414
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
415
+ if war_file = Dir.glob('*.war').first
416
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
417
+ else
418
+ check_unreachable_links(path)
419
+ FileUtils.mkdir(explode_dir)
420
+
421
+ files = Dir.glob('{*,.[^\.]*}')
422
+
423
+ # Do not process .git files
424
+ files.delete('.git') if files
425
+
426
+ FileUtils.cp_r(files, explode_dir)
427
+
428
+ find_sockets(explode_dir).each do |s|
429
+ File.delete s
430
+ end
431
+ end
432
+
433
+ # Send the resource list to the cloudcontroller, the response will tell us what it already has..
434
+ unless @options[:noresources]
435
+ display ' Checking for available resources: ', false
436
+ fingerprints = []
437
+ total_size = 0
438
+ resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
439
+ resource_files.each do |filename|
440
+ next if (File.directory?(filename) || !File.exists?(filename))
441
+ fingerprints << {
442
+ :size => File.size(filename),
443
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
444
+ :fn => filename
445
+ }
446
+ total_size += File.size(filename)
447
+ end
448
+
449
+ # Check to see if the resource check is worth the round trip
450
+ if (total_size > (64*1024)) # 64k for now
451
+ # Send resource fingerprints to the cloud controller
452
+ appcloud_resources = client.check_resources(fingerprints)
453
+ end
454
+ display 'OK'.green
455
+
456
+ if appcloud_resources
457
+ display ' Processing resources: ', false
458
+ # We can then delete what we do not need to send.
459
+ appcloud_resources.each do |resource|
460
+ FileUtils.rm_f resource[:fn]
461
+ # adjust filenames sans the explode_dir prefix
462
+ resource[:fn].sub!("#{explode_dir}/", '')
463
+ end
464
+ display 'OK'.green
465
+ end
466
+
467
+ end
468
+
469
+ # If no resource needs to be sent, add an empty file to ensure we have
470
+ # a multi-part request that is expected by nginx fronting the CC.
471
+ if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
472
+ Dir.chdir(explode_dir) do
473
+ File.new(".__empty__", "w")
474
+ end
475
+ end
476
+ # Perform Packing of the upload bits here.
477
+ display ' Packing application: ', false
478
+ VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
479
+ display 'OK'.green
480
+
481
+ upload_size = File.size(upload_file);
482
+ if upload_size > 1024*1024
483
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
484
+ elsif upload_size > 0
485
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
486
+ else
487
+ upload_size = '0K'
488
+ end
489
+
490
+ upload_str = " Uploading (#{upload_size}): "
491
+ display upload_str, false
492
+
493
+ FileWithPercentOutput.display_str = upload_str
494
+ FileWithPercentOutput.upload_size = File.size(upload_file);
495
+ file = FileWithPercentOutput.open(upload_file, 'rb')
496
+
497
+ client.upload_app(appname, file, appcloud_resources)
498
+ display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
499
+
500
+ display 'Push Status: ', false
501
+ display 'OK'.green
502
+ end
503
+
504
+ ensure
505
+ # Cleanup if we created an exploded directory.
506
+ FileUtils.rm_f(upload_file) if upload_file
507
+ FileUtils.rm_rf(explode_dir) if explode_dir
508
+ end
509
+
510
+ def check_app_limit
511
+ usage = client_info[:usage]
512
+ limits = client_info[:limits]
513
+ return unless usage and limits and limits[:apps]
514
+ if limits[:apps] == usage[:apps]
515
+ display "Not enough capacity for operation.".red
516
+ tapps = limits[:apps] || 0
517
+ apps = usage[:apps] || 0
518
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
519
+ end
520
+ end
521
+
522
+ def check_has_capacity_for(mem_wanted)
523
+ usage = client_info[:usage]
524
+ limits = client_info[:limits]
525
+ return unless usage and limits
526
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
527
+ if mem_wanted > available_for_use
528
+ tmem = pretty_size(limits[:memory]*1024*1024)
529
+ mem = pretty_size(usage[:memory]*1024*1024)
530
+ display "Not enough capacity for operation.".yellow
531
+ available = pretty_size(available_for_use * 1024 * 1024)
532
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
533
+ end
534
+ end
535
+
536
+ def mem_choices
537
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
538
+
539
+ return default unless client_info
540
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
541
+
542
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
543
+ check_has_capacity_for(64) if available_for_use < 64
544
+ return ['64M'] if available_for_use < 128
545
+ return ['64M', '128M'] if available_for_use < 256
546
+ return ['64M', '128M', '256M'] if available_for_use < 512
547
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
548
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
549
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
550
+ end
551
+
552
+ def normalize_mem(mem)
553
+ return mem if /K|G|M/i =~ mem
554
+ "#{mem}M"
555
+ end
556
+
557
+ def mem_choice_to_quota(mem_choice)
558
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
559
+ mem_quota
560
+ end
561
+
562
+ def mem_quota_to_choice(mem)
563
+ if mem < 1024
564
+ mem_choice = "#{mem}M"
565
+ else
566
+ mem_choice = "#{(mem/1024).to_i}G"
567
+ end
568
+ mem_choice
569
+ end
570
+
571
+ def get_instances(appname)
572
+ instances_info_envelope = client.app_instances(appname)
573
+ # Empty array is returned if there are no instances running.
574
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
575
+
576
+ instances_info = instances_info_envelope[:instances] || []
577
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
578
+
579
+ return display JSON.pretty_generate(instances_info) if @options[:json]
580
+
581
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
582
+
583
+ instances_table = table do |t|
584
+ show_debug = instances_info.any? { |e| e[:debug_port] }
585
+
586
+ headings = ['Index', 'State', 'Start Time']
587
+ headings << 'Debug IP' if show_debug
588
+ headings << 'Debug Port' if show_debug
589
+
590
+ t.headings = headings
591
+
592
+ instances_info.each do |entry|
593
+ row = [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
594
+ row << entry[:debug_ip] if show_debug
595
+ row << entry[:debug_port] if show_debug
596
+ t << row
597
+ end
598
+ end
599
+ display "\n"
600
+ display instances_table
601
+ end
602
+
603
+ def change_instances(appname, instances)
604
+ app = client.app_info(appname)
605
+
606
+ match = instances.match(/([+-])?\d+/)
607
+ err "Invalid number of instances '#{instances}'" unless match
608
+
609
+ instances = instances.to_i
610
+ current_instances = app[:instances]
611
+ new_instances = match.captures[0] ? current_instances + instances : instances
612
+ err "There must be at least 1 instance." if new_instances < 1
613
+
614
+ if current_instances == new_instances
615
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
616
+ return
617
+ end
618
+
619
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
620
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
621
+ app[:instances] = new_instances
622
+ client.update_app(appname, app)
623
+ display 'OK'.green
624
+ end
625
+
626
+ def health(d)
627
+ return 'N/A' unless (d and d[:state])
628
+ return 'STOPPED' if d[:state] == 'STOPPED'
629
+
630
+ healthy_instances = d[:runningInstances]
631
+ expected_instance = d[:instances]
632
+ health = nil
633
+
634
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
635
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
636
+ end
637
+
638
+ return 'RUNNING' if health && health == 1.0
639
+ return "#{(health * 100).round}%" if health
640
+ return 'N/A'
641
+ end
642
+
643
+ def app_started_properly(appname, error_on_health)
644
+ app = client.app_info(appname)
645
+ case health(app)
646
+ when 'N/A'
647
+ # Health manager not running.
648
+ err "\nApplication '#{appname}'s state is undetermined, not enough information available." if error_on_health
649
+ return false
650
+ when 'RUNNING'
651
+ return true
652
+ else
653
+ if app[:meta][:debug] == "suspend"
654
+ display "\nApplication [#{appname}] has started in a mode that is waiting for you to trigger startup."
655
+ return true
656
+ else
657
+ return false
658
+ end
659
+ end
660
+ end
661
+
662
+ def display_logfile(path, content, instance='0', banner=nil)
663
+ banner ||= "====> #{path} <====\n\n"
664
+
665
+ unless content.empty?
666
+ display banner
667
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
668
+ unless prefix
669
+ display content
670
+ else
671
+ lines = content.split("\n")
672
+ lines.each { |line| display "#{prefix} #{line}"}
673
+ end
674
+ display ''
675
+ end
676
+ end
677
+
678
+ def log_file_paths
679
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
680
+ end
681
+
682
+ def grab_all_logs(appname)
683
+ instances_info_envelope = client.app_instances(appname)
684
+ return if instances_info_envelope.is_a?(Array)
685
+ instances_info = instances_info_envelope[:instances] || []
686
+ instances_info.each do |entry|
687
+ grab_logs(appname, entry[:index])
688
+ end
689
+ end
690
+
691
+ def grab_logs(appname, instance)
692
+ log_file_paths.each do |path|
693
+ begin
694
+ content = client.app_files(appname, path, instance)
695
+ display_logfile(path, content, instance)
696
+ rescue VMC::Client::TargetError
697
+ end
698
+ end
699
+ end
700
+
701
+ def grab_crash_logs(appname, instance, was_staged=false)
702
+ # stage crash info
703
+ crashes(appname, false) unless was_staged
704
+
705
+ instance ||= '0'
706
+ map = VMC::Cli::Config.instances
707
+ instance = map[instance] if map[instance]
708
+
709
+ %w{
710
+ /logs/err.log /logs/staging.log /logs/migration.log
711
+ /app/logs/stderr.log /app/logs/stdout.log /app/logs/startup.log
712
+ /app/logs/migration.log
713
+ }.each do |path|
714
+ begin
715
+ content = client.app_files(appname, path, instance)
716
+ display_logfile(path, content, instance)
717
+ rescue VMC::Client::TargetError
718
+ end
719
+ end
720
+ end
721
+
722
+ def grab_startup_tail(appname, since = 0)
723
+ new_lines = 0
724
+ path = "logs/startup.log"
725
+ content = client.app_files(appname, path)
726
+ if content && !content.empty?
727
+ display "\n==== displaying startup log ====\n\n" if since == 0
728
+ response_lines = content.split("\n")
729
+ lines = response_lines.size
730
+ tail = response_lines[since, lines] || []
731
+ new_lines = tail.size
732
+ display tail.join("\n") if new_lines > 0
733
+ end
734
+ since + new_lines
735
+ rescue VMC::Client::TargetError
736
+ 0
737
+ end
738
+
739
+ def provisioned_services_apps_hash
740
+ apps = client.apps
741
+ services_apps_hash = {}
742
+ apps.each {|app|
743
+ app[:services].each { |svc|
744
+ svc_apps = services_apps_hash[svc]
745
+ unless svc_apps
746
+ svc_apps = Set.new
747
+ services_apps_hash[svc] = svc_apps
748
+ end
749
+ svc_apps.add(app[:name])
750
+ } unless app[:services] == nil
751
+ }
752
+ services_apps_hash
753
+ end
754
+
755
+ def delete_app(appname, force)
756
+ app = client.app_info(appname)
757
+ services_to_delete = []
758
+ app_services = app[:services]
759
+ services_apps_hash = provisioned_services_apps_hash
760
+ app_services.each { |service|
761
+ del_service = force && no_prompt
762
+ unless no_prompt || force
763
+ del_service = ask(
764
+ "Provisioned service [#{service}] detected, would you like to delete it?",
765
+ :default => false
766
+ )
767
+
768
+ if del_service
769
+ apps_using_service = services_apps_hash[service].reject!{ |app| app == appname}
770
+ if apps_using_service.size > 0
771
+ del_service = ask(
772
+ "Provisioned service [#{service}] is also used by #{apps_using_service.size == 1 ? "app" : "apps"} #{apps_using_service.entries}, are you sure you want to delete it?",
773
+ :default => false
774
+ )
775
+ end
776
+ end
777
+ end
778
+ services_to_delete << service if del_service
779
+ }
780
+
781
+ display "Deleting application [#{appname}]: ", false
782
+ client.delete_app(appname)
783
+ display 'OK'.green
784
+
785
+ services_to_delete.each do |s|
786
+ delete_service_banner(s)
787
+ end
788
+ end
789
+
790
+ def do_start(appname, push=false)
791
+ app = client.app_info(appname)
792
+ return display "Application '#{appname}' could not be found".red if app.nil?
793
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
794
+
795
+
796
+
797
+ if @options[:debug]
798
+ runtimes = client.runtimes_info
799
+ return display "Cannot get runtime information." unless runtimes
800
+
801
+ runtime = runtimes[app[:staging][:stack].to_sym]
802
+ return display "Unknown runtime." unless runtime
803
+
804
+ unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug]
805
+ modes = runtime[:debug_modes] || []
806
+
807
+ display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode"
808
+
809
+ if push
810
+ display "Try 'vmc start' with one of the following modes: #{modes.inspect}"
811
+ else
812
+ display "Available modes: #{modes.inspect}"
813
+ end
814
+
815
+ return
816
+ end
817
+ end
818
+
819
+ banner = "Staging Application '#{appname}': "
820
+ display banner, false
821
+
822
+ t = Thread.new do
823
+ count = 0
824
+ while count < TAIL_TICKS do
825
+ display '.', false
826
+ sleep SLEEP_TIME
827
+ count += 1
828
+ end
829
+ end
830
+
831
+ app[:state] = 'STARTED'
832
+ app[:debug] = @options[:debug]
833
+ app[:console] = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model]).console
834
+ client.update_app(appname, app)
835
+
836
+ Thread.kill(t)
837
+ clear(LINE_LENGTH)
838
+ display "#{banner}#{'OK'.green}"
839
+
840
+ banner = "Starting Application '#{appname}': "
841
+ display banner, false
842
+
843
+ count = log_lines_displayed = 0
844
+ failed = false
845
+ start_time = Time.now.to_i
846
+
847
+ loop do
848
+ display '.', false unless count > TICKER_TICKS
849
+ sleep SLEEP_TIME
850
+
851
+ break if app_started_properly(appname, count > HEALTH_TICKS)
852
+
853
+ if !crashes(appname, false, start_time).empty?
854
+ # Check for the existance of crashes
855
+ display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
856
+ grab_crash_logs(appname, '0', true)
857
+ if push and !no_prompt
858
+ display "\n"
859
+ delete_app(appname, false) if ask "Delete the application?", :default => true
860
+ end
861
+ failed = true
862
+ break
863
+ elsif count > TAIL_TICKS
864
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
865
+ end
866
+
867
+ count += 1
868
+ if count > GIVEUP_TICKS # 2 minutes
869
+ display "\nApplication is taking too long to start, check your logs".yellow
870
+ break
871
+ end
872
+ end
873
+ exit(false) if failed
874
+ clear(LINE_LENGTH)
875
+ display "#{banner}#{'OK'.green}"
876
+ end
877
+
878
+ def do_stop(appname)
879
+ app = client.app_info(appname)
880
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
881
+ display "Stopping Application '#{appname}': ", false
882
+ app[:state] = 'STOPPED'
883
+ client.update_app(appname, app)
884
+ display 'OK'.green
885
+ end
886
+
887
+ def do_push(appname=nil)
888
+ unless @app_info || no_prompt
889
+ @manifest = { "applications" => { @path => { "name" => appname } } }
890
+
891
+ interact
892
+
893
+ if ask("Would you like to save this configuration?", :default => false)
894
+ save_manifest
895
+ end
896
+
897
+ resolve_manifest(@manifest)
898
+
899
+ @app_info = @manifest["applications"][@path]
900
+ end
901
+
902
+ instances = info(:instances, 1)
903
+ exec = info(:exec, 'thin start')
904
+
905
+ ignore_framework = @options[:noframework]
906
+ no_start = @options[:nostart]
907
+
908
+ appname ||= info(:name)
909
+ url = info(:url) || info(:urls)
910
+ mem, memswitch = nil, info(:mem)
911
+ memswitch = normalize_mem(memswitch) if memswitch
912
+
913
+ # Check app existing upfront if we have appname
914
+ app_checked = false
915
+ if appname
916
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
917
+ app_checked = true
918
+ else
919
+ raise VMC::Client::AuthError unless client.logged_in?
920
+ end
921
+
922
+ # check if we have hit our app limit
923
+ check_app_limit
924
+ # check memsize here for capacity
925
+ if memswitch && !no_start
926
+ check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
927
+ end
928
+
929
+ appname ||= ask("Application Name") unless no_prompt
930
+ err "Application Name required." if appname.nil? || appname.empty?
931
+
932
+ check_deploy_directory(@application)
933
+
934
+ if !app_checked and app_exists?(appname)
935
+ err "Application '#{appname}' already exists, use update or delete."
936
+ end
937
+
938
+ default_url = "#{appname}.#{target_base}"
939
+
940
+ unless no_prompt || url
941
+ url = ask(
942
+ "Application Deployed URL",
943
+ :default => default_url
944
+ )
945
+
946
+ # common error case is for prompted users to answer y or Y or yes or
947
+ # YES to this ask() resulting in an unintended URL of y. Special case
948
+ # this common error
949
+ url = nil if YES_SET.member? url
950
+ end
951
+
952
+ url ||= default_url
953
+
954
+ if ignore_framework
955
+ framework = VMC::Cli::Framework.new
956
+ elsif f = info(:framework)
957
+ info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }]
958
+
959
+ framework = VMC::Cli::Framework.new(f["name"], info)
960
+ exec = framework.exec if framework && framework.exec
961
+ else
962
+ framework = detect_framework(prompt_ok)
963
+ end
964
+
965
+ err "Application Type undetermined for path '#{@application}'" unless framework
966
+
967
+ if memswitch
968
+ mem = memswitch
969
+ elsif prompt_ok
970
+ mem = ask("Memory Reservation",
971
+ :default => framework.memory, :choices => mem_choices)
972
+ else
973
+ mem = framework.memory
974
+ end
975
+
976
+ # Set to MB number
977
+ mem_quota = mem_choice_to_quota(mem)
978
+
979
+ # check memsize here for capacity
980
+ check_has_capacity_for(mem_quota * instances) unless no_start
981
+
982
+ display 'Creating Application: ', false
983
+
984
+ manifest = {
985
+ :name => "#{appname}",
986
+ :staging => {
987
+ :framework => framework.name,
988
+ :runtime => info(:runtime)
989
+ },
990
+ :uris => Array(url),
991
+ :instances => instances,
992
+ :resources => {
993
+ :memory => mem_quota
994
+ },
995
+ }
996
+
997
+ # Send the manifest to the cloud controller
998
+ client.create_app(appname, manifest)
999
+ display 'OK'.green
1000
+
1001
+
1002
+ existing = Set.new(client.services.collect { |s| s[:name] })
1003
+
1004
+ if @app_info && services = @app_info["services"]
1005
+ services.each do |name, info|
1006
+ unless existing.include? name
1007
+ create_service_banner(info["type"], name, true)
1008
+ end
1009
+
1010
+ bind_service_banner(name, appname)
1011
+ end
1012
+ end
1013
+
1014
+ # Stage and upload the app bits.
1015
+ upload_app_bits(appname, @application)
1016
+
1017
+ start(appname, true) unless no_start
1018
+ end
1019
+
1020
+ def do_stats(appname)
1021
+ stats = client.app_stats(appname)
1022
+ return display JSON.pretty_generate(stats) if @options[:json]
1023
+
1024
+ stats_table = table do |t|
1025
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
1026
+ stats.each do |entry|
1027
+ index = entry[:instance]
1028
+ stat = entry[:stats]
1029
+ hp = "#{stat[:host]}:#{stat[:port]}"
1030
+ uptime = uptime_string(stat[:uptime])
1031
+ usage = stat[:usage]
1032
+ if usage
1033
+ cpu = usage[:cpu]
1034
+ mem = (usage[:mem] * 1024) # mem comes in K's
1035
+ disk = usage[:disk]
1036
+ end
1037
+ mem_quota = stat[:mem_quota]
1038
+ disk_quota = stat[:disk_quota]
1039
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
1040
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
1041
+ cpu = cpu ? cpu.to_s : 'NA'
1042
+ cpu = "#{cpu}% (#{stat[:cores]})"
1043
+ t << [index, cpu, mem, disk, uptime]
1044
+ end
1045
+ end
1046
+
1047
+ if stats.empty?
1048
+ display "No running instances for [#{appname}]".yellow
1049
+ else
1050
+ display stats_table
1051
+ end
1052
+ end
1053
+
1054
+ def all_files(appname, path)
1055
+ instances_info_envelope = client.app_instances(appname)
1056
+ return if instances_info_envelope.is_a?(Array)
1057
+ instances_info = instances_info_envelope[:instances] || []
1058
+ instances_info.each do |entry|
1059
+ begin
1060
+ content = client.app_files(appname, path, entry[:index])
1061
+ display_logfile(
1062
+ path,
1063
+ content,
1064
+ entry[:index],
1065
+ "====> [#{entry[:index]}: #{path}] <====\n".bold
1066
+ )
1067
+ rescue VMC::Client::TargetError
1068
+ end
1069
+ end
1070
+ end
1071
+ end
1072
+
1073
+ class FileWithPercentOutput < ::File
1074
+ class << self
1075
+ attr_accessor :display_str, :upload_size
1076
+ end
1077
+
1078
+ def update_display(rsize)
1079
+ @read ||= 0
1080
+ @read += rsize
1081
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
1082
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1083
+ clear(FileWithPercentOutput.display_str.size + 5)
1084
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
1085
+ VMC::Cli::Config.output.flush
1086
+ end
1087
+ end
1088
+
1089
+ def read(*args)
1090
+ result = super(*args)
1091
+ if result && result.size > 0
1092
+ update_display(result.size)
1093
+ else
1094
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1095
+ clear(FileWithPercentOutput.display_str.size + 5)
1096
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
1097
+ display('OK'.green)
1098
+ end
1099
+ end
1100
+ result
1101
+ end
1102
+ end
1103
+
1104
+ end