vmc 0.0.8 → 0.2.4
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 +8 -3
- data/README.md +83 -0
- data/Rakefile +11 -65
- data/bin/vmc +3 -2
- data/lib/cli/commands/admin.rb +57 -0
- data/lib/cli/commands/apps.rb +828 -0
- data/lib/cli/commands/base.rb +56 -0
- data/lib/cli/commands/misc.rb +99 -0
- data/lib/cli/commands/services.rb +84 -0
- data/lib/cli/commands/user.rb +60 -0
- data/lib/cli/config.rb +109 -0
- data/lib/cli/core_ext.rb +119 -0
- data/lib/cli/errors.rb +19 -0
- data/lib/cli/frameworks.rb +97 -0
- data/lib/cli/runner.rb +437 -0
- data/lib/cli/services_helper.rb +74 -0
- data/lib/cli/usage.rb +94 -0
- data/lib/cli/version.rb +5 -0
- data/lib/cli/zip_util.rb +61 -0
- data/lib/cli.rb +30 -0
- data/lib/vmc/client.rb +415 -0
- data/lib/vmc/const.rb +19 -0
- data/lib/vmc.rb +2 -1589
- 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/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 +73 -0
- data/spec/unit/client_spec.rb +284 -0
- metadata +114 -71
- data/README +0 -58
- data/lib/parse.rb +0 -719
- data/lib/vmc_base.rb +0 -205
- data/vendor/gems/httpclient/VERSION +0 -1
- data/vendor/gems/httpclient/lib/http-access2/cookie.rb +0 -1
- data/vendor/gems/httpclient/lib/http-access2/http.rb +0 -1
- data/vendor/gems/httpclient/lib/http-access2.rb +0 -53
- data/vendor/gems/httpclient/lib/httpclient/auth.rb +0 -522
- data/vendor/gems/httpclient/lib/httpclient/cacert.p7s +0 -1579
- data/vendor/gems/httpclient/lib/httpclient/cacert_sha1.p7s +0 -1579
- data/vendor/gems/httpclient/lib/httpclient/connection.rb +0 -84
- data/vendor/gems/httpclient/lib/httpclient/cookie.rb +0 -562
- data/vendor/gems/httpclient/lib/httpclient/http.rb +0 -867
- data/vendor/gems/httpclient/lib/httpclient/session.rb +0 -864
- data/vendor/gems/httpclient/lib/httpclient/ssl_config.rb +0 -417
- data/vendor/gems/httpclient/lib/httpclient/timeout.rb +0 -136
- data/vendor/gems/httpclient/lib/httpclient/util.rb +0 -86
- data/vendor/gems/httpclient/lib/httpclient.rb +0 -1020
- data/vendor/gems/httpclient/lib/tags +0 -908
@@ -0,0 +1,828 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
module VMC::Cli::Command
|
7
|
+
|
8
|
+
class Apps < Base
|
9
|
+
include VMC::Cli::ServicesHelper
|
10
|
+
|
11
|
+
def list
|
12
|
+
apps = client.apps
|
13
|
+
return display JSON.pretty_generate(apps || []) if @options[:json]
|
14
|
+
|
15
|
+
display "\n"
|
16
|
+
return display "No Applications" if apps.nil? || apps.empty?
|
17
|
+
|
18
|
+
apps_table = table do |t|
|
19
|
+
t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
|
20
|
+
apps.each do |app|
|
21
|
+
t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
display apps_table
|
25
|
+
end
|
26
|
+
|
27
|
+
alias :apps :list
|
28
|
+
|
29
|
+
SLEEP_TIME = 0.5
|
30
|
+
LINE_LENGTH = 80
|
31
|
+
|
32
|
+
# Numerators are in secs
|
33
|
+
TICKER_TICKS = 15/SLEEP_TIME
|
34
|
+
HEALTH_TICKS = 5/SLEEP_TIME
|
35
|
+
TAIL_TICKS = 25/SLEEP_TIME
|
36
|
+
GIVEUP_TICKS = 120/SLEEP_TIME
|
37
|
+
|
38
|
+
def start(appname)
|
39
|
+
app = client.app_info(appname)
|
40
|
+
return display "Application '#{appname}' could not be found".red if app.nil?
|
41
|
+
return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
|
42
|
+
app[:state] = 'STARTED'
|
43
|
+
client.update_app(appname, app)
|
44
|
+
|
45
|
+
count = log_lines_displayed = 0
|
46
|
+
failed = false
|
47
|
+
start_time = Time.now.to_i
|
48
|
+
|
49
|
+
banner = 'Starting Application: '
|
50
|
+
display banner, false
|
51
|
+
|
52
|
+
loop do
|
53
|
+
display '.', false unless count > TICKER_TICKS
|
54
|
+
sleep SLEEP_TIME
|
55
|
+
begin
|
56
|
+
break if app_started_properly(appname, count > HEALTH_TICKS)
|
57
|
+
if !crashes(appname, false, start_time).empty?
|
58
|
+
# Check for the existance of crashes
|
59
|
+
display "\nError: Application [#{appname}] failed to start, logs information below.\n".red
|
60
|
+
grab_crash_logs(appname, '0', true)
|
61
|
+
if @push
|
62
|
+
display "\n"
|
63
|
+
should_delete = ask 'Should I delete the application? (Y/n)? ' unless no_prompt
|
64
|
+
delete_app(appname, false) unless no_prompt || should_delete.upcase == 'N'
|
65
|
+
end
|
66
|
+
failed = true
|
67
|
+
break
|
68
|
+
elsif count > TAIL_TICKS
|
69
|
+
log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
err(e.message, '')
|
73
|
+
end
|
74
|
+
count += 1
|
75
|
+
if count > GIVEUP_TICKS # 2 minutes
|
76
|
+
display "\nApplication is taking too long to start, check your logs".yellow
|
77
|
+
break
|
78
|
+
end
|
79
|
+
end
|
80
|
+
exit(false) if failed
|
81
|
+
clear(LINE_LENGTH)
|
82
|
+
display "#{banner}#{'OK'.green}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop(appname)
|
86
|
+
app = client.app_info(appname)
|
87
|
+
return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED'
|
88
|
+
display 'Stopping Application: ', false
|
89
|
+
app[:state] = 'STOPPED'
|
90
|
+
client.update_app(appname, app)
|
91
|
+
display 'OK'.green
|
92
|
+
end
|
93
|
+
|
94
|
+
def restart(appname)
|
95
|
+
stop(appname)
|
96
|
+
start(appname)
|
97
|
+
end
|
98
|
+
|
99
|
+
def rename(appname, newname)
|
100
|
+
app = client.app_info(appname)
|
101
|
+
app[:name] = newname
|
102
|
+
display 'Renaming Appliction: '
|
103
|
+
client.update_app(newname, app)
|
104
|
+
display 'OK'.green
|
105
|
+
end
|
106
|
+
|
107
|
+
def mem(appname, memsize=nil)
|
108
|
+
app = client.app_info(appname)
|
109
|
+
mem = current_mem = mem_quota_to_choice(app[:resources][:memory])
|
110
|
+
memsize = normalize_mem(memsize) if memsize
|
111
|
+
|
112
|
+
unless memsize
|
113
|
+
choose do |menu|
|
114
|
+
menu.layout = :one_line
|
115
|
+
menu.prompt = "Update Memory Reservation? [Current:#{current_mem}] "
|
116
|
+
menu.default = current_mem
|
117
|
+
mem_choices.each { |choice| menu.choice(choice) { memsize = choice } }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
mem = mem_choice_to_quota(mem)
|
122
|
+
memsize = mem_choice_to_quota(memsize)
|
123
|
+
current_mem = mem_choice_to_quota(current_mem)
|
124
|
+
|
125
|
+
display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
|
126
|
+
|
127
|
+
# check memsize here for capacity
|
128
|
+
check_has_capacity_for(memsize - mem)
|
129
|
+
|
130
|
+
mem = memsize
|
131
|
+
|
132
|
+
if (mem != current_mem)
|
133
|
+
app[:resources][:memory] = mem
|
134
|
+
client.update_app(appname, app)
|
135
|
+
display 'OK'.green
|
136
|
+
restart appname if app[:state] == 'STARTED'
|
137
|
+
else
|
138
|
+
display 'OK'.green
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def map(appname, url)
|
143
|
+
app = client.app_info(appname)
|
144
|
+
uris = app[:uris] || []
|
145
|
+
uris << url
|
146
|
+
app[:uris] = uris
|
147
|
+
client.update_app(appname, app)
|
148
|
+
display "Succesfully mapped url".green
|
149
|
+
end
|
150
|
+
|
151
|
+
def unmap(appname, url)
|
152
|
+
app = client.app_info(appname)
|
153
|
+
uris = app[:uris] || []
|
154
|
+
url = url.gsub(/^http(s*):\/\//i, '')
|
155
|
+
deleted = uris.delete(url)
|
156
|
+
err "Invalid url" unless deleted
|
157
|
+
app[:uris] = uris
|
158
|
+
client.update_app(appname, app)
|
159
|
+
display "Succesfully unmapped url".green
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
def delete(appname=nil, force=false)
|
164
|
+
if @options[:all]
|
165
|
+
apps = client.apps
|
166
|
+
apps.each { |app| delete_app(app[:name], force) }
|
167
|
+
else
|
168
|
+
err 'No valid appname given' unless appname
|
169
|
+
delete_app(appname, force)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def delete_app(appname, force)
|
174
|
+
app = client.app_info(appname)
|
175
|
+
services_to_delete = []
|
176
|
+
app_services = app[:services]
|
177
|
+
app_services.each { |service|
|
178
|
+
del_service = 'Y' if force
|
179
|
+
del_service = 'N' if no_prompt
|
180
|
+
unless no_prompt || force
|
181
|
+
del_service = ask("Provisioned service [#{service}] detected, would you like to delete it? [Yn]: ")
|
182
|
+
end
|
183
|
+
services_to_delete << service unless del_service.upcase == 'N'
|
184
|
+
}
|
185
|
+
display "Deleting application [#{appname}]: ", false
|
186
|
+
client.delete_app(appname)
|
187
|
+
display 'OK'.green
|
188
|
+
|
189
|
+
services_to_delete.each do |s|
|
190
|
+
display "Deleting service [#{s}]: ", false
|
191
|
+
client.delete_service(s)
|
192
|
+
display 'OK'.green
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def all_files(appname, path)
|
197
|
+
instances_info_envelope = client.app_instances(appname)
|
198
|
+
return if instances_info_envelope.is_a?(Array)
|
199
|
+
instances_info = instances_info_envelope[:instances] || []
|
200
|
+
instances_info.each do |entry|
|
201
|
+
content = client.app_files(appname, path, entry[:index])
|
202
|
+
display_logfile(path, content, entry[:index], "====> [#{entry[:index]}: #{path}] <====\n".bold)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def files(appname, path='/')
|
207
|
+
return all_files(appname, path) if @options[:all] && !@options[:instance]
|
208
|
+
instance = @options[:instance] || '0'
|
209
|
+
content = client.app_files(appname, path, instance)
|
210
|
+
display content
|
211
|
+
rescue VMC::Client::NotFound => e
|
212
|
+
err 'No such file or directory'
|
213
|
+
end
|
214
|
+
|
215
|
+
def logs(appname)
|
216
|
+
return grab_all_logs(appname) if @options[:all] && !@options[:instance]
|
217
|
+
instance = @options[:instance] || '0'
|
218
|
+
grab_logs(appname, instance)
|
219
|
+
end
|
220
|
+
|
221
|
+
def crashes(appname, print_results=true, since=0)
|
222
|
+
crashed = client.app_crashes(appname)[:crashes]
|
223
|
+
crashed.delete_if { |c| c[:since] < since }
|
224
|
+
instance_map = {}
|
225
|
+
|
226
|
+
# return display JSON.pretty_generate(apps) if @options[:json]
|
227
|
+
|
228
|
+
|
229
|
+
counter = 0
|
230
|
+
crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
|
231
|
+
crashed_table = table do |t|
|
232
|
+
t.headings = 'Name', 'Instance ID', 'Crashed Time'
|
233
|
+
crashed.each do |crash|
|
234
|
+
name = "#{appname}-#{counter += 1}"
|
235
|
+
instance_map[name] = crash[:instance]
|
236
|
+
t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
VMC::Cli::Config.store_instances(instance_map)
|
241
|
+
|
242
|
+
if @options[:json]
|
243
|
+
return display JSON.pretty_generate(crashed)
|
244
|
+
elsif print_results
|
245
|
+
display "\n"
|
246
|
+
if crashed.empty?
|
247
|
+
display "No crashed instances for [#{appname}]" if print_results
|
248
|
+
else
|
249
|
+
display crashed_table if print_results
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
crashed
|
254
|
+
end
|
255
|
+
|
256
|
+
def crashlogs(appname)
|
257
|
+
instance = @options[:instance] || '0'
|
258
|
+
grab_crash_logs(appname, instance)
|
259
|
+
end
|
260
|
+
|
261
|
+
def instances(appname, num=nil)
|
262
|
+
if (num)
|
263
|
+
change_instances(appname, num)
|
264
|
+
else
|
265
|
+
get_instances(appname)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def stats(appname)
|
270
|
+
stats = client.app_stats(appname)
|
271
|
+
return display JSON.pretty_generate(stats) if @options[:json]
|
272
|
+
|
273
|
+
stats_table = table do |t|
|
274
|
+
t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime'
|
275
|
+
stats.each do |entry|
|
276
|
+
index = entry[:instance]
|
277
|
+
stat = entry[:stats]
|
278
|
+
hp = "#{stat[:host]}:#{stat[:port]}"
|
279
|
+
uptime = uptime_string(stat[:uptime])
|
280
|
+
usage = stat[:usage]
|
281
|
+
if usage
|
282
|
+
cpu = usage[:cpu]
|
283
|
+
mem = (usage[:mem] * 1024) # mem comes in K's
|
284
|
+
disk = usage[:disk]
|
285
|
+
end
|
286
|
+
mem_quota = stat[:mem_quota]
|
287
|
+
disk_quota = stat[:disk_quota]
|
288
|
+
mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
|
289
|
+
disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
|
290
|
+
cpu = cpu ? cpu.to_s : 'NA'
|
291
|
+
cpu = "#{cpu}% (#{stat[:cores]})"
|
292
|
+
t << [index, cpu, mem, disk, uptime]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
display "\n"
|
296
|
+
if stats.empty?
|
297
|
+
display "No running instances for [#{appname}]".yellow
|
298
|
+
else
|
299
|
+
display stats_table
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def update(appname)
|
304
|
+
path = @options[:path] || '.'
|
305
|
+
upload_app_bits(appname, path)
|
306
|
+
display "Successfully updated Application: '#{appname}'".green
|
307
|
+
if @options[:canary]
|
308
|
+
display "[--canary] is deprecated and will be removed in a future version".yellow
|
309
|
+
end
|
310
|
+
app = client.app_info(appname)
|
311
|
+
restart appname if app[:state] == 'STARTED'
|
312
|
+
end
|
313
|
+
|
314
|
+
def push(appname=nil)
|
315
|
+
instances = @options[:instances] || 1
|
316
|
+
exec = @options[:exec] || 'thin start'
|
317
|
+
ignore_framework = @options[:noframework]
|
318
|
+
no_start = @options[:nostart]
|
319
|
+
|
320
|
+
path = @options[:path] || '.'
|
321
|
+
appname = @options[:name] unless appname
|
322
|
+
url = @options[:url]
|
323
|
+
mem, memswitch = nil, @options[:mem]
|
324
|
+
memswitch = normalize_mem(memswitch) if memswitch
|
325
|
+
|
326
|
+
# Check app existing upfront if we have appname
|
327
|
+
app_checked = false
|
328
|
+
if appname
|
329
|
+
err "Application '#{appname}' already exists, use update" if app_exists?(appname)
|
330
|
+
app_checked = true
|
331
|
+
end
|
332
|
+
|
333
|
+
# check if we have hit our app limit
|
334
|
+
check_app_limit
|
335
|
+
|
336
|
+
# check memsize here for capacity
|
337
|
+
check_has_capacity_for(mem_choice_to_quota(memswitch)) if memswitch
|
338
|
+
|
339
|
+
unless no_prompt || @options[:path]
|
340
|
+
proceed = ask('Would you like to deploy from the current directory? [Yn]: ')
|
341
|
+
if proceed.upcase == 'N'
|
342
|
+
path = ask('Please enter in the deployment path: ')
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
path = File.expand_path(path)
|
347
|
+
check_deploy_directory(path)
|
348
|
+
|
349
|
+
appname = ask("Application Name: ") unless no_prompt || appname
|
350
|
+
err "Application Name required." if appname.nil? || appname.empty?
|
351
|
+
|
352
|
+
unless app_checked
|
353
|
+
err "Application '#{appname}' already exists, use update or delete." if app_exists?(appname)
|
354
|
+
end
|
355
|
+
|
356
|
+
url = ask("Application Deployed URL: '#{appname}.#{VMC::Cli::Config.suggest_url}'? ") unless no_prompt || url
|
357
|
+
url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if url.nil? || url.empty?
|
358
|
+
|
359
|
+
# Detect the appropriate framework.
|
360
|
+
framework = nil
|
361
|
+
unless ignore_framework
|
362
|
+
framework = VMC::Cli::Framework.detect(path)
|
363
|
+
framework_correct = ask("Detected a #{framework}, is this correct? [Yn]: ") if prompt_ok && framework
|
364
|
+
framework_correct ||= 'y'
|
365
|
+
if prompt_ok && (framework.nil? || framework_correct.upcase == 'N')
|
366
|
+
display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
|
367
|
+
choose do |menu|
|
368
|
+
menu.layout = :one_line
|
369
|
+
menu.prompt = "Select Application Type: "
|
370
|
+
menu.default = framework
|
371
|
+
VMC::Cli::Framework.known_frameworks.each do |f|
|
372
|
+
menu.choice(f) { framework = VMC::Cli::Framework.lookup(f) }
|
373
|
+
end
|
374
|
+
end
|
375
|
+
display "Selected #{framework}"
|
376
|
+
end
|
377
|
+
# Framework override, deprecated
|
378
|
+
exec = framework.exec if framework && framework.exec
|
379
|
+
else
|
380
|
+
framework = VMC::Cli::Framework.new
|
381
|
+
end
|
382
|
+
|
383
|
+
err "Application Type undetermined for path '#{path}'" unless framework
|
384
|
+
unless memswitch
|
385
|
+
mem = framework.memory
|
386
|
+
if prompt_ok
|
387
|
+
choose do |menu|
|
388
|
+
menu.layout = :one_line
|
389
|
+
menu.prompt = "Memory Reservation [Default:#{mem}] "
|
390
|
+
menu.default = mem
|
391
|
+
mem_choices.each { |choice| menu.choice(choice) { mem = choice } }
|
392
|
+
end
|
393
|
+
end
|
394
|
+
else
|
395
|
+
mem = memswitch
|
396
|
+
end
|
397
|
+
|
398
|
+
# Set to MB number
|
399
|
+
mem_quota = mem_choice_to_quota(mem)
|
400
|
+
|
401
|
+
# check memsize here for capacity
|
402
|
+
check_has_capacity_for(mem_quota)
|
403
|
+
|
404
|
+
display 'Creating Application: ', false
|
405
|
+
|
406
|
+
manifest = {
|
407
|
+
:name => "#{appname}",
|
408
|
+
:staging => {
|
409
|
+
:model => framework.name,
|
410
|
+
:stack => exec
|
411
|
+
},
|
412
|
+
:uris => [url],
|
413
|
+
:instances => instances,
|
414
|
+
:resources => {
|
415
|
+
:memory => mem_quota
|
416
|
+
},
|
417
|
+
}
|
418
|
+
|
419
|
+
# Send the manifest to the cloud controller
|
420
|
+
client.create_app(appname, manifest)
|
421
|
+
display 'OK'.green
|
422
|
+
|
423
|
+
# Services check
|
424
|
+
services = client.services_info
|
425
|
+
unless no_prompt || @options[:noservices] || services.empty?
|
426
|
+
proceed = ask("Would you like to bind any services to '#{appname}'? [yN]: ")
|
427
|
+
bind_services(appname, services) if proceed.upcase == 'Y'
|
428
|
+
end
|
429
|
+
|
430
|
+
# Stage and upload the app bits.
|
431
|
+
upload_app_bits(appname, path)
|
432
|
+
|
433
|
+
@push = true
|
434
|
+
start(appname) unless no_start
|
435
|
+
end
|
436
|
+
|
437
|
+
private
|
438
|
+
|
439
|
+
def app_exists?(appname)
|
440
|
+
app_info = client.app_info(appname)
|
441
|
+
app_info != nil
|
442
|
+
rescue
|
443
|
+
false
|
444
|
+
end
|
445
|
+
|
446
|
+
def check_deploy_directory(path)
|
447
|
+
err 'Deployment path does not exist' unless File.exists? path
|
448
|
+
err 'Deployment path is not a directory' unless File.directory? path
|
449
|
+
return if File.expand_path(Dir.tmpdir) != File.expand_path(path)
|
450
|
+
err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]"
|
451
|
+
end
|
452
|
+
|
453
|
+
def upload_app_bits(appname, path)
|
454
|
+
display 'Uploading Application:'
|
455
|
+
|
456
|
+
upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil
|
457
|
+
FileUtils.rm_f(upload_file)
|
458
|
+
|
459
|
+
explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files"
|
460
|
+
FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over..
|
461
|
+
|
462
|
+
Dir.chdir(path) do
|
463
|
+
# Stage the app appropriately and do the appropriate fingerprinting, etc.
|
464
|
+
if war_file = Dir.glob('*.war').first
|
465
|
+
VMC::Cli::ZipUtil.unpack(war_file, explode_dir)
|
466
|
+
else
|
467
|
+
FileUtils.mkdir(explode_dir)
|
468
|
+
files = Dir.glob('{*,.[^\.]*}')
|
469
|
+
FileUtils.cp_r(files, explode_dir)
|
470
|
+
end
|
471
|
+
|
472
|
+
# Send the resource list to the cloudcontroller, the response will tell us what it already has..
|
473
|
+
unless @options[:noresources]
|
474
|
+
display ' Checking for available resources: ', false
|
475
|
+
fingerprints = []
|
476
|
+
total_size = 0
|
477
|
+
resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH)
|
478
|
+
resource_files.each do |filename|
|
479
|
+
fingerprints << {
|
480
|
+
:size => File.size(filename),
|
481
|
+
:sha1 => Digest::SHA1.file(filename).hexdigest,
|
482
|
+
:fn => filename
|
483
|
+
} unless (File.directory?(filename) || !File.exists?(filename))
|
484
|
+
total_size += File.size(filename)
|
485
|
+
end
|
486
|
+
|
487
|
+
# Check to see if the resource check is worth the round trip
|
488
|
+
if (total_size > (64*1024)) # 64k for now
|
489
|
+
# Send resource fingerprints to the cloud controller
|
490
|
+
appcloud_resources = client.check_resources(fingerprints)
|
491
|
+
end
|
492
|
+
display 'OK'.green
|
493
|
+
|
494
|
+
if appcloud_resources
|
495
|
+
display ' Processing resources: ', false
|
496
|
+
# We can then delete what we do not need to send.
|
497
|
+
appcloud_resources.each do |resource|
|
498
|
+
FileUtils.rm_f resource[:fn]
|
499
|
+
# adjust filenames sans the explode_dir prefix
|
500
|
+
resource[:fn].sub!("#{explode_dir}/", '')
|
501
|
+
end
|
502
|
+
display 'OK'.green
|
503
|
+
end
|
504
|
+
|
505
|
+
end
|
506
|
+
|
507
|
+
# Perform Packing of the upload bits here.
|
508
|
+
display ' Packing application: ', false
|
509
|
+
VMC::Cli::ZipUtil.pack(explode_dir, upload_file)
|
510
|
+
display 'OK'.green
|
511
|
+
|
512
|
+
upload_size = File.size(upload_file);
|
513
|
+
if upload_size > 1024*1024
|
514
|
+
upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
|
515
|
+
elsif upload_size > 0
|
516
|
+
upload_size = (upload_size/1024.0).round.to_s + 'K'
|
517
|
+
end
|
518
|
+
upload_str = " Uploading (#{upload_size}): "
|
519
|
+
display upload_str, false
|
520
|
+
FileWithPercentOutput.display_str = upload_str
|
521
|
+
FileWithPercentOutput.upload_size = File.size(upload_file);
|
522
|
+
file = FileWithPercentOutput.open(upload_file, 'rb')
|
523
|
+
client.upload_app(appname, file, appcloud_resources)
|
524
|
+
display 'Push Status: ', false
|
525
|
+
display 'OK'.green
|
526
|
+
end
|
527
|
+
|
528
|
+
ensure
|
529
|
+
# Cleanup if we created an exploded directory.
|
530
|
+
FileUtils.rm_f(upload_file) if upload_file
|
531
|
+
FileUtils.rm_rf(explode_dir) if explode_dir
|
532
|
+
end
|
533
|
+
|
534
|
+
def choose_existing_service(appname, user_services)
|
535
|
+
return unless prompt_ok
|
536
|
+
selected = false
|
537
|
+
choose do |menu|
|
538
|
+
menu.header = "The following provisioned services are available:"
|
539
|
+
menu.prompt = 'Please select one you wish to provision: '
|
540
|
+
menu.select_by = :index_or_name
|
541
|
+
user_services.each do |s|
|
542
|
+
menu.choice(s[:name]) do
|
543
|
+
display "Binding Service: ", false
|
544
|
+
client.bind_service(s[:name], appname)
|
545
|
+
display 'OK'.green
|
546
|
+
selected = true
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
selected
|
551
|
+
end
|
552
|
+
|
553
|
+
def choose_new_service(appname, services)
|
554
|
+
return unless prompt_ok
|
555
|
+
choose do |menu|
|
556
|
+
menu.header = "The following system services are available:"
|
557
|
+
menu.prompt = 'Please select one you wish to provision: '
|
558
|
+
menu.select_by = :index_or_name
|
559
|
+
services.each do |service_type, value|
|
560
|
+
value.each do |vendor, version|
|
561
|
+
menu.choice(vendor) do
|
562
|
+
default_name = random_service_name(vendor)
|
563
|
+
service_name = ask("Specify the name of the service [#{default_name}]: ")
|
564
|
+
service_name = default_name if service_name.empty?
|
565
|
+
create_service_banner(vendor, service_name)
|
566
|
+
bind_service_banner(service_name, appname)
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def bind_services(appname, services)
|
574
|
+
user_services = client.services
|
575
|
+
selected_existing = false
|
576
|
+
unless no_prompt || user_services.empty?
|
577
|
+
use_existing = ask "Would you like to use an existing provisioned service [yN]? "
|
578
|
+
if use_existing.upcase == 'Y'
|
579
|
+
selected_existing = choose_existing_service(appname, user_services)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
# Create a new service and bind it here
|
583
|
+
unless selected_existing
|
584
|
+
choose_new_service(appname, services)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
def check_app_limit
|
589
|
+
usage = client_info[:usage]
|
590
|
+
limits = client_info[:limits]
|
591
|
+
return unless usage and limits and limits[:apps]
|
592
|
+
if limits[:apps] == usage[:apps]
|
593
|
+
display "Not enough capacity for operation.".red
|
594
|
+
tapps = limits[:apps] || 0
|
595
|
+
apps = usage[:apps] || 0
|
596
|
+
err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
def check_has_capacity_for(mem_wanted)
|
601
|
+
usage = client_info[:usage]
|
602
|
+
limits = client_info[:limits]
|
603
|
+
return unless usage and limits
|
604
|
+
available_for_use = limits[:memory].to_i - usage[:memory].to_i
|
605
|
+
if mem_wanted > available_for_use
|
606
|
+
tmem = pretty_size(limits[:memory]*1024*1024)
|
607
|
+
mem = pretty_size(usage[:memory]*1024*1024)
|
608
|
+
display "Not enough capacity for operation.".yellow
|
609
|
+
available = pretty_size(available_for_use * 1024 * 1024)
|
610
|
+
err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def mem_choices
|
615
|
+
default = ['64M', '128M', '256M', '512M', '1G', '2G']
|
616
|
+
|
617
|
+
return default unless client_info
|
618
|
+
return default unless (usage = client_info[:usage] and limits = client_info[:limits])
|
619
|
+
|
620
|
+
available_for_use = limits[:memory].to_i - usage[:memory].to_i
|
621
|
+
check_has_capacity_for(64) if available_for_use < 64
|
622
|
+
return ['64M'] if available_for_use < 128
|
623
|
+
return ['64M', '128M'] if available_for_use < 256
|
624
|
+
return ['64M', '128M', '256M'] if available_for_use < 512
|
625
|
+
return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
|
626
|
+
return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
|
627
|
+
return ['64M', '128M', '256M', '512M', '1G', '2G']
|
628
|
+
end
|
629
|
+
|
630
|
+
def normalize_mem(mem)
|
631
|
+
return mem if /K|G|M/i =~ mem
|
632
|
+
"#{mem}M"
|
633
|
+
end
|
634
|
+
|
635
|
+
def mem_choice_to_quota(mem_choice)
|
636
|
+
(mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
|
637
|
+
mem_quota
|
638
|
+
end
|
639
|
+
|
640
|
+
def mem_quota_to_choice(mem)
|
641
|
+
if mem < 1024
|
642
|
+
mem_choice = "#{mem}M"
|
643
|
+
else
|
644
|
+
mem_choice = "#{(mem/1024).to_i}G"
|
645
|
+
end
|
646
|
+
mem_choice
|
647
|
+
end
|
648
|
+
|
649
|
+
def get_instances(appname)
|
650
|
+
instances_info_envelope = client.app_instances(appname)
|
651
|
+
# Empty array is returned if there are no instances running.
|
652
|
+
instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
|
653
|
+
|
654
|
+
instances_info = instances_info_envelope[:instances] || []
|
655
|
+
instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
|
656
|
+
|
657
|
+
return display JSON.pretty_generate(instances_info) if @options[:json]
|
658
|
+
|
659
|
+
return display "No running instances for [#{appname}]".yellow if instances_info.empty?
|
660
|
+
|
661
|
+
instances_table = table do |t|
|
662
|
+
t.headings = 'Index', 'State', 'Start Time'
|
663
|
+
instances_info.each do |entry|
|
664
|
+
t << [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
|
665
|
+
end
|
666
|
+
end
|
667
|
+
display "\n"
|
668
|
+
display instances_table
|
669
|
+
end
|
670
|
+
|
671
|
+
def change_instances(appname, instances)
|
672
|
+
app = client.app_info(appname)
|
673
|
+
|
674
|
+
match = instances.match(/([+-])?\d+/)
|
675
|
+
err "Invalid number of instances '#{instances}'" unless match
|
676
|
+
|
677
|
+
instances = instances.to_i
|
678
|
+
current_instances = app[:instances]
|
679
|
+
new_instances = match.captures[0] ? current_instances + instances : instances
|
680
|
+
err "There must be at least 1 instance." if new_instances < 1
|
681
|
+
|
682
|
+
if current_instances == new_instances
|
683
|
+
display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
|
684
|
+
return
|
685
|
+
end
|
686
|
+
|
687
|
+
up_or_down = new_instances > current_instances ? 'up' : 'down'
|
688
|
+
display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
|
689
|
+
app[:instances] = new_instances
|
690
|
+
client.update_app(appname, app)
|
691
|
+
display 'OK'.green
|
692
|
+
end
|
693
|
+
|
694
|
+
def health(d)
|
695
|
+
return 'N/A' unless (d and d[:state])
|
696
|
+
return 'STOPPED' if d[:state] == 'STOPPED'
|
697
|
+
|
698
|
+
healthy_instances = d[:runningInstances]
|
699
|
+
expected_instance = d[:instances]
|
700
|
+
health = nil
|
701
|
+
|
702
|
+
if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
|
703
|
+
health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
|
704
|
+
end
|
705
|
+
|
706
|
+
return 'RUNNING' if health && health == 1.0
|
707
|
+
return "#{(health * 100).round}%" if health
|
708
|
+
return 'N/A'
|
709
|
+
end
|
710
|
+
|
711
|
+
def app_started_properly(appname, error_on_health)
|
712
|
+
app = client.app_info(appname)
|
713
|
+
case health(app)
|
714
|
+
when 'N/A'
|
715
|
+
# Health manager not running.
|
716
|
+
err "\Application '#{appname}'s state is undetermined, not enough information available." if error_on_health
|
717
|
+
return false
|
718
|
+
when 'RUNNING'
|
719
|
+
return true
|
720
|
+
else
|
721
|
+
return false
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
def display_logfile(path, content, instance='0', banner=nil)
|
726
|
+
banner ||= "====> #{path} <====\n\n"
|
727
|
+
if content && !content.empty?
|
728
|
+
display banner
|
729
|
+
prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
|
730
|
+
unless prefix
|
731
|
+
display content
|
732
|
+
else
|
733
|
+
lines = content.split("\n")
|
734
|
+
lines.each { |line| display "#{prefix} #{line}"}
|
735
|
+
end
|
736
|
+
display ''
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
def log_file_paths
|
741
|
+
%w[logs/stderr.log logs/stdout.log logs/startup.log]
|
742
|
+
end
|
743
|
+
|
744
|
+
def grab_all_logs(appname)
|
745
|
+
instances_info_envelope = client.app_instances(appname)
|
746
|
+
return if instances_info_envelope.is_a?(Array)
|
747
|
+
instances_info = instances_info_envelope[:instances] || []
|
748
|
+
instances_info.each do |entry|
|
749
|
+
grab_logs(appname, entry[:index])
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
def grab_logs(appname, instance)
|
754
|
+
log_file_paths.each do |path|
|
755
|
+
begin
|
756
|
+
content = client.app_files(appname, path, instance)
|
757
|
+
rescue
|
758
|
+
end
|
759
|
+
display_logfile(path, content, instance)
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
def grab_crash_logs(appname, instance, was_staged=false)
|
764
|
+
# stage crash info
|
765
|
+
crashes(appname, false) unless was_staged
|
766
|
+
|
767
|
+
instance ||= '0'
|
768
|
+
map = VMC::Cli::Config.instances
|
769
|
+
instance = map[instance] if map[instance]
|
770
|
+
|
771
|
+
['/logs/err.log', 'logs/stderr.log', 'logs/stdout.log', 'logs/startup.log'].each do |path|
|
772
|
+
begin
|
773
|
+
content = client.app_files(appname, path, instance)
|
774
|
+
rescue
|
775
|
+
end
|
776
|
+
display_logfile(path, content, instance)
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
def grab_startup_tail(appname, since = 0)
|
781
|
+
new_lines = 0
|
782
|
+
path = "logs/startup.log"
|
783
|
+
content = client.app_files(appname, path)
|
784
|
+
if content && !content.empty?
|
785
|
+
display "\n==== displaying startup log ====\n\n" if since == 0
|
786
|
+
response_lines = content.split("\n")
|
787
|
+
lines = response_lines.size
|
788
|
+
tail = response_lines[since, lines] || []
|
789
|
+
new_lines = tail.size
|
790
|
+
display tail.join("\n") if new_lines > 0
|
791
|
+
end
|
792
|
+
since + new_lines
|
793
|
+
end
|
794
|
+
rescue
|
795
|
+
end
|
796
|
+
|
797
|
+
class FileWithPercentOutput < ::File
|
798
|
+
class << self
|
799
|
+
attr_accessor :display_str, :upload_size
|
800
|
+
end
|
801
|
+
|
802
|
+
def update_display(rsize)
|
803
|
+
@read ||= 0
|
804
|
+
@read += rsize
|
805
|
+
p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
|
806
|
+
unless VMC::Cli::Config.output.nil?
|
807
|
+
clear(FileWithPercentOutput.display_str.size + 5)
|
808
|
+
VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
|
809
|
+
VMC::Cli::Config.output.flush
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
def read(*args)
|
814
|
+
result = super(*args)
|
815
|
+
if result && result.size > 0
|
816
|
+
update_display(result.size)
|
817
|
+
else
|
818
|
+
unless VMC::Cli::Config.output.nil?
|
819
|
+
clear(FileWithPercentOutput.display_str.size + 5)
|
820
|
+
VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
|
821
|
+
display('OK'.green)
|
822
|
+
end
|
823
|
+
end
|
824
|
+
result
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
end
|