af 0.3.12

Sign up to get free protection for your applications and to get access to all the features.
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