vmc-stic 0.0.1

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