jdc 0.1.1

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