paasio 0.3.16.beta.2

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,1284 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'tempfile'
5
+ require 'tmpdir'
6
+ require 'set'
7
+
8
+ module VMC::Cli::Command
9
+
10
+ class Apps < Base
11
+ include VMC::Cli::ServicesHelper
12
+ include VMC::Cli::ManifestHelper
13
+
14
+ def list
15
+ apps = client.apps
16
+ apps.sort! {|a, b| a[:name] <=> b[:name] }
17
+ return display JSON.pretty_generate(apps || []) if @options[:json]
18
+
19
+ display "\n"
20
+ return display "No Applications" if apps.nil? || apps.empty?
21
+
22
+ apps_table = table do |t|
23
+ t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
24
+ apps.each do |app|
25
+ t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
26
+ end
27
+ end
28
+ display apps_table
29
+ end
30
+
31
+ alias :apps :list
32
+
33
+ SLEEP_TIME = 1
34
+ LINE_LENGTH = 80
35
+
36
+ # Numerators are in secs
37
+ TICKER_TICKS = 25/SLEEP_TIME
38
+ HEALTH_TICKS = 5/SLEEP_TIME
39
+ TAIL_TICKS = 45/SLEEP_TIME
40
+ GIVEUP_TICKS = 120/SLEEP_TIME
41
+
42
+ def info(what, default=nil)
43
+ @options[what] || (@app_info && @app_info[what.to_s]) || default
44
+ end
45
+
46
+ def start(appname=nil, push=false)
47
+ if appname
48
+ do_start(appname, push)
49
+ else
50
+ each_app do |name|
51
+ do_start(name, push)
52
+ end
53
+ end
54
+ end
55
+
56
+ def stop(appname=nil)
57
+ if appname
58
+ do_stop(appname)
59
+ else
60
+ reversed = []
61
+ each_app do |name|
62
+ reversed.unshift name
63
+ end
64
+
65
+ reversed.each do |name|
66
+ do_stop(name)
67
+ end
68
+ end
69
+ end
70
+
71
+ def restart(appname=nil)
72
+ stop(appname)
73
+ start(appname)
74
+ end
75
+
76
+ def rename(appname, newname)
77
+ app = client.app_info(appname)
78
+ app[:name] = newname
79
+ display 'Renaming Appliction: '
80
+ client.update_app(newname, app)
81
+ display 'OK'.green
82
+ end
83
+
84
+ def mem(appname, memsize=nil)
85
+ app = client.app_info(appname)
86
+ mem = current_mem = mem_quota_to_choice(app[:resources][:memory])
87
+ memsize = normalize_mem(memsize) if memsize
88
+
89
+ memsize ||= ask(
90
+ "Update Memory Reservation?",
91
+ :default => current_mem,
92
+ :choices => mem_choices
93
+ )
94
+
95
+ mem = mem_choice_to_quota(mem)
96
+ memsize = mem_choice_to_quota(memsize)
97
+ current_mem = mem_choice_to_quota(current_mem)
98
+
99
+ display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
100
+
101
+ # check memsize here for capacity
102
+ check_has_capacity_for((memsize - mem) * app[:instances])
103
+
104
+ mem = memsize
105
+
106
+ if (mem != current_mem)
107
+ app[:resources][:memory] = mem
108
+ client.update_app(appname, app)
109
+ display 'OK'.green
110
+ restart appname if app[:state] == 'STARTED'
111
+ else
112
+ display 'OK'.green
113
+ end
114
+ end
115
+
116
+ def map(appname, url)
117
+ app = client.app_info(appname)
118
+ uris = app[:uris] || []
119
+ uris << url
120
+ app[:uris] = uris
121
+ client.update_app(appname, app)
122
+ display "Successfully mapped url".green
123
+ end
124
+
125
+ def unmap(appname, url)
126
+ app = client.app_info(appname)
127
+ uris = app[:uris] || []
128
+ url = url.gsub(/^http(s*):\/\//i, '')
129
+ deleted = uris.delete(url)
130
+ err "Invalid url" unless deleted
131
+ app[:uris] = uris
132
+ client.update_app(appname, app)
133
+ display "Successfully unmapped url".green
134
+ end
135
+
136
+ def delete(appname=nil)
137
+ force = @options[:force]
138
+ if @options[:all]
139
+ if no_prompt || force || ask("Delete ALL applications and services?", :default => false)
140
+ apps = client.apps
141
+ apps.each { |app| delete_app(app[:name], force) }
142
+ end
143
+ else
144
+ err 'No valid appname given' unless appname
145
+ delete_app(appname, force)
146
+ end
147
+ end
148
+
149
+ def files(appname, path='/')
150
+ return all_files(appname, path) if @options[:all] && !@options[:instance]
151
+ instance = @options[:instance] || '0'
152
+ content = client.app_files(appname, path, instance)
153
+ display content
154
+ rescue VMC::Client::NotFound => e
155
+ err 'No such file or directory'
156
+ end
157
+
158
+ def logs(appname)
159
+ # Check if we have an app before progressing further
160
+ client.app_info(appname)
161
+ return grab_all_logs(appname) if @options[:all] && !@options[:instance]
162
+ instance = @options[:instance] || '0'
163
+ grab_logs(appname, instance)
164
+ end
165
+
166
+ def crashes(appname, print_results=true, since=0)
167
+ crashed = client.app_crashes(appname)[:crashes]
168
+ crashed.delete_if { |c| c[:since] < since }
169
+ instance_map = {}
170
+
171
+ # return display JSON.pretty_generate(apps) if @options[:json]
172
+
173
+
174
+ counter = 0
175
+ crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
176
+ crashed_table = table do |t|
177
+ t.headings = 'Name', 'Instance ID', 'Crashed Time'
178
+ crashed.each do |crash|
179
+ name = "#{appname}-#{counter += 1}"
180
+ instance_map[name] = crash[:instance]
181
+ t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
182
+ end
183
+ end
184
+
185
+ VMC::Cli::Config.store_instances(instance_map)
186
+
187
+ if @options[:json]
188
+ return display JSON.pretty_generate(crashed)
189
+ elsif print_results
190
+ display "\n"
191
+ if crashed.empty?
192
+ display "No crashed instances for [#{appname}]" if print_results
193
+ else
194
+ display crashed_table if print_results
195
+ end
196
+ end
197
+
198
+ crashed
199
+ end
200
+
201
+ def crashlogs(appname)
202
+ instance = @options[:instance] || '0'
203
+ grab_crash_logs(appname, instance)
204
+ end
205
+
206
+ def instances(appname, num=nil)
207
+ if num
208
+ change_instances(appname, num)
209
+ else
210
+ get_instances(appname)
211
+ end
212
+ end
213
+
214
+ def stats(appname=nil)
215
+ if appname
216
+ display "\n", false
217
+ do_stats(appname)
218
+ else
219
+ each_app do |n|
220
+ display "\n#{n}:"
221
+ do_stats(n)
222
+ end
223
+ end
224
+ end
225
+
226
+ def upload(appname=nil)
227
+ if appname
228
+ app = client.app_info(appname)
229
+
230
+ # check deploy status
231
+ deploy = client.app_most_recent_deploy(appname)
232
+ if deploy && deploy[:sha]
233
+ unless has_git_commit?(@path, deploy[:sha])
234
+ unless ask("The last commit in the currently deployed code is:\n #{deploy[:sha]}.\n\nThe current path doesn't include that commit.\nManually uploading may revert changes.\nContinue anyway?", :default => false)
235
+ exit 0
236
+ end
237
+ end
238
+ end
239
+
240
+ upload_app_bits(appname, @path)
241
+ restart appname if app[:state] == 'STARTED'
242
+ else
243
+ each_app do |name|
244
+ display "Updating application '#{name}'..."
245
+ app = client.app_info(name)
246
+
247
+ # check deploy status
248
+ deploy = client.app_most_recent_deploy(name)
249
+ if deploy && deploy[:sha]
250
+ unless has_git_commit?(@application, deploy[:sha])
251
+ unless ask("The last commit in the currently deployed code is:\n #{deploy[:sha]}.\n\nThe current path doesn't include that commit.\nManually uploading may revert changes.\nContinue anyway?", :default => false)
252
+ exit 0
253
+ end
254
+ end
255
+ end
256
+
257
+ upload_app_bits(name, @application)
258
+ restart name if app[:state] == 'STARTED'
259
+ end
260
+ end
261
+ end
262
+
263
+ def update(appname=nil)
264
+ if appname
265
+ app = client.app_info(appname)
266
+ if @options[:canary]
267
+ display "[--canary] is deprecated and will be removed in a future version".yellow
268
+ end
269
+ upload_app_bits(appname, @path)
270
+ restart appname if app[:state] == 'STARTED'
271
+ else
272
+ each_app do |name|
273
+ display "Updating application '#{name}'..."
274
+
275
+ app = client.app_info(name)
276
+ upload_app_bits(name, @application)
277
+ restart name if app[:state] == 'STARTED'
278
+ end
279
+ end
280
+ end
281
+
282
+ def create(appname=nil)
283
+ created = false
284
+ each_app(false) do |name|
285
+ display "Creating application '#{name}'..." if name
286
+ do_create(name)
287
+ created = true
288
+ end
289
+
290
+ unless created
291
+ do_create(appname)
292
+ end
293
+ end
294
+
295
+ def push(appname=nil)
296
+ unless no_prompt || @options[:path]
297
+ proceed = ask(
298
+ 'Would you like to deploy from the current directory?',
299
+ :default => true
300
+ )
301
+
302
+ unless proceed
303
+ @path = ask('Deployment path')
304
+ end
305
+ end
306
+
307
+ pushed = false
308
+ each_app(false) do |name|
309
+ display "Pushing application '#{name}'..." if name
310
+ do_push(name)
311
+ pushed = true
312
+ end
313
+
314
+ unless pushed
315
+ @application = @path
316
+ do_push(appname)
317
+ end
318
+ end
319
+
320
+ def environment(appname)
321
+ app = client.app_info(appname)
322
+ env = app[:env] || []
323
+ return display JSON.pretty_generate(env) if @options[:json]
324
+ return display "No Environment Variables" if env.empty?
325
+ etable = table do |t|
326
+ t.headings = 'Variable', 'Value'
327
+ env.each do |e|
328
+ k,v = e.split('=', 2)
329
+ t << [k, v]
330
+ end
331
+ end
332
+ display "\n"
333
+ display etable
334
+ end
335
+
336
+ def environment_add(appname, k, v=nil)
337
+ app = client.app_info(appname)
338
+ env = app[:env] || []
339
+ k,v = k.split('=', 2) unless v
340
+ env << "#{k}=#{v}"
341
+ display "Adding Environment Variable [#{k}=#{v}]: ", false
342
+ app[:env] = env
343
+ client.update_app(appname, app)
344
+ display 'OK'.green
345
+ restart appname if app[:state] == 'STARTED'
346
+ end
347
+
348
+ def environment_del(appname, variable)
349
+ app = client.app_info(appname)
350
+ env = app[:env] || []
351
+ deleted_env = nil
352
+ env.each do |e|
353
+ k,v = e.split('=')
354
+ if (k == variable)
355
+ deleted_env = e
356
+ break;
357
+ end
358
+ end
359
+ display "Deleting Environment Variable [#{variable}]: ", false
360
+ if deleted_env
361
+ env.delete(deleted_env)
362
+ app[:env] = env
363
+ client.update_app(appname, app)
364
+ display 'OK'.green
365
+ restart appname if app[:state] == 'STARTED'
366
+ else
367
+ display 'OK'.green
368
+ end
369
+ end
370
+
371
+ private
372
+
373
+ def app_exists?(appname)
374
+ app_info = client.app_info(appname)
375
+ app_info != nil
376
+ rescue VMC::Client::NotFound
377
+ false
378
+ end
379
+
380
+ def check_deploy_directory(path)
381
+ err 'Deployment path does not exist' unless File.exists? path
382
+ err 'Deployment path is not a directory' unless File.directory? path
383
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
384
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
385
+ end
386
+
387
+ def check_unreachable_links(path)
388
+ path = File.expand_path(path)
389
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
390
+
391
+ pwd = Pathname.pwd
392
+
393
+ abspath = File.expand_path(path)
394
+ unreachable = []
395
+ files.each do |f|
396
+ file = Pathname.new(f)
397
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
398
+ unreachable << file.relative_path_from(pwd)
399
+ end
400
+ end
401
+
402
+ unless unreachable.empty?
403
+ root = Pathname.new(path).relative_path_from(pwd)
404
+ err "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
405
+ end
406
+ end
407
+
408
+ def find_sockets(path)
409
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
410
+ files && files.select { |f| File.socket? f }
411
+ end
412
+
413
+ def upload_app_bits(appname, path)
414
+ display 'Uploading Application:'
415
+
416
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
417
+ FileUtils.rm_f(upload_file)
418
+
419
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
420
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
421
+
422
+ Dir.chdir(path) do
423
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
424
+ if war_file = Dir.glob('*.war').first
425
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
426
+ else
427
+ check_unreachable_links(path)
428
+ FileUtils.mkdir(explode_dir)
429
+
430
+ files = Dir.glob('{*,.[^\.]*}')
431
+
432
+ # Do not process .git files
433
+ files.delete('.git') if files
434
+
435
+ FileUtils.cp_r(files, explode_dir)
436
+
437
+ find_sockets(explode_dir).each do |s|
438
+ File.delete s
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 VMC::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
+ VMC::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 VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
508
+
509
+ display 'Push Status: ', false
510
+ display 'OK'.green
511
+ end
512
+
513
+ ensure
514
+ # Cleanup if we created an exploded directory.
515
+ FileUtils.rm_f(upload_file) if upload_file
516
+ FileUtils.rm_rf(explode_dir) if explode_dir
517
+ end
518
+
519
+ def check_app_limit
520
+ usage = client_info[:usage]
521
+ limits = client_info[:limits]
522
+ return unless usage and limits and limits[:apps]
523
+ if limits[:apps] == usage[:apps]
524
+ display "Not enough capacity for operation.".red
525
+ tapps = limits[:apps] || 0
526
+ apps = usage[:apps] || 0
527
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
528
+ end
529
+ end
530
+
531
+ def check_has_capacity_for(mem_wanted)
532
+ usage = client_info[:usage]
533
+ limits = client_info[:limits]
534
+ return unless usage and limits
535
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
536
+ if mem_wanted > available_for_use
537
+ tmem = pretty_size(limits[:memory]*1024*1024)
538
+ mem = pretty_size(usage[:memory]*1024*1024)
539
+ display "Not enough capacity for operation.".yellow
540
+ available = pretty_size(available_for_use * 1024 * 1024)
541
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
542
+ end
543
+ end
544
+
545
+ def mem_choices
546
+ default = ['128M', '256M', '512M', '1G', '2G']
547
+
548
+ return default unless client_info
549
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
550
+
551
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
552
+ check_has_capacity_for(128) if available_for_use < 128
553
+ return ['128M'] if available_for_use < 256
554
+ return ['128M', '256M'] if available_for_use < 512
555
+ return ['128M', '256M', '512M'] if available_for_use < 1024
556
+ return ['128M', '256M', '512M', '1G'] if available_for_use < 2048
557
+ return ['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 log_file_paths
687
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
688
+ end
689
+
690
+ def grab_all_logs(appname)
691
+ instances_info_envelope = client.app_instances(appname)
692
+ return if instances_info_envelope.is_a?(Array)
693
+ instances_info = instances_info_envelope[:instances] || []
694
+ instances_info.each do |entry|
695
+ grab_logs(appname, entry[:index])
696
+ end
697
+ end
698
+
699
+ def grab_logs(appname, instance)
700
+ log_file_paths.each do |path|
701
+ begin
702
+ content = client.app_files(appname, path, instance)
703
+ display_logfile(path, content, instance)
704
+ rescue VMC::Client::NotFound
705
+ end
706
+ end
707
+ end
708
+
709
+ def grab_crash_logs(appname, instance, was_staged=false)
710
+ # stage crash info
711
+ crashes(appname, false) unless was_staged
712
+
713
+ instance ||= '0'
714
+ map = VMC::Cli::Config.instances
715
+ instance = map[instance] if map[instance]
716
+
717
+ %w{
718
+ /logs/err.log /logs/staging.log /app/logs/stderr.log
719
+ /app/logs/stdout.log /app/logs/startup.log /app/logs/migration.log
720
+ }.each do |path|
721
+ begin
722
+ content = client.app_files(appname, path, instance)
723
+ display_logfile(path, content, instance)
724
+ rescue VMC::Client::NotFound
725
+ end
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
+ end
743
+
744
+ def provisioned_services_apps_hash
745
+ apps = client.apps
746
+ services_apps_hash = {}
747
+ apps.each {|app|
748
+ app[:services].each { |svc|
749
+ svc_apps = services_apps_hash[svc]
750
+ unless svc_apps
751
+ svc_apps = Set.new
752
+ services_apps_hash[svc] = svc_apps
753
+ end
754
+ svc_apps.add(app[:name])
755
+ } unless app[:services] == nil
756
+ }
757
+ services_apps_hash
758
+ end
759
+
760
+ def delete_app(appname, force)
761
+ app = client.app_info(appname)
762
+ services_to_delete = []
763
+ app_services = app[:services]
764
+ services_apps_hash = provisioned_services_apps_hash
765
+ app_services.each { |service|
766
+ del_service = force && no_prompt
767
+ unless no_prompt || force
768
+ del_service = ask(
769
+ "Provisioned service [#{service}] detected, would you like to delete it?",
770
+ :default => false
771
+ )
772
+
773
+ if del_service
774
+ apps_using_service = services_apps_hash[service].reject!{ |app| app == appname}
775
+ if apps_using_service.size > 0
776
+ del_service = ask(
777
+ "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?",
778
+ :default => false
779
+ )
780
+ end
781
+ end
782
+ end
783
+ services_to_delete << service if del_service
784
+ }
785
+
786
+ display "Deleting application [#{appname}]: ", false
787
+ client.delete_app(appname)
788
+ display 'OK'.green
789
+
790
+ services_to_delete.each do |s|
791
+ delete_service_banner(s)
792
+ end
793
+ end
794
+
795
+ def do_start(appname, push=false)
796
+ app = client.app_info(appname)
797
+
798
+ return display "Application '#{appname}' could not be found".red if app.nil?
799
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
800
+
801
+ if @options[:debug]
802
+ runtimes = client.runtimes_info
803
+ return display "Cannot get runtime information." unless runtimes
804
+
805
+ runtime = runtimes[app[:staging][:stack].to_sym]
806
+ return display "Unknown runtime." unless runtime
807
+
808
+ unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug]
809
+ modes = runtime[:debug_modes] || []
810
+
811
+ display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode"
812
+
813
+ if push
814
+ display "Try 'vmc start' with one of the following modes: #{modes.inspect}"
815
+ else
816
+ display "Available modes: #{modes.inspect}"
817
+ end
818
+
819
+ return
820
+ end
821
+ end
822
+
823
+ banner = "Staging Application '#{appname}': "
824
+ display banner, false
825
+
826
+ t = Thread.new do
827
+ count = 0
828
+ while count < TAIL_TICKS do
829
+ display '.', false
830
+ sleep SLEEP_TIME
831
+ count += 1
832
+ end
833
+ end
834
+
835
+ app[:state] = 'STARTED'
836
+ app[:debug] = @options[:debug]
837
+ client.update_app(appname, app)
838
+
839
+ Thread.kill(t)
840
+ clear(LINE_LENGTH)
841
+ display "#{banner}#{'OK'.green}"
842
+
843
+ banner = "Starting Application '#{appname}': "
844
+ display banner, false
845
+
846
+ count = log_lines_displayed = 0
847
+ failed = false
848
+ start_time = Time.now.to_i
849
+
850
+ loop do
851
+ display '.', false unless count > TICKER_TICKS
852
+ sleep SLEEP_TIME
853
+ begin
854
+ break if app_started_properly(appname, count > HEALTH_TICKS)
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
+ rescue => e
869
+ err(e.message, '')
870
+ end
871
+ count += 1
872
+ if count > GIVEUP_TICKS # 2 minutes
873
+ display "\nApplication is taking too long to start, check your logs".yellow
874
+ break
875
+ end
876
+ end
877
+ exit(false) if failed
878
+ clear(LINE_LENGTH)
879
+ display "#{banner}#{'OK'.green}"
880
+ end
881
+
882
+ def do_stop(appname)
883
+ app = client.app_info(appname)
884
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
885
+ display "Stopping Application '#{appname}': ", false
886
+ app[:state] = 'STOPPED'
887
+ client.update_app(appname, app)
888
+ display 'OK'.green
889
+ end
890
+
891
+ def do_create(appname=nil)
892
+ instances = info(:instances, 1)
893
+ exec = info(:exec, 'thin start')
894
+
895
+ ignore_framework = @options[:noframework]
896
+
897
+ appname ||= info(:name)
898
+ url = info(:url) || info(:urls)
899
+ mem, memswitch = nil, info(:mem)
900
+ memswitch = normalize_mem(memswitch) if memswitch
901
+
902
+ # Check app existing upfront if we have appname
903
+ app_checked = false
904
+ if appname
905
+ err "Application '#{appname}' already exists, please choose another name" if app_exists?(appname)
906
+ app_checked = true
907
+ else
908
+ raise VMC::Client::AuthError unless client.logged_in?
909
+ end
910
+
911
+ # check if we have hit our app limit
912
+ check_app_limit
913
+ # check memsize here for capacity
914
+ if memswitch && !no_start
915
+ check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
916
+ end
917
+
918
+ if !no_prompt && (appname.nil? || appname.empty?)
919
+ appname ||= ask("Application Name")
920
+ err "Application Name required." if appname.nil? || appname.empty?
921
+ else
922
+ puts "Using application name \"#{appname}\"..."
923
+ end
924
+
925
+ if !app_checked and app_exists?(appname)
926
+ err "Application '#{appname}' already exists, please choose another name."
927
+ end
928
+
929
+ default_url = "#{appname}.#{VMC::Cli::Config.suggest_url}"
930
+
931
+ unless no_prompt || url
932
+ url = ask(
933
+ "Application Deployed URL",
934
+ :default => default_url
935
+ )
936
+
937
+ # common error case is for prompted users to answer y or Y or yes or
938
+ # YES to this ask() resulting in an unintended URL of y. Special case
939
+ # this common error
940
+ url = nil if YES_SET.member? url
941
+ end
942
+
943
+ url ||= default_url
944
+
945
+ if ignore_framework
946
+ framework = VMC::Cli::Framework.new
947
+ elsif f = info(:framework)
948
+ info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }]
949
+
950
+ framework = VMC::Cli::Framework.new(f["name"], info)
951
+ exec = framework.exec if framework && framework.exec
952
+ else
953
+ @path = @application = '.'
954
+ framework = detect_framework(prompt_ok)
955
+ end
956
+
957
+ err "Application Type undetermined for '#{appname}'" unless framework
958
+
959
+ if memswitch
960
+ mem = memswitch
961
+ elsif prompt_ok
962
+ mem = ask("Memory Reservation",
963
+ :default => framework.memory, :choices => mem_choices)
964
+ else
965
+ mem = framework.memory
966
+ end
967
+
968
+ # Set to MB number
969
+ mem_quota = mem_choice_to_quota(mem)
970
+
971
+ # check memsize here for capacity
972
+ check_has_capacity_for(mem_quota * instances)
973
+
974
+ display 'Creating Application: ', false
975
+
976
+ manifest = {
977
+ :name => "#{appname}",
978
+ :staging => {
979
+ :framework => framework.name,
980
+ :runtime => info(:runtime)
981
+ },
982
+ :uris => Array(url),
983
+ :instances => instances,
984
+ :resources => {
985
+ :memory => mem_quota
986
+ },
987
+ }
988
+ manifest[:scm_type] = @options[:scm_type] if @options[:scm_type]
989
+
990
+ # Send the manifest to the cloud controller
991
+ client.create_app(appname, manifest)
992
+ display 'OK'.green
993
+
994
+ # Get the app info back
995
+ app = client.app_info(appname)
996
+ case app[:scm_type]
997
+ when 'git'
998
+ create_git_remote(appname, @options[:remote] || 'paasio', app[:repository_url])
999
+ when 'hg'
1000
+ create_hg_remote(appname, @options[:remote] || 'paasio', app[:repository_url])
1001
+ end
1002
+
1003
+ existing = Set.new(client.services.collect { |s| s[:name] })
1004
+
1005
+ if @app_info && services = @app_info["services"]
1006
+ services.each do |name, info|
1007
+ unless existing.include? name
1008
+ create_service_banner(info["type"], name, true)
1009
+ end
1010
+
1011
+ bind_service_banner(name, appname)
1012
+ end
1013
+ end
1014
+ end
1015
+
1016
+ def do_push(appname=nil)
1017
+ unless @app_info || no_prompt
1018
+ @manifest = { "applications" => { @path => { "name" => appname } } }
1019
+
1020
+ interact
1021
+
1022
+ if ask("Would you like to save this configuration?", :default => false)
1023
+ save_manifest
1024
+ end
1025
+
1026
+ resolve_manifest(@manifest)
1027
+
1028
+ @app_info = @manifest["applications"][@path]
1029
+ end
1030
+
1031
+ instances = info(:instances, 1)
1032
+ exec = info(:exec, 'thin start')
1033
+
1034
+ ignore_framework = @options[:noframework]
1035
+ no_start = @options[:nostart]
1036
+
1037
+ appname ||= info(:name)
1038
+ url = info(:url) || info(:urls)
1039
+ mem, memswitch = nil, info(:mem)
1040
+ memswitch = normalize_mem(memswitch) if memswitch
1041
+
1042
+ # Check app existing upfront if we have appname
1043
+ app_checked = false
1044
+ if appname
1045
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
1046
+ app_checked = true
1047
+ else
1048
+ raise VMC::Client::AuthError unless client.logged_in?
1049
+ end
1050
+
1051
+ # check if we have hit our app limit
1052
+ check_app_limit
1053
+ # check memsize here for capacity
1054
+ if memswitch && !no_start
1055
+ check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
1056
+ end
1057
+
1058
+ appname ||= ask("Application Name") unless no_prompt
1059
+ err "Application Name required." if appname.nil? || appname.empty?
1060
+
1061
+ check_deploy_directory(@application)
1062
+
1063
+ if !app_checked and app_exists?(appname)
1064
+ err "Application '#{appname}' already exists, use update or delete."
1065
+ end
1066
+
1067
+ default_url = "#{appname}.#{VMC::Cli::Config.suggest_url}"
1068
+
1069
+ unless no_prompt || url
1070
+ url = ask(
1071
+ "Application Deployed URL",
1072
+ :default => default_url
1073
+ )
1074
+
1075
+ # common error case is for prompted users to answer y or Y or yes or
1076
+ # YES to this ask() resulting in an unintended URL of y. Special case
1077
+ # this common error
1078
+ url = nil if YES_SET.member? url
1079
+ end
1080
+
1081
+ url ||= default_url
1082
+
1083
+ if ignore_framework
1084
+ framework = VMC::Cli::Framework.new
1085
+ elsif f = info(:framework)
1086
+ info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }]
1087
+
1088
+ framework = VMC::Cli::Framework.new(f["name"], info)
1089
+ exec = framework.exec if framework && framework.exec
1090
+ else
1091
+ framework = detect_framework(prompt_ok)
1092
+ end
1093
+
1094
+ err "Application Type undetermined for path '#{@application}'" unless framework
1095
+
1096
+ if memswitch
1097
+ mem = memswitch
1098
+ elsif prompt_ok
1099
+ mem = ask("Memory Reservation",
1100
+ :default => framework.memory, :choices => mem_choices)
1101
+ else
1102
+ mem = framework.memory
1103
+ end
1104
+
1105
+ # Set to MB number
1106
+ mem_quota = mem_choice_to_quota(mem)
1107
+
1108
+ # check memsize here for capacity
1109
+ check_has_capacity_for(mem_quota * instances) unless no_start
1110
+
1111
+ display 'Creating Application: ', false
1112
+
1113
+ manifest = {
1114
+ :name => "#{appname}",
1115
+ :staging => {
1116
+ :framework => framework.name,
1117
+ :runtime => info(:runtime)
1118
+ },
1119
+ :uris => Array(url),
1120
+ :instances => instances,
1121
+ :resources => {
1122
+ :memory => mem_quota
1123
+ },
1124
+ }
1125
+
1126
+ # Send the manifest to the cloud controller
1127
+ client.create_app(appname, manifest)
1128
+ display 'OK'.green
1129
+
1130
+
1131
+ existing = Set.new(client.services.collect { |s| s[:name] })
1132
+
1133
+ if @app_info && services = @app_info["services"]
1134
+ services.each do |name, info|
1135
+ unless existing.include? name
1136
+ create_service_banner(info["type"], name, true)
1137
+ end
1138
+
1139
+ bind_service_banner(name, appname)
1140
+ end
1141
+ end
1142
+
1143
+ # Stage and upload the app bits.
1144
+ upload_app_bits(appname, @application)
1145
+
1146
+ start(appname, true) unless no_start
1147
+ end
1148
+
1149
+ def do_stats(appname)
1150
+ stats = client.app_stats(appname)
1151
+ return display JSON.pretty_generate(stats) if @options[:json]
1152
+
1153
+ stats_table = table do |t|
1154
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
1155
+ stats.each do |entry|
1156
+ index = entry[:instance]
1157
+ stat = entry[:stats]
1158
+ hp = "#{stat[:host]}:#{stat[:port]}"
1159
+ uptime = uptime_string(stat[:uptime])
1160
+ usage = stat[:usage]
1161
+ if usage
1162
+ cpu = usage[:cpu]
1163
+ mem = (usage[:mem] * 1024) # mem comes in K's
1164
+ disk = usage[:disk]
1165
+ end
1166
+ mem_quota = stat[:mem_quota]
1167
+ disk_quota = stat[:disk_quota]
1168
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
1169
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
1170
+ cpu = cpu ? cpu.to_s : 'NA'
1171
+ cpu = "#{cpu}% (#{stat[:cores]})"
1172
+ t << [index, cpu, mem, disk, uptime]
1173
+ end
1174
+ end
1175
+
1176
+ if stats.empty?
1177
+ display "No running instances for [#{appname}]".yellow
1178
+ else
1179
+ display stats_table
1180
+ end
1181
+ end
1182
+
1183
+ def all_files(appname, path)
1184
+ instances_info_envelope = client.app_instances(appname)
1185
+ return if instances_info_envelope.is_a?(Array)
1186
+ instances_info = instances_info_envelope[:instances] || []
1187
+ instances_info.each do |entry|
1188
+ begin
1189
+ content = client.app_files(appname, path, entry[:index])
1190
+ display_logfile(
1191
+ path,
1192
+ content,
1193
+ entry[:index],
1194
+ "====> [#{entry[:index]}: #{path}] <====\n".bold
1195
+ )
1196
+ rescue VMC::Client::NotFound
1197
+ end
1198
+ end
1199
+ end
1200
+
1201
+ def create_hg_remote(appname, remote, repourl)
1202
+ return unless has_hg?
1203
+ return unless File.exists?(".hg")
1204
+
1205
+ # read hgrc
1206
+ hgrc = File.exists?('.hg/hgrc') ? File.read(".hg/hgrc") : ""
1207
+ if hgrc =~ /\[paths\]/
1208
+ hgrc.sub(/\[paths\]/, "[paths]\n#{remote} = #{repourl}")
1209
+ else
1210
+ hgrc << "\n[paths]\n#{remote} = #{repourl}\n"
1211
+ end
1212
+
1213
+ # rewrite the hgrc
1214
+ f = File.open('.hg/hgrc', 'w')
1215
+ f.puts hgrc
1216
+ f.close
1217
+
1218
+ display "Mercurial path #{remote} added"
1219
+ end
1220
+
1221
+ def create_git_remote(appname, remote, repourl)
1222
+ return unless has_git?
1223
+ return unless File.exists?(".git")
1224
+ return if git('remote').split("\n").include?(remote)
1225
+ git "remote add #{remote} #{repourl}"
1226
+ display "Git remote #{remote} added"
1227
+ end
1228
+
1229
+ def has_hg?
1230
+ %x{ hg --version }
1231
+ $?.success?
1232
+ end
1233
+
1234
+ def has_git?
1235
+ %x{ git --version }
1236
+ $?.success?
1237
+ end
1238
+
1239
+ def has_git_commit?(path, sha)
1240
+ result = git("--git-dir=#{File.expand_path(File.join(path, '.git'))} branch --contains #{sha}")
1241
+ return false unless $?.success?
1242
+ !!(result =~ /^\*\s/)
1243
+ end
1244
+
1245
+ def git(args)
1246
+ return "" unless has_git?
1247
+ flattened_args = [args].flatten.compact.join(" ")
1248
+ %x{ git #{flattened_args} 2>&1 }.strip
1249
+ end
1250
+
1251
+ end
1252
+
1253
+ class FileWithPercentOutput < ::File
1254
+ class << self
1255
+ attr_accessor :display_str, :upload_size
1256
+ end
1257
+
1258
+ def update_display(rsize)
1259
+ @read ||= 0
1260
+ @read += rsize
1261
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
1262
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1263
+ clear(FileWithPercentOutput.display_str.size + 5)
1264
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
1265
+ VMC::Cli::Config.output.flush
1266
+ end
1267
+ end
1268
+
1269
+ def read(*args)
1270
+ result = super(*args)
1271
+ if result && result.size > 0
1272
+ update_display(result.size)
1273
+ else
1274
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1275
+ clear(FileWithPercentOutput.display_str.size + 5)
1276
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
1277
+ display('OK'.green)
1278
+ end
1279
+ end
1280
+ result
1281
+ end
1282
+ end
1283
+
1284
+ end