vmc-tsuru 0.1.alpha

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