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.
- data/LICENSE +24 -0
- data/README.md +92 -0
- data/Rakefile +17 -0
- data/bin/af +6 -0
- data/lib/cli.rb +30 -0
- data/lib/cli/commands/admin.rb +77 -0
- data/lib/cli/commands/apps.rb +940 -0
- data/lib/cli/commands/base.rb +79 -0
- data/lib/cli/commands/misc.rb +128 -0
- data/lib/cli/commands/services.rb +86 -0
- data/lib/cli/commands/user.rb +60 -0
- data/lib/cli/config.rb +110 -0
- data/lib/cli/core_ext.rb +119 -0
- data/lib/cli/errors.rb +19 -0
- data/lib/cli/frameworks.rb +109 -0
- data/lib/cli/runner.rb +490 -0
- data/lib/cli/services_helper.rb +78 -0
- data/lib/cli/usage.rb +104 -0
- data/lib/cli/version.rb +7 -0
- data/lib/cli/zip_util.rb +77 -0
- data/lib/vmc.rb +3 -0
- data/lib/vmc/client.rb +451 -0
- data/lib/vmc/const.rb +21 -0
- data/spec/assets/app_info.txt +9 -0
- data/spec/assets/app_listings.txt +9 -0
- data/spec/assets/bad_create_app.txt +9 -0
- data/spec/assets/delete_app.txt +9 -0
- data/spec/assets/global_service_listings.txt +9 -0
- data/spec/assets/good_create_app.txt +9 -0
- data/spec/assets/good_create_service.txt +9 -0
- data/spec/assets/info_authenticated.txt +27 -0
- data/spec/assets/info_return.txt +15 -0
- data/spec/assets/info_return_bad.txt +16 -0
- data/spec/assets/list_users.txt +13 -0
- data/spec/assets/login_fail.txt +9 -0
- data/spec/assets/login_success.txt +9 -0
- data/spec/assets/sample_token.txt +1 -0
- data/spec/assets/service_already_exists.txt +9 -0
- data/spec/assets/service_listings.txt +9 -0
- data/spec/assets/service_not_found.txt +9 -0
- data/spec/assets/user_info.txt +9 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unit/cli_opts_spec.rb +68 -0
- data/spec/unit/client_spec.rb +332 -0
- 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
|
+
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
data/lib/cli.rb
ADDED
@@ -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
|