vmc-tsuru 0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1107 @@
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
+ upload_app_bits(appname, @path)
272
+ restart appname if app[:state] == 'STARTED'
273
+ else
274
+ each_app do |name|
275
+ display "Updating application '#{name}'..."
276
+
277
+ app = client.app_info(name)
278
+ upload_app_bits(name, @application)
279
+ restart name if app[:state] == 'STARTED'
280
+ end
281
+ end
282
+ end
283
+
284
+ def push(appname=nil)
285
+ unless no_prompt || @options[:path]
286
+ proceed = ask(
287
+ 'Would you like to deploy from the current directory?',
288
+ :default => true
289
+ )
290
+
291
+ unless proceed
292
+ @path = ask('Deployment path')
293
+ end
294
+ end
295
+
296
+ pushed = false
297
+ each_app(false) do |name|
298
+ display "Pushing application '#{name}'..." if name
299
+ do_push(name)
300
+ pushed = true
301
+ end
302
+
303
+ unless pushed
304
+ @application = @path
305
+ do_push(appname)
306
+ end
307
+ end
308
+
309
+ def environment(appname)
310
+ app = client.app_info(appname)
311
+ env = app[:env] || []
312
+ return display JSON.pretty_generate(env) if @options[:json]
313
+ return display "No Environment Variables" if env.empty?
314
+ etable = table do |t|
315
+ t.headings = 'Variable', 'Value'
316
+ env.each do |e|
317
+ k,v = e.split('=', 2)
318
+ t << [k, v]
319
+ end
320
+ end
321
+ display "\n"
322
+ display etable
323
+ end
324
+
325
+ def environment_add(appname, k, v=nil)
326
+ app = client.app_info(appname)
327
+ env = app[:env] || []
328
+ k,v = k.split('=', 2) unless v
329
+ env << "#{k}=#{v}"
330
+ display "Adding Environment Variable [#{k}=#{v}]: ", false
331
+ app[:env] = env
332
+ client.update_app(appname, app)
333
+ display 'OK'.green
334
+ restart appname if app[:state] == 'STARTED'
335
+ end
336
+
337
+ def environment_del(appname, variable)
338
+ app = client.app_info(appname)
339
+ env = app[:env] || []
340
+ deleted_env = nil
341
+ env.each do |e|
342
+ k,v = e.split('=')
343
+ if (k == variable)
344
+ deleted_env = e
345
+ break;
346
+ end
347
+ end
348
+ display "Deleting Environment Variable [#{variable}]: ", false
349
+ if deleted_env
350
+ env.delete(deleted_env)
351
+ app[:env] = env
352
+ client.update_app(appname, app)
353
+ display 'OK'.green
354
+ restart appname if app[:state] == 'STARTED'
355
+ else
356
+ display 'OK'.green
357
+ end
358
+ end
359
+
360
+ private
361
+
362
+ def app_exists?(appname)
363
+ app_info = client.app_info(appname)
364
+ app_info != nil
365
+ rescue VMC::Client::NotFound
366
+ false
367
+ end
368
+
369
+ def check_deploy_directory(path)
370
+ err 'Deployment path does not exist' unless File.exists? path
371
+ err 'Deployment path is not a directory' unless File.directory? path
372
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
373
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
374
+ end
375
+
376
+ def check_unreachable_links(path)
377
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
378
+
379
+ pwd = Pathname.pwd
380
+
381
+ abspath = File.expand_path(path)
382
+ unreachable = []
383
+ files.each do |f|
384
+ file = Pathname.new(f)
385
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
386
+ unreachable << file.relative_path_from(pwd)
387
+ end
388
+ end
389
+
390
+ unless unreachable.empty?
391
+ root = Pathname.new(path).relative_path_from(pwd)
392
+ err "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
393
+ end
394
+ end
395
+
396
+ def find_sockets(path)
397
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
398
+ files && files.select { |f| File.socket? f }
399
+ end
400
+
401
+ def upload_app_bits(appname, path)
402
+ display 'Uploading Application:'
403
+
404
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
405
+ FileUtils.rm_f(upload_file)
406
+
407
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
408
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
409
+
410
+ Dir.chdir(path) do
411
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
412
+ if war_file = Dir.glob('*.war').first
413
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
414
+ else
415
+ check_unreachable_links(path)
416
+ FileUtils.mkdir(explode_dir)
417
+
418
+ files = Dir.glob('{*,.[^\.]*}')
419
+
420
+ # Do not process .git files
421
+ files.delete('.git') if files
422
+
423
+ FileUtils.cp_r(files, explode_dir)
424
+
425
+ find_sockets(explode_dir).each do |s|
426
+ File.delete s
427
+ end
428
+ end
429
+
430
+ # Send the resource list to the cloudcontroller, the response will tell us what it already has..
431
+ unless @options[:noresources]
432
+ display ' Checking for available resources: ', false
433
+ fingerprints = []
434
+ total_size = 0
435
+ resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
436
+ resource_files.each do |filename|
437
+ next if (File.directory?(filename) || !File.exists?(filename))
438
+ fingerprints << {
439
+ :size => File.size(filename),
440
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
441
+ :fn => filename
442
+ }
443
+ total_size += File.size(filename)
444
+ end
445
+
446
+ # Check to see if the resource check is worth the round trip
447
+ if (total_size > (64*1024)) # 64k for now
448
+ # Send resource fingerprints to the cloud controller
449
+ appcloud_resources = client.check_resources(fingerprints)
450
+ end
451
+ display 'OK'.green
452
+
453
+ if appcloud_resources
454
+ display ' Processing resources: ', false
455
+ # We can then delete what we do not need to send.
456
+ appcloud_resources.each do |resource|
457
+ FileUtils.rm_f resource[:fn]
458
+ # adjust filenames sans the explode_dir prefix
459
+ resource[:fn].sub!("#{explode_dir}/", '')
460
+ end
461
+ display 'OK'.green
462
+ end
463
+
464
+ end
465
+
466
+ # If no resource needs to be sent, add an empty file to ensure we have
467
+ # a multi-part request that is expected by nginx fronting the CC.
468
+ if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
469
+ Dir.chdir(explode_dir) do
470
+ File.new(".__empty__", "w")
471
+ end
472
+ end
473
+ # Perform Packing of the upload bits here.
474
+ display ' Packing application: ', false
475
+ VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
476
+ display 'OK'.green
477
+
478
+ upload_size = File.size(upload_file);
479
+ if upload_size > 1024*1024
480
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
481
+ elsif upload_size > 0
482
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
483
+ else
484
+ upload_size = '0K'
485
+ end
486
+
487
+ upload_str = " Uploading (#{upload_size}): "
488
+ display upload_str, false
489
+
490
+ FileWithPercentOutput.display_str = upload_str
491
+ FileWithPercentOutput.upload_size = File.size(upload_file);
492
+ file = FileWithPercentOutput.open(upload_file, 'rb')
493
+ client.upload_app(appname, file, appcloud_resources)
494
+
495
+ display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
496
+
497
+ display 'Push Status: ', false
498
+ display 'OK'.green
499
+ end
500
+
501
+ ensure
502
+ # Cleanup if we created an exploded directory.
503
+ FileUtils.rm_f(upload_file) if upload_file
504
+ FileUtils.rm_rf(explode_dir) if explode_dir
505
+ end
506
+
507
+ def check_app_limit
508
+ usage = client_info[:usage]
509
+ limits = client_info[:limits]
510
+ return unless usage and limits and limits[:apps]
511
+ if limits[:apps] == usage[:apps]
512
+ display "Not enough capacity for operation.".red
513
+ tapps = limits[:apps] || 0
514
+ apps = usage[:apps] || 0
515
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
516
+ end
517
+ end
518
+
519
+ def check_has_capacity_for(mem_wanted)
520
+ usage = client_info[:usage]
521
+ limits = client_info[:limits]
522
+ return unless usage and limits
523
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
524
+ if mem_wanted > available_for_use
525
+ tmem = pretty_size(limits[:memory]*1024*1024)
526
+ mem = pretty_size(usage[:memory]*1024*1024)
527
+ display "Not enough capacity for operation.".yellow
528
+ available = pretty_size(available_for_use * 1024 * 1024)
529
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
530
+ end
531
+ end
532
+
533
+ def mem_choices
534
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
535
+
536
+ return default unless client_info
537
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
538
+
539
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
540
+ check_has_capacity_for(64) if available_for_use < 64
541
+ return ['64M'] if available_for_use < 128
542
+ return ['64M', '128M'] if available_for_use < 256
543
+ return ['64M', '128M', '256M'] if available_for_use < 512
544
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
545
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
546
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
547
+ end
548
+
549
+ def normalize_mem(mem)
550
+ return mem if /K|G|M/i =~ mem
551
+ "#{mem}M"
552
+ end
553
+
554
+ def mem_choice_to_quota(mem_choice)
555
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
556
+ mem_quota
557
+ end
558
+
559
+ def mem_quota_to_choice(mem)
560
+ if mem < 1024
561
+ mem_choice = "#{mem}M"
562
+ else
563
+ mem_choice = "#{(mem/1024).to_i}G"
564
+ end
565
+ mem_choice
566
+ end
567
+
568
+ def get_instances(appname)
569
+ instances_info_envelope = client.app_instances(appname)
570
+ # Empty array is returned if there are no instances running.
571
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
572
+
573
+ instances_info = instances_info_envelope[:instances] || []
574
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
575
+
576
+ return display JSON.pretty_generate(instances_info) if @options[:json]
577
+
578
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
579
+
580
+ instances_table = table do |t|
581
+ show_debug = instances_info.any? { |e| e[:debug_port] }
582
+
583
+ headings = ['Index', 'State', 'Start Time']
584
+ headings << 'Debug IP' if show_debug
585
+ headings << 'Debug Port' if show_debug
586
+
587
+ t.headings = headings
588
+
589
+ instances_info.each do |entry|
590
+ row = [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
591
+ row << entry[:debug_ip] if show_debug
592
+ row << entry[:debug_port] if show_debug
593
+ t << row
594
+ end
595
+ end
596
+ display "\n"
597
+ display instances_table
598
+ end
599
+
600
+ def change_instances(appname, instances)
601
+ app = client.app_info(appname)
602
+
603
+ match = instances.match(/([+-])?\d+/)
604
+ err "Invalid number of instances '#{instances}'" unless match
605
+
606
+ instances = instances.to_i
607
+ current_instances = app[:instances]
608
+ new_instances = match.captures[0] ? current_instances + instances : instances
609
+ err "There must be at least 1 instance." if new_instances < 1
610
+
611
+ if current_instances == new_instances
612
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
613
+ return
614
+ end
615
+
616
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
617
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
618
+ app[:instances] = new_instances
619
+ client.update_app(appname, app)
620
+ display 'OK'.green
621
+ end
622
+
623
+ def health(d)
624
+ return 'N/A' unless (d and d[:state])
625
+ return 'STOPPED' if d[:state] == 'STOPPED'
626
+
627
+ healthy_instances = d[:runningInstances]
628
+ expected_instance = d[:instances]
629
+ health = nil
630
+
631
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
632
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
633
+ end
634
+
635
+ return 'RUNNING' if health && health == 1.0
636
+ return "#{(health * 100).round}%" if health
637
+ return 'N/A'
638
+ end
639
+
640
+ def app_started_properly(appname, error_on_health)
641
+ app = client.app_info(appname)
642
+ case health(app)
643
+ when 'N/A'
644
+ # Health manager not running.
645
+ err "\nApplication '#{appname}'s state is undetermined, not enough information available." if error_on_health
646
+ return false
647
+ when 'RUNNING'
648
+ return true
649
+ else
650
+ if app[:meta][:debug] == "suspend"
651
+ display "\nApplication [#{appname}] has started in a mode that is waiting for you to trigger startup."
652
+ return true
653
+ else
654
+ return false
655
+ end
656
+ end
657
+ end
658
+
659
+ def display_logfile(path, content, instance='0', banner=nil)
660
+ banner ||= "====> #{path} <====\n\n"
661
+
662
+ unless content.empty?
663
+ display banner
664
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
665
+ unless prefix
666
+ display content
667
+ else
668
+ lines = content.split("\n")
669
+ lines.each { |line| display "#{prefix} #{line}"}
670
+ end
671
+ display ''
672
+ end
673
+ end
674
+
675
+ def log_file_paths
676
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
677
+ end
678
+
679
+ def grab_all_logs(appname)
680
+ instances_info_envelope = client.app_instances(appname)
681
+ return if instances_info_envelope.is_a?(Array)
682
+ instances_info = instances_info_envelope[:instances] || []
683
+ instances_info.each do |entry|
684
+ grab_logs(appname, entry[:index])
685
+ end
686
+ end
687
+
688
+ def grab_logs(appname, instance)
689
+ log_file_paths.each do |path|
690
+ begin
691
+ content = client.app_files(appname, path, instance)
692
+ display_logfile(path, content, instance)
693
+ rescue VMC::Client::TargetError
694
+ end
695
+ end
696
+ end
697
+
698
+ def grab_crash_logs(appname, instance, was_staged=false)
699
+ # stage crash info
700
+ crashes(appname, false) unless was_staged
701
+
702
+ instance ||= '0'
703
+ map = VMC::Cli::Config.instances
704
+ instance = map[instance] if map[instance]
705
+
706
+ %w{
707
+ /logs/err.log /logs/staging.log /logs/migration.log
708
+ /app/logs/stderr.log /app/logs/stdout.log /app/logs/startup.log
709
+ /app/logs/migration.log
710
+ }.each do |path|
711
+ begin
712
+ content = client.app_files(appname, path, instance)
713
+ display_logfile(path, content, instance)
714
+ rescue VMC::Client::TargetError
715
+ end
716
+ end
717
+ end
718
+
719
+ def grab_startup_tail(appname, since = 0)
720
+ new_lines = 0
721
+ path = "logs/startup.log"
722
+ content = client.app_files(appname, path)
723
+ if content && !content.empty?
724
+ display "\n==== displaying startup log ====\n\n" if since == 0
725
+ response_lines = content.split("\n")
726
+ lines = response_lines.size
727
+ tail = response_lines[since, lines] || []
728
+ new_lines = tail.size
729
+ display tail.join("\n") if new_lines > 0
730
+ end
731
+ since + new_lines
732
+ rescue VMC::Client::TargetError
733
+ 0
734
+ end
735
+
736
+ def provisioned_services_apps_hash
737
+ apps = client.apps
738
+ services_apps_hash = {}
739
+ apps.each {|app|
740
+ app[:services].each { |svc|
741
+ svc_apps = services_apps_hash[svc]
742
+ unless svc_apps
743
+ svc_apps = Set.new
744
+ services_apps_hash[svc] = svc_apps
745
+ end
746
+ svc_apps.add(app[:name])
747
+ } unless app[:services] == nil
748
+ }
749
+ services_apps_hash
750
+ end
751
+
752
+ def run(appname, command)
753
+ app = client.app_info(appname)
754
+ display client.run(appname, command)[:output]
755
+ end
756
+
757
+ def delete_app(appname, force)
758
+ app = client.app_info(appname)
759
+ services_to_delete = []
760
+ app_services = app[:services]
761
+ services_apps_hash = provisioned_services_apps_hash
762
+ app_services.each { |service|
763
+ del_service = force && no_prompt
764
+ unless no_prompt || force
765
+ del_service = ask(
766
+ "Provisioned service [#{service}] detected, would you like to delete it?",
767
+ :default => false
768
+ )
769
+
770
+ if del_service
771
+ apps_using_service = services_apps_hash[service].reject!{ |app| app == appname}
772
+ if apps_using_service.size > 0
773
+ del_service = ask(
774
+ "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?",
775
+ :default => false
776
+ )
777
+ end
778
+ end
779
+ end
780
+ services_to_delete << service if del_service
781
+ }
782
+
783
+ display "Deleting application [#{appname}]: ", false
784
+ client.delete_app(appname)
785
+ display 'OK'.green
786
+
787
+ services_to_delete.each do |s|
788
+ delete_service_banner(s)
789
+ end
790
+ end
791
+
792
+ def do_start(appname, push=false)
793
+ app = client.app_info(appname)
794
+ return display "Application '#{appname}' could not be found".red if app.nil?
795
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
796
+
797
+
798
+
799
+ if @options[:debug]
800
+ runtimes = client.runtimes_info
801
+ return display "Cannot get runtime information." unless runtimes
802
+
803
+ runtime = runtimes[app[:staging][:stack].to_sym]
804
+ return display "Unknown runtime." unless runtime
805
+
806
+ unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug]
807
+ modes = runtime[:debug_modes] || []
808
+
809
+ display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode"
810
+
811
+ if push
812
+ display "Try 'vmc start' with one of the following modes: #{modes.inspect}"
813
+ else
814
+ display "Available modes: #{modes.inspect}"
815
+ end
816
+
817
+ return
818
+ end
819
+ end
820
+
821
+ banner = "Staging Application '#{appname}': "
822
+ display banner, false
823
+
824
+ t = Thread.new do
825
+ count = 0
826
+ while count < TAIL_TICKS do
827
+ display '.', false
828
+ sleep SLEEP_TIME
829
+ count += 1
830
+ end
831
+ end
832
+
833
+ app[:state] = 'STARTED'
834
+ app[:debug] = @options[:debug]
835
+ app[:console] = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model]).console
836
+ client.update_app(appname, app)
837
+
838
+ Thread.kill(t)
839
+ clear(LINE_LENGTH)
840
+ display "#{banner}#{'OK'.green}"
841
+
842
+ banner = "Starting Application '#{appname}': "
843
+ display banner, false
844
+
845
+ count = log_lines_displayed = 0
846
+ failed = false
847
+ start_time = Time.now.to_i
848
+
849
+ loop do
850
+ display '.', false unless count > TICKER_TICKS
851
+ sleep SLEEP_TIME
852
+
853
+ break if app_started_properly(appname, count > HEALTH_TICKS)
854
+
855
+ if !crashes(appname, false, start_time).empty?
856
+ # Check for the existance of crashes
857
+ display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
858
+ grab_crash_logs(appname, '0', true)
859
+ if push and !no_prompt
860
+ display "\n"
861
+ delete_app(appname, false) if ask "Delete the application?", :default => true
862
+ end
863
+ failed = true
864
+ break
865
+ elsif count > TAIL_TICKS
866
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
867
+ end
868
+
869
+ count += 1
870
+ if count > GIVEUP_TICKS # 2 minutes
871
+ display "\nApplication is taking too long to start, check your logs".yellow
872
+ break
873
+ end
874
+ end
875
+ exit(false) if failed
876
+ clear(LINE_LENGTH)
877
+ display "#{banner}#{'OK'.green}"
878
+ end
879
+
880
+ def do_stop(appname)
881
+ app = client.app_info(appname)
882
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
883
+ display "Stopping Application '#{appname}': ", false
884
+ app[:state] = 'STOPPED'
885
+ client.update_app(appname, app)
886
+ display 'OK'.green
887
+ end
888
+
889
+ def do_push(appname=nil)
890
+ unless @app_info || no_prompt
891
+ @manifest = { "applications" => { @path => { "name" => appname } } }
892
+
893
+ interact
894
+
895
+ # if ask("Would you like to save this configuration?", :default => false)
896
+ # save_manifest
897
+ # end
898
+
899
+ resolve_manifest(@manifest)
900
+
901
+ @app_info = @manifest["applications"][@path]
902
+ end
903
+
904
+ # instances = info(:instances, 1)
905
+ # exec = info(:exec, 'thin start')
906
+
907
+ ignore_framework = @options[:noframework]
908
+ no_start = @options[:nostart]
909
+
910
+ appname ||= info(:name)
911
+ url = info(:url) || info(:urls)
912
+ mem, memswitch = nil, info(:mem)
913
+ memswitch = normalize_mem(memswitch) if memswitch
914
+
915
+ # Check app existing upfront if we have appname
916
+ app_checked = false
917
+ if appname
918
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
919
+ app_checked = true
920
+ else
921
+ raise VMC::Client::AuthError unless client.logged_in?
922
+ end
923
+
924
+ # check if we have hit our app limit
925
+ # check_app_limit
926
+ # check memsize here for capacity
927
+ # if memswitch && !no_start
928
+ # check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
929
+ # end
930
+
931
+ appname ||= ask("Application Name") unless no_prompt
932
+ err "Application Name required." if appname.nil? || appname.empty?
933
+
934
+ check_deploy_directory(@application)
935
+
936
+ if !app_checked and app_exists?(appname)
937
+ err "Application '#{appname}' already exists, use update or delete."
938
+ end
939
+
940
+ default_url = "#{appname}.#{target_base}"
941
+
942
+ # unless no_prompt || url
943
+ # url = ask(
944
+ # "Application Deployed URL",
945
+ # :default => default_url
946
+ # )
947
+
948
+ # # common error case is for prompted users to answer y or Y or yes or
949
+ # # YES to this ask() resulting in an unintended URL of y. Special case
950
+ # # this common error
951
+ # url = nil if YES_SET.member? url
952
+ # end
953
+
954
+ url ||= default_url
955
+
956
+ if ignore_framework
957
+ framework = VMC::Cli::Framework.new
958
+ elsif f = info(:framework)
959
+ info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }]
960
+
961
+ framework = VMC::Cli::Framework.new(f["name"], info)
962
+ exec = framework.exec if framework && framework.exec
963
+ else
964
+ framework = detect_framework(prompt_ok)
965
+ end
966
+
967
+ err "Application Type undetermined for path '#{@application}'" unless framework
968
+
969
+ # if memswitch
970
+ # mem = memswitch
971
+ # elsif prompt_ok
972
+ # mem = ask("Memory Reservation",
973
+ # :default => framework.memory, :choices => mem_choices)
974
+ # else
975
+ # mem = framework.memory
976
+ # end
977
+
978
+ # Set to MB number
979
+ #mem_quota = mem_choice_to_quota(mem)
980
+
981
+ # check memsize here for capacity
982
+ #check_has_capacity_for(mem_quota * instances) unless no_start
983
+
984
+ display 'Creating Application: ', false
985
+
986
+ manifest = {
987
+ :name => "#{appname}",
988
+ :staging => {
989
+ :framework => framework.name,
990
+ :runtime => info(:runtime)
991
+ },
992
+ :framework => framework.name,
993
+ :uris => Array(url),
994
+ #:instances => instances,
995
+ # :resources => {
996
+ # :memory => mem_quota
997
+ # },
998
+ }
999
+
1000
+ # Send the manifest to the cloud controller
1001
+ client.create_app(appname, manifest)
1002
+ display 'OK'.green
1003
+
1004
+
1005
+ # existing = Set.new(client.services.collect { |s| s[:name] })
1006
+
1007
+ # if @app_info && services = @app_info["services"]
1008
+ # services.each do |name, info|
1009
+ # unless existing.include? name
1010
+ # create_service_banner(info["type"], name, true)
1011
+ # end
1012
+
1013
+ # bind_service_banner(name, appname)
1014
+ # end
1015
+ # end
1016
+
1017
+ # Stage and upload the app bits.
1018
+ #upload_app_bits(appname, @application)
1019
+
1020
+ #start(appname, true) unless no_start
1021
+ end
1022
+
1023
+ def do_stats(appname)
1024
+ stats = client.app_stats(appname)
1025
+ return display JSON.pretty_generate(stats) if @options[:json]
1026
+
1027
+ stats_table = table do |t|
1028
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
1029
+ stats.each do |entry|
1030
+ index = entry[:instance]
1031
+ stat = entry[:stats]
1032
+ hp = "#{stat[:host]}:#{stat[:port]}"
1033
+ uptime = uptime_string(stat[:uptime])
1034
+ usage = stat[:usage]
1035
+ if usage
1036
+ cpu = usage[:cpu]
1037
+ mem = (usage[:mem] * 1024) # mem comes in K's
1038
+ disk = usage[:disk]
1039
+ end
1040
+ mem_quota = stat[:mem_quota]
1041
+ disk_quota = stat[:disk_quota]
1042
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
1043
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
1044
+ cpu = cpu ? cpu.to_s : 'NA'
1045
+ cpu = "#{cpu}% (#{stat[:cores]})"
1046
+ t << [index, cpu, mem, disk, uptime]
1047
+ end
1048
+ end
1049
+
1050
+ if stats.empty?
1051
+ display "No running instances for [#{appname}]".yellow
1052
+ else
1053
+ display stats_table
1054
+ end
1055
+ end
1056
+
1057
+ def all_files(appname, path)
1058
+ instances_info_envelope = client.app_instances(appname)
1059
+ return if instances_info_envelope.is_a?(Array)
1060
+ instances_info = instances_info_envelope[:instances] || []
1061
+ instances_info.each do |entry|
1062
+ begin
1063
+ content = client.app_files(appname, path, entry[:index])
1064
+ display_logfile(
1065
+ path,
1066
+ content,
1067
+ entry[:index],
1068
+ "====> [#{entry[:index]}: #{path}] <====\n".bold
1069
+ )
1070
+ rescue VMC::Client::TargetError
1071
+ end
1072
+ end
1073
+ end
1074
+ end
1075
+
1076
+ class FileWithPercentOutput < ::File
1077
+ class << self
1078
+ attr_accessor :display_str, :upload_size
1079
+ end
1080
+
1081
+ def update_display(rsize)
1082
+ @read ||= 0
1083
+ @read += rsize
1084
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
1085
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1086
+ clear(FileWithPercentOutput.display_str.size + 5)
1087
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
1088
+ VMC::Cli::Config.output.flush
1089
+ end
1090
+ end
1091
+
1092
+ def read(*args)
1093
+ result = super(*args)
1094
+ if result && result.size > 0
1095
+ update_display(result.size)
1096
+ else
1097
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1098
+ clear(FileWithPercentOutput.display_str.size + 5)
1099
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
1100
+ display('OK'.green)
1101
+ end
1102
+ end
1103
+ result
1104
+ end
1105
+ end
1106
+
1107
+ end