olympe 0.1

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