vmcu 0.3.17

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 +160 -0
  3. data/Rakefile +101 -0
  4. data/bin/vmcu +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 +47 -0
  13. data/lib/cli/commands/admin.rb +80 -0
  14. data/lib/cli/commands/apps.rb +1128 -0
  15. data/lib/cli/commands/base.rb +238 -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 +277 -0
  19. data/lib/cli/commands/services.rb +180 -0
  20. data/lib/cli/commands/user.rb +96 -0
  21. data/lib/cli/config.rb +192 -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 +244 -0
  26. data/lib/cli/manifest_helper.rb +302 -0
  27. data/lib/cli/runner.rb +543 -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 +118 -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 +591 -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 +263 -0
@@ -0,0 +1,47 @@
1
+ require "rbconfig"
2
+
3
+ ROOT = File.expand_path(File.dirname(__FILE__))
4
+ WINDOWS = !!(RbConfig::CONFIG['host_os'] =~ /mingw|mswin32|cygwin/)
5
+
6
+ module VMC
7
+ autoload :Client, "#{ROOT}/vmc/client"
8
+ autoload :Micro, "#{ROOT}/vmc/micro"
9
+
10
+ module Micro
11
+ module Switcher
12
+ autoload :Base, "#{ROOT}/vmc/micro/switcher/base"
13
+ autoload :Darwin, "#{ROOT}/vmc/micro/switcher/darwin"
14
+ autoload :Dummy, "#{ROOT}/vmc/micro/switcher/dummy"
15
+ autoload :Linux, "#{ROOT}/vmc/micro/switcher/linux"
16
+ autoload :Windows, "#{ROOT}/vmc/micro/switcher/windows"
17
+ end
18
+ autoload :VMrun, "#{ROOT}/vmc/micro/vmrun"
19
+ end
20
+
21
+ module Cli
22
+ autoload :Config, "#{ROOT}/cli/config"
23
+ autoload :Framework, "#{ROOT}/cli/frameworks"
24
+ autoload :Runner, "#{ROOT}/cli/runner"
25
+ autoload :ZipUtil, "#{ROOT}/cli/zip_util"
26
+ autoload :ServicesHelper, "#{ROOT}/cli/services_helper"
27
+ autoload :TunnelHelper, "#{ROOT}/cli/tunnel_helper"
28
+ autoload :ManifestHelper, "#{ROOT}/cli/manifest_helper"
29
+ autoload :ConsoleHelper, "#{ROOT}/cli/console_helper"
30
+
31
+ module Command
32
+ autoload :Base, "#{ROOT}/cli/commands/base"
33
+ autoload :Admin, "#{ROOT}/cli/commands/admin"
34
+ autoload :Apps, "#{ROOT}/cli/commands/apps"
35
+ autoload :Micro, "#{ROOT}/cli/commands/micro"
36
+ autoload :Misc, "#{ROOT}/cli/commands/misc"
37
+ autoload :Services, "#{ROOT}/cli/commands/services"
38
+ autoload :User, "#{ROOT}/cli/commands/user"
39
+ autoload :Manifest, "#{ROOT}/cli/commands/manifest"
40
+ end
41
+
42
+ end
43
+ end
44
+
45
+ require "#{ROOT}/cli/version"
46
+ require "#{ROOT}/cli/core_ext"
47
+ require "#{ROOT}/cli/errors"
@@ -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,1128 @@
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 `vmcu 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
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
375
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
376
+ end
377
+
378
+ def check_unreachable_links(path)
379
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
380
+
381
+ pwd = Pathname.pwd
382
+
383
+ abspath = File.expand_path(path)
384
+ unreachable = []
385
+ files.each do |f|
386
+ file = Pathname.new(f)
387
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
388
+ unreachable << file.relative_path_from(pwd)
389
+ end
390
+ end
391
+
392
+ unless unreachable.empty?
393
+ root = Pathname.new(path).relative_path_from(pwd)
394
+ err "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
395
+ end
396
+ end
397
+
398
+ def find_sockets(path)
399
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
400
+ files && files.select { |f| File.socket? f }
401
+ end
402
+
403
+ def upload_app_bits(appname, path)
404
+ display 'Uploading Application:'
405
+
406
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
407
+ FileUtils.rm_f(upload_file)
408
+
409
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
410
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
411
+
412
+ if path =~ /\.(war|zip)$/
413
+ #single file that needs unpacking
414
+ VMC::Cli::ZipUtil.unpack(path, explode_dir)
415
+ elsif !File.directory? path
416
+ #single file that doesn't need unpacking
417
+ FileUtils.mkdir(explode_dir)
418
+ FileUtils.cp(path,explode_dir)
419
+ else
420
+ Dir.chdir(path) do
421
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
422
+ if war_file = Dir.glob('*.war').first
423
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
424
+ elsif zip_file = Dir.glob('*.zip').first
425
+ VMC::Cli::ZipUtil.unpack(zip_file, explode_dir)
426
+ else
427
+ check_unreachable_links(path)
428
+ FileUtils.mkdir(explode_dir)
429
+
430
+ files = Dir.glob('{*,.[^\.]*}')
431
+
432
+ # Do not process .git files
433
+ files.delete('.git') if files
434
+
435
+ FileUtils.cp_r(files, explode_dir)
436
+
437
+ find_sockets(explode_dir).each do |s|
438
+ File.delete s
439
+ end
440
+ end
441
+ end
442
+ end
443
+
444
+ # Send the resource list to the cloudcontroller, the response will tell us what it already has..
445
+ unless @options[:noresources]
446
+ display ' Checking for available resources: ', false
447
+ fingerprints = []
448
+ total_size = 0
449
+ resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
450
+ resource_files.each do |filename|
451
+ next if (File.directory?(filename) || !File.exists?(filename))
452
+ fingerprints << {
453
+ :size => File.size(filename),
454
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
455
+ :fn => filename
456
+ }
457
+ total_size += File.size(filename)
458
+ end
459
+
460
+ # Check to see if the resource check is worth the round trip
461
+ if (total_size > (64*1024)) # 64k for now
462
+ # Send resource fingerprints to the cloud controller
463
+ appcloud_resources = client.check_resources(fingerprints)
464
+ end
465
+ display 'OK'.green
466
+
467
+ if appcloud_resources
468
+ display ' Processing resources: ', false
469
+ # We can then delete what we do not need to send.
470
+ appcloud_resources.each do |resource|
471
+ FileUtils.rm_f resource[:fn]
472
+ # adjust filenames sans the explode_dir prefix
473
+ resource[:fn].sub!("#{explode_dir}/", '')
474
+ end
475
+ display 'OK'.green
476
+ end
477
+
478
+ end
479
+
480
+ # If no resource needs to be sent, add an empty file to ensure we have
481
+ # a multi-part request that is expected by nginx fronting the CC.
482
+ if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
483
+ Dir.chdir(explode_dir) do
484
+ File.new(".__empty__", "w")
485
+ end
486
+ end
487
+ # Perform Packing of the upload bits here.
488
+ display ' Packing application: ', false
489
+ VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
490
+ display 'OK'.green
491
+
492
+ upload_size = File.size(upload_file);
493
+ if upload_size > 1024*1024
494
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
495
+ elsif upload_size > 0
496
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
497
+ else
498
+ upload_size = '0K'
499
+ end
500
+
501
+ upload_str = " Uploading (#{upload_size}): "
502
+ display upload_str, false
503
+
504
+ FileWithPercentOutput.display_str = upload_str
505
+ FileWithPercentOutput.upload_size = File.size(upload_file);
506
+ file = FileWithPercentOutput.open(upload_file, 'rb')
507
+
508
+ client.upload_app(appname, file, appcloud_resources)
509
+ display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
510
+
511
+ display 'Push Status: ', false
512
+ display 'OK'.green
513
+
514
+ ensure
515
+ # Cleanup if we created an exploded directory.
516
+ FileUtils.rm_f(upload_file) if upload_file
517
+ FileUtils.rm_rf(explode_dir) if explode_dir
518
+ end
519
+
520
+ def check_app_limit
521
+ usage = client_info[:usage]
522
+ limits = client_info[:limits]
523
+ return unless usage and limits and limits[:apps]
524
+ if limits[:apps] == usage[:apps]
525
+ display "Not enough capacity for operation.".red
526
+ tapps = limits[:apps] || 0
527
+ apps = usage[:apps] || 0
528
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
529
+ end
530
+ end
531
+
532
+ def check_has_capacity_for(mem_wanted)
533
+ usage = client_info[:usage]
534
+ limits = client_info[:limits]
535
+ return unless usage and limits
536
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
537
+ if mem_wanted > available_for_use
538
+ tmem = pretty_size(limits[:memory]*1024*1024)
539
+ mem = pretty_size(usage[:memory]*1024*1024)
540
+ display "Not enough capacity for operation.".yellow
541
+ available = pretty_size(available_for_use * 1024 * 1024)
542
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
543
+ end
544
+ end
545
+
546
+ def mem_choices
547
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
548
+
549
+ return default unless client_info
550
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
551
+
552
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
553
+ check_has_capacity_for(64) if available_for_use < 64
554
+ return ['64M'] if available_for_use < 128
555
+ return ['64M', '128M'] if available_for_use < 256
556
+ return ['64M', '128M', '256M'] if available_for_use < 512
557
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
558
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
559
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
560
+ end
561
+
562
+ def normalize_mem(mem)
563
+ return mem if /K|G|M/i =~ mem
564
+ "#{mem}M"
565
+ end
566
+
567
+ def mem_choice_to_quota(mem_choice)
568
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
569
+ mem_quota
570
+ end
571
+
572
+ def mem_quota_to_choice(mem)
573
+ if mem < 1024
574
+ mem_choice = "#{mem}M"
575
+ else
576
+ mem_choice = "#{(mem/1024).to_i}G"
577
+ end
578
+ mem_choice
579
+ end
580
+
581
+ def get_instances(appname)
582
+ instances_info_envelope = client.app_instances(appname)
583
+ # Empty array is returned if there are no instances running.
584
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
585
+
586
+ instances_info = instances_info_envelope[:instances] || []
587
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
588
+
589
+ return display JSON.pretty_generate(instances_info) if @options[:json]
590
+
591
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
592
+
593
+ instances_table = table do |t|
594
+ show_debug = instances_info.any? { |e| e[:debug_port] }
595
+
596
+ headings = ['Index', 'State', 'Start Time']
597
+ headings << 'Debug IP' if show_debug
598
+ headings << 'Debug Port' if show_debug
599
+
600
+ t.headings = headings
601
+
602
+ instances_info.each do |entry|
603
+ row = [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
604
+ row << entry[:debug_ip] if show_debug
605
+ row << entry[:debug_port] if show_debug
606
+ t << row
607
+ end
608
+ end
609
+ display "\n"
610
+ display instances_table
611
+ end
612
+
613
+ def change_instances(appname, instances)
614
+ app = client.app_info(appname)
615
+
616
+ match = instances.match(/([+-])?\d+/)
617
+ err "Invalid number of instances '#{instances}'" unless match
618
+
619
+ instances = instances.to_i
620
+ current_instances = app[:instances]
621
+ new_instances = match.captures[0] ? current_instances + instances : instances
622
+ err "There must be at least 1 instance." if new_instances < 1
623
+
624
+ if current_instances == new_instances
625
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
626
+ return
627
+ end
628
+
629
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
630
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
631
+ app[:instances] = new_instances
632
+ client.update_app(appname, app)
633
+ display 'OK'.green
634
+ end
635
+
636
+ def health(d)
637
+ return 'N/A' unless (d and d[:state])
638
+ return 'STOPPED' if d[:state] == 'STOPPED'
639
+
640
+ healthy_instances = d[:runningInstances]
641
+ expected_instance = d[:instances]
642
+ health = nil
643
+
644
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
645
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
646
+ end
647
+
648
+ return 'RUNNING' if health && health == 1.0
649
+ return "#{(health * 100).round}%" if health
650
+ return 'N/A'
651
+ end
652
+
653
+ def app_started_properly(appname, error_on_health)
654
+ app = client.app_info(appname)
655
+ case health(app)
656
+ when 'N/A'
657
+ # Health manager not running.
658
+ err "\nApplication '#{appname}'s state is undetermined, not enough information available." if error_on_health
659
+ return false
660
+ when 'RUNNING'
661
+ return true
662
+ else
663
+ if app[:meta][:debug] == "suspend"
664
+ display "\nApplication [#{appname}] has started in a mode that is waiting for you to trigger startup."
665
+ return true
666
+ else
667
+ return false
668
+ end
669
+ end
670
+ end
671
+
672
+ def display_logfile(path, content, instance='0', banner=nil)
673
+ banner ||= "====> #{path} <====\n\n"
674
+
675
+ unless content.empty?
676
+ display banner
677
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
678
+ unless prefix
679
+ display content
680
+ else
681
+ lines = content.split("\n")
682
+ lines.each { |line| display "#{prefix} #{line}"}
683
+ end
684
+ display ''
685
+ end
686
+ end
687
+
688
+ def grab_all_logs(appname)
689
+ instances_info_envelope = client.app_instances(appname)
690
+ return if instances_info_envelope.is_a?(Array)
691
+ instances_info = instances_info_envelope[:instances] || []
692
+ instances_info.each do |entry|
693
+ grab_logs(appname, entry[:index])
694
+ end
695
+ end
696
+
697
+ def grab_logs(appname, instance)
698
+ files_under(appname, instance, "/logs").each do |path|
699
+ begin
700
+ content = client.app_files(appname, path, instance)
701
+ display_logfile(path, content, instance)
702
+ rescue VMC::Client::NotFound, VMC::Client::TargetError
703
+ end
704
+ end
705
+ end
706
+
707
+ def files_under(appname, instance, path)
708
+ client.app_files(appname, path, instance).split("\n").collect do |l|
709
+ "#{path}/#{l.split[0]}"
710
+ end
711
+ rescue VMC::Client::NotFound, VMC::Client::TargetError
712
+ []
713
+ end
714
+
715
+ def grab_crash_logs(appname, instance, was_staged=false)
716
+ # stage crash info
717
+ crashes(appname, false) unless was_staged
718
+
719
+ instance ||= '0'
720
+ map = VMC::Cli::Config.instances
721
+ instance = map[instance] if map[instance]
722
+
723
+ (files_under(appname, instance, "/logs") +
724
+ files_under(appname, instance, "/app/logs") +
725
+ files_under(appname, instance, "/app/log")).each do |path|
726
+ content = client.app_files(appname, path, instance)
727
+ display_logfile(path, content, instance)
728
+ end
729
+ end
730
+
731
+ def grab_startup_tail(appname, since = 0)
732
+ new_lines = 0
733
+ path = "logs/startup.log"
734
+ content = client.app_files(appname, path)
735
+ if content && !content.empty?
736
+ display "\n==== displaying startup log ====\n\n" if since == 0
737
+ response_lines = content.split("\n")
738
+ lines = response_lines.size
739
+ tail = response_lines[since, lines] || []
740
+ new_lines = tail.size
741
+ display tail.join("\n") if new_lines > 0
742
+ end
743
+ since + new_lines
744
+ rescue VMC::Client::NotFound, VMC::Client::TargetError
745
+ 0
746
+ end
747
+
748
+ def provisioned_services_apps_hash
749
+ apps = client.apps
750
+ services_apps_hash = {}
751
+ apps.each {|app|
752
+ app[:services].each { |svc|
753
+ svc_apps = services_apps_hash[svc]
754
+ unless svc_apps
755
+ svc_apps = Set.new
756
+ services_apps_hash[svc] = svc_apps
757
+ end
758
+ svc_apps.add(app[:name])
759
+ } unless app[:services] == nil
760
+ }
761
+ services_apps_hash
762
+ end
763
+
764
+ def delete_app(appname, force)
765
+ app = client.app_info(appname)
766
+ services_to_delete = []
767
+ app_services = app[:services]
768
+ services_apps_hash = provisioned_services_apps_hash
769
+ app_services.each { |service|
770
+ del_service = force && no_prompt
771
+ unless no_prompt || force
772
+ del_service = ask(
773
+ "Provisioned service [#{service}] detected, would you like to delete it?",
774
+ :default => false
775
+ )
776
+
777
+ if del_service
778
+ apps_using_service = services_apps_hash[service].reject!{ |app| app == appname}
779
+ if apps_using_service.size > 0
780
+ del_service = ask(
781
+ "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?",
782
+ :default => false
783
+ )
784
+ end
785
+ end
786
+ end
787
+ services_to_delete << service if del_service
788
+ }
789
+
790
+ display "Deleting application [#{appname}]: ", false
791
+ client.delete_app(appname)
792
+ display 'OK'.green
793
+
794
+ services_to_delete.each do |s|
795
+ delete_service_banner(s)
796
+ end
797
+ end
798
+
799
+ def do_start(appname, push=false)
800
+ app = client.app_info(appname)
801
+ return display "Application '#{appname}' could not be found".red if app.nil?
802
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
803
+
804
+
805
+
806
+ if @options[:debug]
807
+ runtimes = client.runtimes_info
808
+ return display "Cannot get runtime information." unless runtimes
809
+
810
+ runtime = runtimes[app[:staging][:stack].to_sym]
811
+ return display "Unknown runtime." unless runtime
812
+
813
+ unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug]
814
+ modes = runtime[:debug_modes] || []
815
+
816
+ display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode"
817
+
818
+ if push
819
+ display "Try 'vmcu start' with one of the following modes: #{modes.inspect}"
820
+ else
821
+ display "Available modes: #{modes.inspect}"
822
+ end
823
+
824
+ return
825
+ end
826
+ end
827
+
828
+ banner = "Staging Application '#{appname}': "
829
+ display banner, false
830
+
831
+ t = Thread.new do
832
+ count = 0
833
+ while count < TAIL_TICKS do
834
+ display '.', false
835
+ sleep SLEEP_TIME
836
+ count += 1
837
+ end
838
+ end
839
+
840
+ app[:state] = 'STARTED'
841
+ app[:debug] = @options[:debug]
842
+ app[:console] = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model]).console
843
+ client.update_app(appname, app)
844
+
845
+ Thread.kill(t)
846
+ clear(LINE_LENGTH)
847
+ display "#{banner}#{'OK'.green}"
848
+
849
+ banner = "Starting Application '#{appname}': "
850
+ display banner, false
851
+
852
+ count = log_lines_displayed = 0
853
+ failed = false
854
+ start_time = Time.now.to_i
855
+
856
+ loop do
857
+ display '.', false unless count > TICKER_TICKS
858
+ sleep SLEEP_TIME
859
+
860
+ break if app_started_properly(appname, count > HEALTH_TICKS)
861
+
862
+ if !crashes(appname, false, start_time).empty?
863
+ # Check for the existance of crashes
864
+ display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
865
+ grab_crash_logs(appname, '0', true)
866
+ if push and !no_prompt
867
+ display "\n"
868
+ delete_app(appname, false) if ask "Delete the application?", :default => true
869
+ end
870
+ failed = true
871
+ break
872
+ elsif count > TAIL_TICKS
873
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
874
+ end
875
+
876
+ count += 1
877
+ if count > GIVEUP_TICKS # 2 minutes
878
+ display "\nApplication is taking too long to start, check your logs".yellow
879
+ break
880
+ end
881
+ end
882
+ exit(false) if failed
883
+ clear(LINE_LENGTH)
884
+ display "#{banner}#{'OK'.green}"
885
+ end
886
+
887
+ def do_stop(appname)
888
+ app = client.app_info(appname)
889
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
890
+ display "Stopping Application '#{appname}': ", false
891
+ app[:state] = 'STOPPED'
892
+ client.update_app(appname, app)
893
+ display 'OK'.green
894
+ end
895
+
896
+ def do_push(appname=nil)
897
+ unless @app_info || no_prompt
898
+ @manifest = { "applications" => { @path => { "name" => appname } } }
899
+
900
+ interact
901
+
902
+ if ask("Would you like to save this configuration?", :default => false)
903
+ save_manifest
904
+ end
905
+
906
+ resolve_manifest(@manifest)
907
+
908
+ @app_info = @manifest["applications"][@path]
909
+ end
910
+
911
+ instances = info(:instances, 1)
912
+ exec = info(:exec, 'thin start')
913
+
914
+ ignore_framework = @options[:noframework]
915
+ no_start = @options[:nostart]
916
+
917
+ appname ||= info(:name)
918
+ url = info(:url) || info(:urls)
919
+ mem, memswitch = nil, info(:mem)
920
+ memswitch = normalize_mem(memswitch) if memswitch
921
+ command = info(:command)
922
+ runtime = info(:runtime)
923
+ env = info(:env)
924
+
925
+ # Check app existing upfront if we have appname
926
+ app_checked = false
927
+ if appname
928
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
929
+ app_checked = true
930
+ else
931
+ raise VMC::Client::AuthError unless client.logged_in?
932
+ end
933
+
934
+ # check if we have hit our app limit
935
+ check_app_limit
936
+ # check memsize here for capacity
937
+ if memswitch && !no_start
938
+ check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
939
+ end
940
+
941
+ appname ||= ask("Application Name") unless no_prompt
942
+ err "Application Name required." if appname.nil? || appname.empty?
943
+
944
+ check_deploy_directory(@application)
945
+
946
+ if !app_checked and app_exists?(appname)
947
+ err "Application '#{appname}' already exists, use update or delete."
948
+ end
949
+
950
+ if ignore_framework
951
+ framework = VMC::Cli::Framework.new
952
+ elsif f = info(:framework)
953
+ info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }]
954
+
955
+ framework = VMC::Cli::Framework.create(f["name"], info)
956
+ exec = framework.exec if framework && framework.exec
957
+ else
958
+ framework = detect_framework(prompt_ok)
959
+ end
960
+
961
+ err "Application Type undetermined for path '#{@application}'" unless framework
962
+
963
+ if not runtime
964
+ default_runtime = framework.default_runtime @application
965
+ runtime = detect_runtime(default_runtime, !no_prompt) if framework.prompt_for_runtime?
966
+ end
967
+ command = ask("Start Command") if !command && framework.require_start_command?
968
+
969
+ default_url = "None"
970
+ default_url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if framework.require_url?
971
+
972
+
973
+ unless no_prompt || url || !framework.require_url?
974
+ url = ask(
975
+ "Application Deployed URL",
976
+ :default => default_url
977
+ )
978
+
979
+ # common error case is for prompted users to answer y or Y or yes or
980
+ # YES to this ask() resulting in an unintended URL of y. Special case
981
+ # this common error
982
+ url = nil if YES_SET.member? url
983
+ end
984
+ url = nil if url == "None"
985
+ default_url = nil if default_url == "None"
986
+ url ||= default_url
987
+
988
+ if memswitch
989
+ mem = memswitch
990
+ elsif prompt_ok
991
+ mem = ask("Memory Reservation",
992
+ :default => framework.memory(runtime),
993
+ :choices => mem_choices)
994
+ else
995
+ mem = framework.memory runtime
996
+ end
997
+
998
+ # Set to MB number
999
+ mem_quota = mem_choice_to_quota(mem)
1000
+
1001
+ # check memsize here for capacity
1002
+ check_has_capacity_for(mem_quota * instances) unless no_start
1003
+
1004
+ display 'Creating Application: ', false
1005
+
1006
+ manifest = {
1007
+ :name => "#{appname}",
1008
+ :staging => {
1009
+ :framework => framework.name,
1010
+ :runtime => runtime
1011
+ },
1012
+ :uris => Array(url),
1013
+ :instances => instances,
1014
+ :resources => {
1015
+ :memory => mem_quota
1016
+ },
1017
+ :env => Array(env)
1018
+ }
1019
+ manifest[:staging][:command] = command if command
1020
+
1021
+ # Send the manifest to the cloud controller
1022
+ client.create_app(appname, manifest)
1023
+ display 'OK'.green
1024
+
1025
+
1026
+ existing = Set.new(client.services.collect { |s| s[:name] })
1027
+
1028
+ if @app_info && services = @app_info["services"]
1029
+ services.each do |name, info|
1030
+ unless existing.include? name
1031
+ create_service_banner(info["type"], name, true)
1032
+ end
1033
+
1034
+ bind_service_banner(name, appname)
1035
+ end
1036
+ end
1037
+
1038
+ # Stage and upload the app bits.
1039
+ upload_app_bits(appname, @application)
1040
+
1041
+ start(appname, true) unless no_start
1042
+ end
1043
+
1044
+ def do_stats(appname)
1045
+ stats = client.app_stats(appname)
1046
+ return display JSON.pretty_generate(stats) if @options[:json]
1047
+
1048
+ stats_table = table do |t|
1049
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
1050
+ stats.each do |entry|
1051
+ index = entry[:instance]
1052
+ stat = entry[:stats]
1053
+ hp = "#{stat[:host]}:#{stat[:port]}"
1054
+ uptime = uptime_string(stat[:uptime])
1055
+ usage = stat[:usage]
1056
+ if usage
1057
+ cpu = usage[:cpu]
1058
+ mem = (usage[:mem] * 1024) # mem comes in K's
1059
+ disk = usage[:disk]
1060
+ end
1061
+ mem_quota = stat[:mem_quota]
1062
+ disk_quota = stat[:disk_quota]
1063
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
1064
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
1065
+ cpu = cpu ? cpu.to_s : 'NA'
1066
+ cpu = "#{cpu}% (#{stat[:cores]})"
1067
+ t << [index, cpu, mem, disk, uptime]
1068
+ end
1069
+ end
1070
+
1071
+ if stats.empty?
1072
+ display "No running instances for [#{appname}]".yellow
1073
+ else
1074
+ display stats_table
1075
+ end
1076
+ end
1077
+
1078
+ def all_files(appname, path)
1079
+ instances_info_envelope = client.app_instances(appname)
1080
+ return if instances_info_envelope.is_a?(Array)
1081
+ instances_info = instances_info_envelope[:instances] || []
1082
+ instances_info.each do |entry|
1083
+ begin
1084
+ content = client.app_files(appname, path, entry[:index])
1085
+ display_logfile(
1086
+ path,
1087
+ content,
1088
+ entry[:index],
1089
+ "====> [#{entry[:index]}: #{path}] <====\n".bold
1090
+ )
1091
+ rescue VMC::Client::NotFound, VMC::Client::TargetError
1092
+ end
1093
+ end
1094
+ end
1095
+ end
1096
+
1097
+ class FileWithPercentOutput < ::File
1098
+ class << self
1099
+ attr_accessor :display_str, :upload_size
1100
+ end
1101
+
1102
+ def update_display(rsize)
1103
+ @read ||= 0
1104
+ @read += rsize
1105
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
1106
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1107
+ clear(FileWithPercentOutput.display_str.size + 5)
1108
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
1109
+ VMC::Cli::Config.output.flush
1110
+ end
1111
+ end
1112
+
1113
+ def read(*args)
1114
+ result = super(*args)
1115
+ if result && result.size > 0
1116
+ update_display(result.size)
1117
+ else
1118
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
1119
+ clear(FileWithPercentOutput.display_str.size + 5)
1120
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
1121
+ display('OK'.green)
1122
+ end
1123
+ end
1124
+ result
1125
+ end
1126
+ end
1127
+
1128
+ end