af 0.3.12

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 (45) hide show
  1. data/LICENSE +24 -0
  2. data/README.md +92 -0
  3. data/Rakefile +17 -0
  4. data/bin/af +6 -0
  5. data/lib/cli.rb +30 -0
  6. data/lib/cli/commands/admin.rb +77 -0
  7. data/lib/cli/commands/apps.rb +940 -0
  8. data/lib/cli/commands/base.rb +79 -0
  9. data/lib/cli/commands/misc.rb +128 -0
  10. data/lib/cli/commands/services.rb +86 -0
  11. data/lib/cli/commands/user.rb +60 -0
  12. data/lib/cli/config.rb +110 -0
  13. data/lib/cli/core_ext.rb +119 -0
  14. data/lib/cli/errors.rb +19 -0
  15. data/lib/cli/frameworks.rb +109 -0
  16. data/lib/cli/runner.rb +490 -0
  17. data/lib/cli/services_helper.rb +78 -0
  18. data/lib/cli/usage.rb +104 -0
  19. data/lib/cli/version.rb +7 -0
  20. data/lib/cli/zip_util.rb +77 -0
  21. data/lib/vmc.rb +3 -0
  22. data/lib/vmc/client.rb +451 -0
  23. data/lib/vmc/const.rb +21 -0
  24. data/spec/assets/app_info.txt +9 -0
  25. data/spec/assets/app_listings.txt +9 -0
  26. data/spec/assets/bad_create_app.txt +9 -0
  27. data/spec/assets/delete_app.txt +9 -0
  28. data/spec/assets/global_service_listings.txt +9 -0
  29. data/spec/assets/good_create_app.txt +9 -0
  30. data/spec/assets/good_create_service.txt +9 -0
  31. data/spec/assets/info_authenticated.txt +27 -0
  32. data/spec/assets/info_return.txt +15 -0
  33. data/spec/assets/info_return_bad.txt +16 -0
  34. data/spec/assets/list_users.txt +13 -0
  35. data/spec/assets/login_fail.txt +9 -0
  36. data/spec/assets/login_success.txt +9 -0
  37. data/spec/assets/sample_token.txt +1 -0
  38. data/spec/assets/service_already_exists.txt +9 -0
  39. data/spec/assets/service_listings.txt +9 -0
  40. data/spec/assets/service_not_found.txt +9 -0
  41. data/spec/assets/user_info.txt +9 -0
  42. data/spec/spec_helper.rb +11 -0
  43. data/spec/unit/cli_opts_spec.rb +68 -0
  44. data/spec/unit/client_spec.rb +332 -0
  45. metadata +221 -0
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2010-2011 VMware Inc, All Rights Reserved
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
21
+ This software downloads additional open source software components upon install
22
+ that are distributed under separate terms and conditions. Please see the license
23
+ information provided in the individual software components for more information.
24
+
@@ -0,0 +1,92 @@
1
+ # af
2
+
3
+ The AppFog.com CLI. This is the command line interface to AppFog's Application Platform
4
+
5
+ af is based on vmc but will have features specific to the AppFog service as well as having the default target set to AppFog's service
6
+
7
+ _Copyright 2010-2011, VMware, Inc. Licensed under the
8
+ MIT license, please see the LICENSE file. All rights reserved._
9
+
10
+ Usage: af [options] command [<args>] [command_options]
11
+ Try 'af help [command]' or 'af help options' for more information.
12
+
13
+ Currently available af commands are:
14
+
15
+ Getting Started
16
+ target [url] Reports current target or sets a new target
17
+ login [email] [--email, --passwd] Login
18
+ info System and account information
19
+
20
+ Applications
21
+ apps List deployed applications
22
+
23
+ Application Creation
24
+ push [appname] Create, push, map, and start a new application
25
+ push [appname] --path Push application from specified path
26
+ push [appname] --url Set the url for the application
27
+ push [appname] --instances <N> Set the expected number <N> of instances
28
+ push [appname] --mem M Set the memory reservation for the application
29
+ push [appname] --no-start Do not auto-start the application
30
+
31
+ Application Operations
32
+ start <appname> Start the application
33
+ stop <appname> Stop the application
34
+ restart <appname> Restart the application
35
+ delete <appname> Delete the application
36
+ rename <appname> <newname> Rename the application
37
+
38
+ Application Updates
39
+ update <appname> [--path] Update the application bits
40
+ mem <appname> [memsize] Update the memory reservation for an application
41
+ map <appname> <url> Register the application to the url
42
+ unmap <appname> <url> Unregister the application from the url
43
+ instances <appname> <num|delta> Scale the application instances up or down
44
+
45
+ Application Information
46
+ crashes <appname> List recent application crashes
47
+ crashlogs <appname> Display log information for crashed applications
48
+ logs <appname> [--all] Display log information for the application
49
+ files <appname> [path] [--all] Display directory listing or file download for path
50
+ stats <appname> Display resource usage for the application
51
+ instances <appname> List application instances
52
+
53
+ Application Environment
54
+ env <appname> List application environment variables
55
+ env-add <appname> <variable[=]value> Add an environment variable to an application
56
+ env-del <appname> <variable> Delete an environment variable to an application
57
+
58
+ Services
59
+ services Lists of services available and provisioned
60
+ create-service <service> [--name,--bind] Create a provisioned service
61
+ create-service <service> <name> Create a provisioned service and assign it <name>
62
+ create-service <service> <name> <app> Create a provisioned service and assign it <name>, and bind to <app>
63
+ delete-service [servicename] Delete a provisioned service
64
+ bind-service <servicename> <appname> Bind a service to an application
65
+ unbind-service <servicename> <appname> Unbind service from the application
66
+ clone-services <src-app> <dest-app> Clone service bindings from <src-app> application to <dest-app>
67
+
68
+ Administration
69
+ user Display user account information
70
+ passwd Change the password for the current user
71
+ logout Logs current user out of the target system
72
+ add-user [--email, --passwd] Register a new user (requires admin privileges)
73
+ delete-user <user> Delete a user and all apps and services (requires admin privileges)
74
+
75
+ System
76
+ runtimes Display the supported runtimes of the target system
77
+ frameworks Display the recognized frameworks of the target system
78
+
79
+ Misc
80
+ aliases List aliases
81
+ alias <alias[=]command> Create an alias for a command
82
+ unalias <alias> Remove an alias
83
+ targets List known targets and associated authorization tokens
84
+
85
+ Help
86
+ help [command] Get general help or help on a specific command
87
+ help options Get help on available options
88
+
89
+ ## Simple Story (for PHP apps)
90
+
91
+ af login
92
+ af push
@@ -0,0 +1,17 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run specs"
5
+ task :spec do
6
+ sh('bundle install')
7
+ Spec::Rake::SpecTask.new('spec') do |t|
8
+ t.spec_opts = %w(-fs -c)
9
+ t.spec_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+ end
12
+
13
+ desc "Synonym for spec"
14
+ task :test => :spec
15
+ desc "Synonym for spec"
16
+ task :tests => :spec
17
+ task :default => :spec
data/bin/af ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/cli', __FILE__)
4
+
5
+ VMC::Cli::Runner.run(ARGV.dup)
6
+
@@ -0,0 +1,30 @@
1
+
2
+ ROOT = File.expand_path(File.dirname(__FILE__))
3
+
4
+ module VMC
5
+
6
+ autoload :Client, "#{ROOT}/vmc/client"
7
+
8
+ module Cli
9
+
10
+ autoload :Config, "#{ROOT}/cli/config"
11
+ autoload :Framework, "#{ROOT}/cli/frameworks"
12
+ autoload :Runner, "#{ROOT}/cli/runner"
13
+ autoload :ZipUtil, "#{ROOT}/cli/zip_util"
14
+ autoload :ServicesHelper, "#{ROOT}/cli/services_helper"
15
+
16
+ module Command
17
+ autoload :Base, "#{ROOT}/cli/commands/base"
18
+ autoload :Admin, "#{ROOT}/cli/commands/admin"
19
+ autoload :Apps, "#{ROOT}/cli/commands/apps"
20
+ autoload :Misc, "#{ROOT}/cli/commands/misc"
21
+ autoload :Services, "#{ROOT}/cli/commands/services"
22
+ autoload :User, "#{ROOT}/cli/commands/user"
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ require "#{ROOT}/cli/version"
29
+ require "#{ROOT}/cli/core_ext"
30
+ require "#{ROOT}/cli/errors"
@@ -0,0 +1,77 @@
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] unless email
26
+ password = @options[:password]
27
+ email = ask("Email: ") unless no_prompt || email
28
+ unless no_prompt || password
29
+ password = ask("Password: ") {|q| q.echo = '*'}
30
+ password2 = ask("Verify Password: ") {|q| q.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("\nDeployed applications and associated services will be DELETED, continue? [yN]: ")
57
+ err "Aborted" if proceed.upcase != 'Y'
58
+ end
59
+ cmd = Apps.new(@options)
60
+ apps.each { |app| cmd.delete_app(app[:name], true) }
61
+ end
62
+
63
+ services = client.services
64
+ if (services && !services.empty?)
65
+ cmd = Services.new(@options)
66
+ services.each { |s| cmd.delete_service(s[:name])}
67
+ end
68
+
69
+ display 'Deleting User: ', false
70
+ client.proxy = nil
71
+ client.delete_user(user_email)
72
+ display 'OK'.green
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,940 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'tmpdir'
5
+ require 'set'
6
+
7
+ module VMC::Cli::Command
8
+
9
+ class Apps < Base
10
+ include VMC::Cli::ServicesHelper
11
+
12
+ def list
13
+ apps = client.apps
14
+ apps.sort! {|a, b| a[:name] <=> b[:name] }
15
+ return display JSON.pretty_generate(apps || []) if @options[:json]
16
+
17
+ display "\n"
18
+ return display "No Applications" if apps.nil? || apps.empty?
19
+
20
+ apps_table = table do |t|
21
+ t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
22
+ apps.each do |app|
23
+ t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
24
+ end
25
+ end
26
+ display apps_table
27
+ end
28
+
29
+ alias :apps :list
30
+
31
+ SLEEP_TIME = 1
32
+ LINE_LENGTH = 80
33
+
34
+ # Numerators are in secs
35
+ TICKER_TICKS = 25/SLEEP_TIME
36
+ HEALTH_TICKS = 5/SLEEP_TIME
37
+ TAIL_TICKS = 45/SLEEP_TIME
38
+ GIVEUP_TICKS = 120/SLEEP_TIME
39
+ YES_SET = Set.new(["y", "Y", "yes", "YES"])
40
+
41
+ def start(appname, push = false)
42
+ app = client.app_info(appname)
43
+
44
+ return display "Application '#{appname}' could not be found".red if app.nil?
45
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
46
+
47
+ banner = 'Staging Application: '
48
+ display banner, false
49
+
50
+ t = Thread.new do
51
+ count = 0
52
+ while count < TAIL_TICKS do
53
+ display '.', false
54
+ sleep SLEEP_TIME
55
+ count += 1
56
+ end
57
+ end
58
+
59
+ app[:state] = 'STARTED'
60
+ client.update_app(appname, app)
61
+
62
+ Thread.kill(t)
63
+ clear(LINE_LENGTH)
64
+ display "#{banner}#{'OK'.green}"
65
+
66
+ banner = 'Starting Application: '
67
+ display banner, false
68
+
69
+ count = log_lines_displayed = 0
70
+ failed = false
71
+ start_time = Time.now.to_i
72
+
73
+ loop do
74
+ display '.', false unless count > TICKER_TICKS
75
+ sleep SLEEP_TIME
76
+ begin
77
+ break if app_started_properly(appname, count > HEALTH_TICKS)
78
+ if !crashes(appname, false, start_time).empty?
79
+ # Check for the existance of crashes
80
+ display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
81
+ grab_crash_logs(appname, '0', true)
82
+ if push
83
+ display "\n"
84
+ should_delete = ask 'Should I delete the application? (Y/n)? ' unless no_prompt
85
+ delete_app(appname, false) unless no_prompt || should_delete.upcase == 'N'
86
+ end
87
+ failed = true
88
+ break
89
+ elsif count > TAIL_TICKS
90
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
91
+ end
92
+ rescue => e
93
+ err(e.message, '')
94
+ end
95
+ count += 1
96
+ if count > GIVEUP_TICKS # 2 minutes
97
+ display "\nApplication is taking too long to start, check your logs".yellow
98
+ break
99
+ end
100
+ end
101
+ exit(false) if failed
102
+ clear(LINE_LENGTH)
103
+ display "#{banner}#{'OK'.green}"
104
+ end
105
+
106
+ def stop(appname)
107
+ app = client.app_info(appname)
108
+ return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
109
+ display 'Stopping Application: ', false
110
+ app[:state] = 'STOPPED'
111
+ client.update_app(appname, app)
112
+ display 'OK'.green
113
+ end
114
+
115
+ def restart(appname)
116
+ stop(appname)
117
+ start(appname)
118
+ end
119
+
120
+ def rename(appname, newname)
121
+ app = client.app_info(appname)
122
+ app[:name] = newname
123
+ display 'Renaming Appliction: '
124
+ client.update_app(newname, app)
125
+ display 'OK'.green
126
+ end
127
+
128
+ def mem(appname, memsize=nil)
129
+ app = client.app_info(appname)
130
+ mem = current_mem = mem_quota_to_choice(app[:resources][:memory])
131
+ memsize = normalize_mem(memsize) if memsize
132
+
133
+ unless memsize
134
+ choose do |menu|
135
+ menu.layout = :one_line
136
+ menu.prompt = "Update Memory Reservation? [Current:#{current_mem}] "
137
+ menu.default = current_mem
138
+ mem_choices.each { |choice| menu.choice(choice) { memsize = choice } }
139
+ end
140
+ end
141
+
142
+ mem = mem_choice_to_quota(mem)
143
+ memsize = mem_choice_to_quota(memsize)
144
+ current_mem = mem_choice_to_quota(current_mem)
145
+
146
+ display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
147
+
148
+ # check memsize here for capacity
149
+ check_has_capacity_for((memsize - mem) * app[:instances])
150
+
151
+ mem = memsize
152
+
153
+ if (mem != current_mem)
154
+ app[:resources][:memory] = mem
155
+ client.update_app(appname, app)
156
+ display 'OK'.green
157
+ restart appname if app[:state] == 'STARTED'
158
+ else
159
+ display 'OK'.green
160
+ end
161
+ end
162
+
163
+ def map(appname, url)
164
+ app = client.app_info(appname)
165
+ uris = app[:uris] || []
166
+ uris << url
167
+ app[:uris] = uris
168
+ client.update_app(appname, app)
169
+ display "Succesfully mapped url".green
170
+ end
171
+
172
+ def unmap(appname, url)
173
+ app = client.app_info(appname)
174
+ uris = app[:uris] || []
175
+ url = url.gsub(/^http(s*):\/\//i, '')
176
+ deleted = uris.delete(url)
177
+ err "Invalid url" unless deleted
178
+ app[:uris] = uris
179
+ client.update_app(appname, app)
180
+ display "Succesfully unmapped url".green
181
+
182
+ end
183
+
184
+ def delete(appname=nil)
185
+ force = @options[:force]
186
+ if @options[:all]
187
+ should_delete = force && no_prompt ? 'Y' : 'N'
188
+ unless no_prompt || force
189
+ should_delete = ask 'Delete ALL Applications and Services? (y/N)? '
190
+ end
191
+ if should_delete.upcase == 'Y'
192
+ apps = client.apps
193
+ apps.each { |app| delete_app(app[:name], force) }
194
+ end
195
+ else
196
+ err 'No valid appname given' unless appname
197
+ delete_app(appname, force)
198
+ end
199
+ end
200
+
201
+ def delete_app(appname, force)
202
+ app = client.app_info(appname)
203
+ services_to_delete = []
204
+ app_services = app[:services]
205
+ app_services.each { |service|
206
+ del_service = force && no_prompt ? 'Y' : 'N'
207
+ unless no_prompt || force
208
+ del_service = ask("Provisioned service [#{service}] detected, would you like to delete it? [yN]: ")
209
+ end
210
+ services_to_delete << service if del_service.upcase == 'Y'
211
+ }
212
+ display "Deleting application [#{appname}]: ", false
213
+ client.delete_app(appname)
214
+ display 'OK'.green
215
+
216
+ services_to_delete.each do |s|
217
+ display "Deleting service [#{s}]: ", false
218
+ client.delete_service(s)
219
+ display 'OK'.green
220
+ end
221
+ end
222
+
223
+ def all_files(appname, path)
224
+ instances_info_envelope = client.app_instances(appname)
225
+ return if instances_info_envelope.is_a?(Array)
226
+ instances_info = instances_info_envelope[:instances] || []
227
+ instances_info.each do |entry|
228
+ content = client.app_files(appname, path, entry[:index])
229
+ display_logfile(path, content, entry[:index], "====> [#{entry[:index]}: #{path}] <====\n".bold)
230
+ end
231
+ end
232
+
233
+ def files(appname, path='/')
234
+ return all_files(appname, path) if @options[:all] && !@options[:instance]
235
+ instance = @options[:instance] || '0'
236
+ content = client.app_files(appname, path, instance)
237
+ display content
238
+ rescue VMC::Client::NotFound => e
239
+ err 'No such file or directory'
240
+ end
241
+
242
+ def logs(appname)
243
+ return grab_all_logs(appname) if @options[:all] && !@options[:instance]
244
+ instance = @options[:instance] || '0'
245
+ grab_logs(appname, instance)
246
+ end
247
+
248
+ def crashes(appname, print_results=true, since=0)
249
+ crashed = client.app_crashes(appname)[:crashes]
250
+ crashed.delete_if { |c| c[:since] < since }
251
+ instance_map = {}
252
+
253
+ # return display JSON.pretty_generate(apps) if @options[:json]
254
+
255
+
256
+ counter = 0
257
+ crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
258
+ crashed_table = table do |t|
259
+ t.headings = 'Name', 'Instance ID', 'Crashed Time'
260
+ crashed.each do |crash|
261
+ name = "#{appname}-#{counter += 1}"
262
+ instance_map[name] = crash[:instance]
263
+ t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
264
+ end
265
+ end
266
+
267
+ VMC::Cli::Config.store_instances(instance_map)
268
+
269
+ if @options[:json]
270
+ return display JSON.pretty_generate(crashed)
271
+ elsif print_results
272
+ display "\n"
273
+ if crashed.empty?
274
+ display "No crashed instances for [#{appname}]" if print_results
275
+ else
276
+ display crashed_table if print_results
277
+ end
278
+ end
279
+
280
+ crashed
281
+ end
282
+
283
+ def crashlogs(appname)
284
+ instance = @options[:instance] || '0'
285
+ grab_crash_logs(appname, instance)
286
+ end
287
+
288
+ def instances(appname, num=nil)
289
+ if (num)
290
+ change_instances(appname, num)
291
+ else
292
+ get_instances(appname)
293
+ end
294
+ end
295
+
296
+ def stats(appname)
297
+ stats = client.app_stats(appname)
298
+ return display JSON.pretty_generate(stats) if @options[:json]
299
+
300
+ stats_table = table do |t|
301
+ t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
302
+ stats.each do |entry|
303
+ index = entry[:instance]
304
+ stat = entry[:stats]
305
+ hp = "#{stat[:host]}:#{stat[:port]}"
306
+ uptime = uptime_string(stat[:uptime])
307
+ usage = stat[:usage]
308
+ if usage
309
+ cpu = usage[:cpu]
310
+ mem = (usage[:mem] * 1024) # mem comes in K's
311
+ disk = usage[:disk]
312
+ end
313
+ mem_quota = stat[:mem_quota]
314
+ disk_quota = stat[:disk_quota]
315
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
316
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
317
+ cpu = cpu ? cpu.to_s : 'NA'
318
+ cpu = "#{cpu}% (#{stat[:cores]})"
319
+ t << [index, cpu, mem, disk, uptime]
320
+ end
321
+ end
322
+ display "\n"
323
+ if stats.empty?
324
+ display "No running instances for [#{appname}]".yellow
325
+ else
326
+ display stats_table
327
+ end
328
+ end
329
+
330
+ def update(appname)
331
+ app = client.app_info(appname)
332
+ if @options[:canary]
333
+ display "[--canary] is deprecated and will be removed in a future version".yellow
334
+ end
335
+ path = @options[:path] || '.'
336
+ upload_app_bits(appname, path)
337
+ restart appname if app[:state] == 'STARTED'
338
+ end
339
+
340
+ def push(appname=nil)
341
+ instances = @options[:instances] || 1
342
+ exec = @options[:exec] || 'thin start'
343
+ ignore_framework = @options[:noframework]
344
+ no_start = @options[:nostart]
345
+
346
+ path = @options[:path] || '.'
347
+ appname = @options[:name] unless appname
348
+ url = @options[:url]
349
+ mem, memswitch = nil, @options[:mem]
350
+ memswitch = normalize_mem(memswitch) if memswitch
351
+
352
+ # Check app existing upfront if we have appname
353
+ app_checked = false
354
+ if appname
355
+ err "Application '#{appname}' already exists, use update" if app_exists?(appname)
356
+ app_checked = true
357
+ else
358
+ raise VMC::Client::AuthError unless client.logged_in?
359
+ end
360
+
361
+ # check if we have hit our app limit
362
+ check_app_limit
363
+
364
+ # check memsize here for capacity
365
+ if memswitch && !no_start
366
+ check_has_capacity_for(mem_choice_to_quota(memswitch) * instances)
367
+ end
368
+
369
+ unless no_prompt || @options[:path]
370
+ proceed = ask('Would you like to deploy from the current directory? [Yn]: ')
371
+ if proceed.upcase == 'N'
372
+ path = ask('Please enter in the deployment path: ')
373
+ end
374
+ end
375
+
376
+ path = File.expand_path(path)
377
+ check_deploy_directory(path)
378
+
379
+ appname = ask("Application Name: ") unless no_prompt || appname
380
+ err "Application Name required." if appname.nil? || appname.empty?
381
+
382
+ unless app_checked
383
+ err "Application '#{appname}' already exists, use update or delete." if app_exists?(appname)
384
+ end
385
+
386
+ unless no_prompt || url
387
+ url = ask("Application Deployed URL: '#{appname}.#{VMC::Cli::Config.suggest_url}'? ")
388
+
389
+ # common error case is for prompted users to answer y or Y or yes or YES to this ask() resulting in an
390
+ # unintended URL of y. Special case this common error
391
+ if YES_SET.member?(url)
392
+ #silently revert to the stock url
393
+ url = "#{appname}.#{VMC::Cli::Config.suggest_url}"
394
+ end
395
+ end
396
+
397
+ url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if url.nil? || url.empty?
398
+
399
+ # Detect the appropriate framework.
400
+ framework = nil
401
+ unless ignore_framework
402
+ framework = VMC::Cli::Framework.detect(path)
403
+ framework_correct = ask("Detected a #{framework}, is this correct? [Yn]: ") if prompt_ok && framework
404
+ framework_correct ||= 'y'
405
+ if prompt_ok && (framework.nil? || framework_correct.upcase == 'N')
406
+ display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
407
+ framework = nil if framework_correct.upcase == 'N'
408
+ choose do |menu|
409
+ menu.layout = :one_line
410
+ menu.prompt = "Select Application Type: "
411
+ menu.default = framework
412
+ VMC::Cli::Framework.known_frameworks.each do |f|
413
+ menu.choice(f) { framework = VMC::Cli::Framework.lookup(f) }
414
+ end
415
+ end
416
+ display "Selected #{framework}"
417
+ end
418
+ # Framework override, deprecated
419
+ exec = framework.exec if framework && framework.exec
420
+ else
421
+ framework = VMC::Cli::Framework.new
422
+ end
423
+
424
+ err "Application Type undetermined for path '#{path}'" unless framework
425
+ unless memswitch
426
+ mem = framework.memory
427
+ if prompt_ok
428
+ choose do |menu|
429
+ menu.layout = :one_line
430
+ menu.prompt = "Memory Reservation [Default:#{mem}] "
431
+ menu.default = mem
432
+ mem_choices.each { |choice| menu.choice(choice) { mem = choice } }
433
+ end
434
+ end
435
+ else
436
+ mem = memswitch
437
+ end
438
+
439
+ # Set to MB number
440
+ mem_quota = mem_choice_to_quota(mem)
441
+
442
+ # check memsize here for capacity
443
+ check_has_capacity_for(mem_quota * instances) unless no_start
444
+
445
+ display 'Creating Application: ', false
446
+
447
+ manifest = {
448
+ :name => "#{appname}",
449
+ :staging => {
450
+ :framework => framework.name,
451
+ :runtime => @options[:runtime]
452
+ },
453
+ :uris => [url],
454
+ :instances => instances,
455
+ :resources => {
456
+ :memory => mem_quota
457
+ },
458
+ }
459
+
460
+ # Send the manifest to the cloud controller
461
+ client.create_app(appname, manifest)
462
+ display 'OK'.green
463
+
464
+ # Services check
465
+ unless no_prompt || @options[:noservices]
466
+ services = client.services_info
467
+ unless services.empty?
468
+ proceed = ask("Would you like to bind any services to '#{appname}'? [yN]: ")
469
+ bind_services(appname, services) if proceed.upcase == 'Y'
470
+ end
471
+ end
472
+
473
+ # Stage and upload the app bits.
474
+ upload_app_bits(appname, path)
475
+
476
+ start(appname, true) unless no_start
477
+ end
478
+
479
+ def environment(appname)
480
+ app = client.app_info(appname)
481
+ env = app[:env] || []
482
+ return display JSON.pretty_generate(env) if @options[:json]
483
+ return display "No Environment Variables" if env.empty?
484
+ etable = table do |t|
485
+ t.headings = 'Variable', 'Value'
486
+ env.each do |e|
487
+ k,v = e.split('=', 2)
488
+ t << [k, v]
489
+ end
490
+ end
491
+ display "\n"
492
+ display etable
493
+ end
494
+
495
+ def environment_add(appname, k, v=nil)
496
+ app = client.app_info(appname)
497
+ env = app[:env] || []
498
+ k,v = k.split('=', 2) unless v
499
+ env << "#{k}=#{v}"
500
+ display "Adding Environment Variable [#{k}=#{v}]: ", false
501
+ app[:env] = env
502
+ client.update_app(appname, app)
503
+ display 'OK'.green
504
+ restart appname if app[:state] == 'STARTED'
505
+ end
506
+
507
+ def environment_del(appname, variable)
508
+ app = client.app_info(appname)
509
+ env = app[:env] || []
510
+ deleted_env = nil
511
+ env.each do |e|
512
+ k,v = e.split('=')
513
+ if (k == variable)
514
+ deleted_env = e
515
+ break;
516
+ end
517
+ end
518
+ display "Deleting Environment Variable [#{variable}]: ", false
519
+ if deleted_env
520
+ env.delete(deleted_env)
521
+ app[:env] = env
522
+ client.update_app(appname, app)
523
+ display 'OK'.green
524
+ restart appname if app[:state] == 'STARTED'
525
+ else
526
+ display 'OK'.green
527
+ end
528
+ end
529
+
530
+ private
531
+
532
+ def app_exists?(appname)
533
+ app_info = client.app_info(appname)
534
+ app_info != nil
535
+ rescue VMC::Client::NotFound
536
+ false
537
+ end
538
+
539
+ def check_deploy_directory(path)
540
+ err 'Deployment path does not exist' unless File.exists? path
541
+ err 'Deployment path is not a directory' unless File.directory? path
542
+ return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
543
+ err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
544
+ end
545
+
546
+ def upload_app_bits(appname, path)
547
+ display 'Uploading Application:'
548
+
549
+ upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
550
+ FileUtils.rm_f(upload_file)
551
+
552
+ explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
553
+ FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
554
+
555
+ Dir.chdir(path) do
556
+ # Stage the app appropriately and do the appropriate fingerprinting, etc.
557
+ if war_file = Dir.glob('*.war').first
558
+ VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
559
+ else
560
+ FileUtils.mkdir(explode_dir)
561
+ files = Dir.glob('{*,.[^\.]*}')
562
+ # Do not process .git files
563
+ files.delete('.git') if files
564
+ FileUtils.cp_r(files, explode_dir)
565
+ end
566
+
567
+ # Send the resource list to the cloudcontroller, the response will tell us what it already has..
568
+ unless @options[:noresources]
569
+ display ' Checking for available resources: ', false
570
+ fingerprints = []
571
+ total_size = 0
572
+ resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
573
+ resource_files.each do |filename|
574
+ next if (File.directory?(filename) || !File.exists?(filename))
575
+ fingerprints << {
576
+ :size => File.size(filename),
577
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
578
+ :fn => filename
579
+ }
580
+ total_size += File.size(filename)
581
+ end
582
+
583
+ # Check to see if the resource check is worth the round trip
584
+ if (total_size > (64*1024)) # 64k for now
585
+ # Send resource fingerprints to the cloud controller
586
+ appcloud_resources = client.check_resources(fingerprints)
587
+ end
588
+ display 'OK'.green
589
+
590
+ if appcloud_resources
591
+ display ' Processing resources: ', false
592
+ # We can then delete what we do not need to send.
593
+ appcloud_resources.each do |resource|
594
+ FileUtils.rm_f resource[:fn]
595
+ # adjust filenames sans the explode_dir prefix
596
+ resource[:fn].sub!("#{explode_dir}/", '')
597
+ end
598
+ display 'OK'.green
599
+ end
600
+
601
+ end
602
+
603
+ # Perform Packing of the upload bits here.
604
+ unless VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
605
+ display ' Packing application: ', false
606
+ VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
607
+ display 'OK'.green
608
+
609
+ upload_size = File.size(upload_file);
610
+ if upload_size > 1024*1024
611
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
612
+ elsif upload_size > 0
613
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
614
+ end
615
+ else
616
+ upload_size = '0K'
617
+ end
618
+
619
+ upload_str = " Uploading (#{upload_size}): "
620
+ display upload_str, false
621
+
622
+ unless VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
623
+ FileWithPercentOutput.display_str = upload_str
624
+ FileWithPercentOutput.upload_size = File.size(upload_file);
625
+ file = FileWithPercentOutput.open(upload_file, 'rb')
626
+ end
627
+
628
+ client.upload_app(appname, file, appcloud_resources)
629
+ display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
630
+
631
+ display 'Push Status: ', false
632
+ display 'OK'.green
633
+ end
634
+
635
+ ensure
636
+ # Cleanup if we created an exploded directory.
637
+ FileUtils.rm_f(upload_file) if upload_file
638
+ FileUtils.rm_rf(explode_dir) if explode_dir
639
+ end
640
+
641
+ def choose_existing_service(appname, user_services)
642
+ return unless prompt_ok
643
+ selected = false
644
+ choose do |menu|
645
+ menu.header = "The following provisioned services are available"
646
+ menu.prompt = 'Please select one you wish to provision: '
647
+ menu.select_by = :index_or_name
648
+ user_services.each do |s|
649
+ menu.choice(s[:name]) do
650
+ display "Binding Service: ", false
651
+ client.bind_service(s[:name], appname)
652
+ display 'OK'.green
653
+ selected = true
654
+ end
655
+ end
656
+ end
657
+ selected
658
+ end
659
+
660
+ def choose_new_service(appname, services)
661
+ return unless prompt_ok
662
+ choose do |menu|
663
+ menu.header = "The following system services are available"
664
+ menu.prompt = 'Please select one you wish to provision: '
665
+ menu.select_by = :index_or_name
666
+ service_choices = []
667
+ services.each do |service_type, value|
668
+ value.each do |vendor, version|
669
+ service_choices << vendor
670
+ end
671
+ end
672
+ service_choices.sort! {|a, b| a.to_s <=> b.to_s }
673
+ service_choices.each do |vendor|
674
+ menu.choice(vendor) do
675
+ default_name = random_service_name(vendor)
676
+ service_name = ask("Specify the name of the service [#{default_name}]: ")
677
+ service_name = default_name if service_name.empty?
678
+ create_service_banner(vendor, service_name)
679
+ bind_service_banner(service_name, appname)
680
+ end
681
+ end
682
+ end
683
+ end
684
+
685
+ def bind_services(appname, services)
686
+ user_services = client.services
687
+ selected_existing = false
688
+ unless no_prompt || user_services.empty?
689
+ use_existing = ask "Would you like to use an existing provisioned service [yN]? "
690
+ if use_existing.upcase == 'Y'
691
+ selected_existing = choose_existing_service(appname, user_services)
692
+ end
693
+ end
694
+ # Create a new service and bind it here
695
+ unless selected_existing
696
+ choose_new_service(appname, services)
697
+ end
698
+ end
699
+
700
+ def check_app_limit
701
+ usage = client_info[:usage]
702
+ limits = client_info[:limits]
703
+ return unless usage and limits and limits[:apps]
704
+ if limits[:apps] == usage[:apps]
705
+ display "Not enough capacity for operation.".red
706
+ tapps = limits[:apps] || 0
707
+ apps = usage[:apps] || 0
708
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
709
+ end
710
+ end
711
+
712
+ def check_has_capacity_for(mem_wanted)
713
+ usage = client_info[:usage]
714
+ limits = client_info[:limits]
715
+ return unless usage and limits
716
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
717
+ if mem_wanted > available_for_use
718
+ tmem = pretty_size(limits[:memory]*1024*1024)
719
+ mem = pretty_size(usage[:memory]*1024*1024)
720
+ display "Not enough capacity for operation.".yellow
721
+ available = pretty_size(available_for_use * 1024 * 1024)
722
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
723
+ end
724
+ end
725
+
726
+ def mem_choices
727
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
728
+
729
+ return default unless client_info
730
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
731
+
732
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
733
+ check_has_capacity_for(64) if available_for_use < 64
734
+ return ['64M'] if available_for_use < 128
735
+ return ['64M', '128M'] if available_for_use < 256
736
+ return ['64M', '128M', '256M'] if available_for_use < 512
737
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
738
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
739
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
740
+ end
741
+
742
+ def normalize_mem(mem)
743
+ return mem if /K|G|M/i =~ mem
744
+ "#{mem}M"
745
+ end
746
+
747
+ def mem_choice_to_quota(mem_choice)
748
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
749
+ mem_quota
750
+ end
751
+
752
+ def mem_quota_to_choice(mem)
753
+ if mem < 1024
754
+ mem_choice = "#{mem}M"
755
+ else
756
+ mem_choice = "#{(mem/1024).to_i}G"
757
+ end
758
+ mem_choice
759
+ end
760
+
761
+ def get_instances(appname)
762
+ instances_info_envelope = client.app_instances(appname)
763
+ # Empty array is returned if there are no instances running.
764
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
765
+
766
+ instances_info = instances_info_envelope[:instances] || []
767
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
768
+
769
+ return display JSON.pretty_generate(instances_info) if @options[:json]
770
+
771
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
772
+
773
+ instances_table = table do |t|
774
+ t.headings = 'Index', 'State', 'Start Time'
775
+ instances_info.each do |entry|
776
+ t << [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
777
+ end
778
+ end
779
+ display "\n"
780
+ display instances_table
781
+ end
782
+
783
+ def change_instances(appname, instances)
784
+ app = client.app_info(appname)
785
+
786
+ match = instances.match(/([+-])?\d+/)
787
+ err "Invalid number of instances '#{instances}'" unless match
788
+
789
+ instances = instances.to_i
790
+ current_instances = app[:instances]
791
+ new_instances = match.captures[0] ? current_instances + instances : instances
792
+ err "There must be at least 1 instance." if new_instances < 1
793
+
794
+ if current_instances == new_instances
795
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
796
+ return
797
+ end
798
+
799
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
800
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
801
+ app[:instances] = new_instances
802
+ client.update_app(appname, app)
803
+ display 'OK'.green
804
+ end
805
+
806
+ def health(d)
807
+ return 'N/A' unless (d and d[:state])
808
+ return 'STOPPED' if d[:state] == 'STOPPED'
809
+
810
+ healthy_instances = d[:runningInstances]
811
+ expected_instance = d[:instances]
812
+ health = nil
813
+
814
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
815
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
816
+ end
817
+
818
+ return 'RUNNING' if health && health == 1.0
819
+ return "#{(health * 100).round}%" if health
820
+ return 'N/A'
821
+ end
822
+
823
+ def app_started_properly(appname, error_on_health)
824
+ app = client.app_info(appname)
825
+ case health(app)
826
+ when 'N/A'
827
+ # Health manager not running.
828
+ err "\Application '#{appname}'s state is undetermined, not enough information available." if error_on_health
829
+ return false
830
+ when 'RUNNING'
831
+ return true
832
+ else
833
+ return false
834
+ end
835
+ end
836
+
837
+ def display_logfile(path, content, instance='0', banner=nil)
838
+ banner ||= "====> #{path} <====\n\n"
839
+ if content && !content.empty?
840
+ display banner
841
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
842
+ unless prefix
843
+ display content
844
+ else
845
+ lines = content.split("\n")
846
+ lines.each { |line| display "#{prefix} #{line}"}
847
+ end
848
+ display ''
849
+ end
850
+ end
851
+
852
+ def log_file_paths
853
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
854
+ end
855
+
856
+ def grab_all_logs(appname)
857
+ instances_info_envelope = client.app_instances(appname)
858
+ return if instances_info_envelope.is_a?(Array)
859
+ instances_info = instances_info_envelope[:instances] || []
860
+ instances_info.each do |entry|
861
+ grab_logs(appname, entry[:index])
862
+ end
863
+ end
864
+
865
+ def grab_logs(appname, instance)
866
+ log_file_paths.each do |path|
867
+ begin
868
+ content = client.app_files(appname, path, instance)
869
+ rescue
870
+ end
871
+ display_logfile(path, content, instance)
872
+ end
873
+ end
874
+
875
+ def grab_crash_logs(appname, instance, was_staged=false)
876
+ # stage crash info
877
+ crashes(appname, false) unless was_staged
878
+
879
+ instance ||= '0'
880
+ map = VMC::Cli::Config.instances
881
+ instance = map[instance] if map[instance]
882
+
883
+ ['/logs/err.log', '/logs/staging.log', 'logs/stderr.log', 'logs/stdout.log', 'logs/startup.log'].each do |path|
884
+ begin
885
+ content = client.app_files(appname, path, instance)
886
+ rescue
887
+ end
888
+ display_logfile(path, content, instance)
889
+ end
890
+ end
891
+
892
+ def grab_startup_tail(appname, since = 0)
893
+ new_lines = 0
894
+ path = "logs/startup.log"
895
+ content = client.app_files(appname, path)
896
+ if content && !content.empty?
897
+ display "\n==== displaying startup log ====\n\n" if since == 0
898
+ response_lines = content.split("\n")
899
+ lines = response_lines.size
900
+ tail = response_lines[since, lines] || []
901
+ new_lines = tail.size
902
+ display tail.join("\n") if new_lines > 0
903
+ end
904
+ since + new_lines
905
+ end
906
+ rescue
907
+ end
908
+
909
+ class FileWithPercentOutput < ::File
910
+ class << self
911
+ attr_accessor :display_str, :upload_size
912
+ end
913
+
914
+ def update_display(rsize)
915
+ @read ||= 0
916
+ @read += rsize
917
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
918
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
919
+ clear(FileWithPercentOutput.display_str.size + 5)
920
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
921
+ VMC::Cli::Config.output.flush
922
+ end
923
+ end
924
+
925
+ def read(*args)
926
+ result = super(*args)
927
+ if result && result.size > 0
928
+ update_display(result.size)
929
+ else
930
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
931
+ clear(FileWithPercentOutput.display_str.size + 5)
932
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
933
+ display('OK'.green)
934
+ end
935
+ end
936
+ result
937
+ end
938
+ end
939
+
940
+ end