vmc-stic 0.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 (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