jdc 0.1.1

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