olympe 0.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.
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