af 0.3.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'terminal-table/import'
|
4
|
+
require 'highline/import'
|
5
|
+
|
6
|
+
module VMC::Cli
|
7
|
+
|
8
|
+
module Command
|
9
|
+
|
10
|
+
class Base
|
11
|
+
attr_reader :no_prompt, :prompt_ok
|
12
|
+
|
13
|
+
def initialize(options={})
|
14
|
+
@options = options.dup
|
15
|
+
@no_prompt = @options[:noprompts]
|
16
|
+
@prompt_ok = !no_prompt
|
17
|
+
|
18
|
+
# Fix for system ruby and Highline (stdin) on MacOSX
|
19
|
+
if RUBY_PLATFORM =~ /darwin/ && RUBY_VERSION == '1.8.7' && RUBY_PATCHLEVEL <= 174
|
20
|
+
HighLine.track_eof = false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Suppress colorize on Windows systems for now.
|
24
|
+
if !!RUBY_PLATFORM['mingw'] || !!RUBY_PLATFORM['mswin32'] || !!RUBY_PLATFORM['cygwin']
|
25
|
+
VMC::Cli::Config.colorize = false
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def client
|
31
|
+
return @client if @client
|
32
|
+
@client = VMC::Client.new(target_url, auth_token)
|
33
|
+
@client.trace = VMC::Cli::Config.trace if VMC::Cli::Config.trace
|
34
|
+
@client.proxy_for @options[:proxy] if @options[:proxy]
|
35
|
+
@client
|
36
|
+
end
|
37
|
+
|
38
|
+
def client_info
|
39
|
+
return @client_info if @client_info
|
40
|
+
@client_info = client.info
|
41
|
+
end
|
42
|
+
|
43
|
+
def target_url
|
44
|
+
return @target_url if @target_url
|
45
|
+
@target_url = VMC::Cli::Config.target_url
|
46
|
+
end
|
47
|
+
|
48
|
+
def auth_token
|
49
|
+
return @auth_token if @auth_token
|
50
|
+
@auth_token = VMC::Cli::Config.auth_token
|
51
|
+
end
|
52
|
+
|
53
|
+
def runtimes_info
|
54
|
+
return @runtimes if @runtimes
|
55
|
+
info = client_info
|
56
|
+
@runtimes = {}
|
57
|
+
if info[:frameworks]
|
58
|
+
info[:frameworks].each_value do |f|
|
59
|
+
next unless f[:runtimes]
|
60
|
+
f[:runtimes].each { |r| @runtimes[r[:name]] = r}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
@runtimes
|
64
|
+
end
|
65
|
+
|
66
|
+
def frameworks_info
|
67
|
+
return @frameworks if @frameworks
|
68
|
+
info = client_info
|
69
|
+
@frameworks = []
|
70
|
+
if info[:frameworks]
|
71
|
+
info[:frameworks].each_value { |f| @frameworks << [f[:name]] }
|
72
|
+
end
|
73
|
+
@frameworks
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module VMC::Cli::Command
|
2
|
+
|
3
|
+
class Misc < Base
|
4
|
+
def version
|
5
|
+
say "vmc #{VMC::Cli::VERSION}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def target
|
9
|
+
return display JSON.pretty_generate({:target => target_url}) if @options[:json]
|
10
|
+
banner "[#{target_url}]"
|
11
|
+
end
|
12
|
+
|
13
|
+
def targets
|
14
|
+
targets = VMC::Cli::Config.targets
|
15
|
+
return display JSON.pretty_generate(targets) if @options[:json]
|
16
|
+
return display 'None specified' if targets.empty?
|
17
|
+
targets_table = table do |t|
|
18
|
+
t.headings = 'Target', 'Authorization'
|
19
|
+
targets.each { |target, token| t << [target, token] }
|
20
|
+
end
|
21
|
+
display "\n"
|
22
|
+
display targets_table
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :tokens :targets
|
26
|
+
|
27
|
+
def set_target(target_url)
|
28
|
+
target_url = "http://#{target_url}" unless /^https?/ =~ target_url
|
29
|
+
target_url = target_url.gsub(/\/+$/, '')
|
30
|
+
client = VMC::Client.new(target_url)
|
31
|
+
unless client.target_valid?
|
32
|
+
if prompt_ok
|
33
|
+
display "Host is not valid: '#{target_url}'".red
|
34
|
+
show_response = ask "Would you like see the response [yN]? "
|
35
|
+
display "\n<<<\n#{client.raw_info}\n>>>\n" if show_response.upcase == 'Y'
|
36
|
+
end
|
37
|
+
exit(false)
|
38
|
+
else
|
39
|
+
VMC::Cli::Config.store_target(target_url)
|
40
|
+
say "Succesfully targeted to [#{target_url}]".green
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def info
|
45
|
+
info = client_info
|
46
|
+
return display JSON.pretty_generate(info) if @options[:json]
|
47
|
+
|
48
|
+
display "\n#{info[:description]}"
|
49
|
+
display "For support visit #{info[:support]}"
|
50
|
+
display ""
|
51
|
+
display "Target: #{target_url} (v#{info[:version]})"
|
52
|
+
display "Client: v#{VMC::Cli::VERSION}"
|
53
|
+
if info[:user]
|
54
|
+
display ''
|
55
|
+
display "User: #{info[:user]}"
|
56
|
+
end
|
57
|
+
if usage = info[:usage] and limits = info[:limits]
|
58
|
+
tmem = pretty_size(limits[:memory]*1024*1024)
|
59
|
+
mem = pretty_size(usage[:memory]*1024*1024)
|
60
|
+
tser = limits[:services]
|
61
|
+
ser = usage[:services]
|
62
|
+
tapps = limits[:apps] || 0
|
63
|
+
apps = usage[:apps] || 0
|
64
|
+
display "Usage: Memory (#{mem} of #{tmem} total)"
|
65
|
+
display " Services (#{ser} of #{tser} total)"
|
66
|
+
display " Apps (#{apps} of #{tapps} total)" if limits[:apps]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def runtimes
|
71
|
+
raise VMC::Client::AuthError unless client.logged_in?
|
72
|
+
return display JSON.pretty_generate(runtimes_info) if @options[:json]
|
73
|
+
return display "No Runtimes" if runtimes_info.empty?
|
74
|
+
rtable = table do |t|
|
75
|
+
t.headings = 'Name', 'Description', 'Version'
|
76
|
+
runtimes_info.each_value { |rt| t << [rt[:name], rt[:description], rt[:version]] }
|
77
|
+
end
|
78
|
+
display "\n"
|
79
|
+
display rtable
|
80
|
+
end
|
81
|
+
|
82
|
+
def frameworks
|
83
|
+
raise VMC::Client::AuthError unless client.logged_in?
|
84
|
+
return display JSON.pretty_generate(frameworks_info) if @options[:json]
|
85
|
+
return display "No Frameworks" if frameworks_info.empty?
|
86
|
+
rtable = table do |t|
|
87
|
+
t.headings = ['Name']
|
88
|
+
frameworks_info.each { |f| t << f }
|
89
|
+
end
|
90
|
+
display "\n"
|
91
|
+
display rtable
|
92
|
+
end
|
93
|
+
|
94
|
+
def aliases
|
95
|
+
aliases = VMC::Cli::Config.aliases
|
96
|
+
return display JSON.pretty_generate(aliases) if @options[:json]
|
97
|
+
return display "No Aliases" if aliases.empty?
|
98
|
+
atable = table do |t|
|
99
|
+
t.headings = 'Alias', 'Command'
|
100
|
+
aliases.each { |k,v| t << [k, v] }
|
101
|
+
end
|
102
|
+
display "\n"
|
103
|
+
display atable
|
104
|
+
end
|
105
|
+
|
106
|
+
def alias(k, v=nil)
|
107
|
+
k,v = k.split('=') unless v
|
108
|
+
aliases = VMC::Cli::Config.aliases
|
109
|
+
aliases[k] = v
|
110
|
+
VMC::Cli::Config.store_aliases(aliases)
|
111
|
+
display "Successfully aliased '#{k}' to '#{v}'".green
|
112
|
+
end
|
113
|
+
|
114
|
+
def unalias(key)
|
115
|
+
aliases = VMC::Cli::Config.aliases
|
116
|
+
if aliases.has_key?(key)
|
117
|
+
aliases.delete(key)
|
118
|
+
VMC::Cli::Config.store_aliases(aliases)
|
119
|
+
display "Successfully unaliased '#{key}'".green
|
120
|
+
else
|
121
|
+
display "Unknown alias '#{key}'".red
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module VMC::Cli::Command
|
2
|
+
|
3
|
+
class Services < Base
|
4
|
+
include VMC::Cli::ServicesHelper
|
5
|
+
|
6
|
+
def services
|
7
|
+
ss = client.services_info
|
8
|
+
ps = client.services
|
9
|
+
ps.sort! {|a, b| a[:name] <=> b[:name] }
|
10
|
+
|
11
|
+
if @options[:json]
|
12
|
+
services = { :system => ss, :provisioned => ps }
|
13
|
+
return display JSON.pretty_generate(services)
|
14
|
+
end
|
15
|
+
display_system_services(ss)
|
16
|
+
display_provisioned_services(ps)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_service(service=nil, name=nil, appname=nil)
|
20
|
+
unless no_prompt || service
|
21
|
+
services = client.services_info
|
22
|
+
err 'No services available to provision' if services.empty?
|
23
|
+
choose do |menu|
|
24
|
+
menu.prompt = 'Please select one you wish to provision: '
|
25
|
+
menu.select_by = :index_or_name
|
26
|
+
services.each do |service_type, value|
|
27
|
+
value.each do |vendor, version|
|
28
|
+
menu.choice(vendor.to_s) { service = vendor.to_s }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
name = @options[:name] unless name
|
34
|
+
unless name
|
35
|
+
name = random_service_name(service)
|
36
|
+
picked_name = true
|
37
|
+
end
|
38
|
+
create_service_banner(service, name, picked_name)
|
39
|
+
appname = @options[:bind] unless appname
|
40
|
+
bind_service_banner(name, appname) if appname
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete_service(service=nil)
|
44
|
+
unless no_prompt || service
|
45
|
+
user_services = client.services
|
46
|
+
err 'No services available to delete' if user_services.empty?
|
47
|
+
choose do |menu|
|
48
|
+
menu.prompt = 'Please select one you wish to delete: '
|
49
|
+
menu.select_by = :index_or_name
|
50
|
+
user_services.each do |s|
|
51
|
+
menu.choice(s[:name]) { service = s[:name] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
err "Service name required." unless service
|
56
|
+
display "Deleting service [#{service}]: ", false
|
57
|
+
client.delete_service(service)
|
58
|
+
display 'OK'.green
|
59
|
+
end
|
60
|
+
|
61
|
+
def bind_service(service, appname)
|
62
|
+
bind_service_banner(service, appname)
|
63
|
+
end
|
64
|
+
|
65
|
+
def unbind_service(service, appname)
|
66
|
+
unbind_service_banner(service, appname)
|
67
|
+
end
|
68
|
+
|
69
|
+
def clone_services(src_app, dest_app)
|
70
|
+
begin
|
71
|
+
src = client.app_info(src_app)
|
72
|
+
dest = client.app_info(dest_app)
|
73
|
+
rescue
|
74
|
+
end
|
75
|
+
|
76
|
+
err "Application '#{src_app}' does not exist" unless src
|
77
|
+
err "Application '#{dest_app}' does not exist" unless dest
|
78
|
+
|
79
|
+
services = src[:services]
|
80
|
+
err 'No services to clone' unless services && !services.empty?
|
81
|
+
services.each { |service| bind_service_banner(service, dest_app, false) }
|
82
|
+
check_app_for_restart(dest_app)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module VMC::Cli::Command
|
2
|
+
|
3
|
+
class User < Base
|
4
|
+
|
5
|
+
def info
|
6
|
+
info = client_info
|
7
|
+
username = info[:user] || 'N/A'
|
8
|
+
return display JSON.pretty_generate([username]) if @options[:json]
|
9
|
+
display "\n[#{username}]"
|
10
|
+
end
|
11
|
+
|
12
|
+
def login(email=nil)
|
13
|
+
email = @options[:email] unless email
|
14
|
+
password = @options[:password]
|
15
|
+
tries ||= 0
|
16
|
+
email = ask("Email: ") unless no_prompt || email
|
17
|
+
password = ask("Password: ") {|q| q.echo = '*'} unless no_prompt || password
|
18
|
+
err "Need a valid email" unless email
|
19
|
+
err "Need a password" unless password
|
20
|
+
login_and_save_token(email, password)
|
21
|
+
say "Successfully logged into [#{target_url}]".green
|
22
|
+
rescue VMC::Client::TargetError
|
23
|
+
display "Problem with login, invalid account or password.".red
|
24
|
+
retry if (tries += 1) < 3 && prompt_ok && !@options[:password]
|
25
|
+
exit 1
|
26
|
+
rescue => e
|
27
|
+
display "Problem with login, #{e}, try again or register for an account.".red
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def logout
|
32
|
+
VMC::Cli::Config.remove_token_file
|
33
|
+
say "Successfully logged out of [#{target_url}]".green
|
34
|
+
end
|
35
|
+
|
36
|
+
def change_password(password=nil)
|
37
|
+
info = client_info
|
38
|
+
email = info[:user]
|
39
|
+
err "Need to be logged in to change password." unless email
|
40
|
+
say "Changing password for '#{email}'\n"
|
41
|
+
unless no_prompt
|
42
|
+
password = ask("New Password: ") {|q| q.echo = '*'}
|
43
|
+
password2 = ask("Verify Password: ") {|q| q.echo = '*'}
|
44
|
+
err "Passwords did not match, try again" if password != password2
|
45
|
+
end
|
46
|
+
err "Password required" unless password
|
47
|
+
client.change_password(password)
|
48
|
+
say "\nSuccessfully changed password".green
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def login_and_save_token(email, password)
|
54
|
+
token = client.login(email, password)
|
55
|
+
VMC::Cli::Config.store_token(token)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/cli/config.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'json/pure'
|
6
|
+
|
7
|
+
module VMC::Cli
|
8
|
+
class Config
|
9
|
+
|
10
|
+
DEFAULT_TARGET = 'aws.af.cm'
|
11
|
+
DEFAULT_SUGGEST = 'af.cm'
|
12
|
+
|
13
|
+
TARGET_FILE = '~/.vmc_target'
|
14
|
+
TOKEN_FILE = '~/.vmc_token'
|
15
|
+
INSTANCES_FILE = '~/.vmc_instances'
|
16
|
+
ALIASES_FILE = '~/.vmc_aliases'
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :colorize
|
20
|
+
attr_accessor :output
|
21
|
+
attr_accessor :trace
|
22
|
+
attr_accessor :nozip
|
23
|
+
attr_reader :suggest_url
|
24
|
+
|
25
|
+
def target_url
|
26
|
+
return @target_url if @target_url
|
27
|
+
target_file = File.expand_path(TARGET_FILE)
|
28
|
+
if File.exists? target_file
|
29
|
+
@target_url = File.read(target_file).strip!
|
30
|
+
ha = @target_url.split('.')
|
31
|
+
ha.shift
|
32
|
+
@suggest_url = ha.join('.')
|
33
|
+
@suggest_url = DEFAULT_SUGGEST if @suggest_url.empty?
|
34
|
+
else
|
35
|
+
@target_url = DEFAULT_TARGET
|
36
|
+
@suggest_url = DEFAULT_SUGGEST
|
37
|
+
end
|
38
|
+
@target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
|
39
|
+
@target_url = @target_url.gsub(/\/+$/, '')
|
40
|
+
@target_url
|
41
|
+
end
|
42
|
+
|
43
|
+
def store_target(target_host)
|
44
|
+
target_file = File.expand_path(TARGET_FILE)
|
45
|
+
File.open(target_file, 'w+') { |f| f.puts target_host }
|
46
|
+
FileUtils.chmod 0600, target_file
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_tokens
|
50
|
+
token_file = File.expand_path(TOKEN_FILE)
|
51
|
+
return nil unless File.exists? token_file
|
52
|
+
contents = File.read(token_file).strip
|
53
|
+
JSON.parse(contents)
|
54
|
+
end
|
55
|
+
|
56
|
+
alias :targets :all_tokens
|
57
|
+
|
58
|
+
def auth_token
|
59
|
+
return @token if @token
|
60
|
+
tokens = all_tokens
|
61
|
+
@token = tokens[target_url] if tokens
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove_token_file
|
65
|
+
FileUtils.rm_f(File.expand_path(TOKEN_FILE))
|
66
|
+
end
|
67
|
+
|
68
|
+
def store_token(token)
|
69
|
+
tokens = all_tokens || {}
|
70
|
+
tokens[target_url] = token
|
71
|
+
token_file = File.expand_path(TOKEN_FILE)
|
72
|
+
File.open(token_file, 'w+') { |f| f.write(tokens.to_json) }
|
73
|
+
FileUtils.chmod 0600, token_file
|
74
|
+
end
|
75
|
+
|
76
|
+
def instances
|
77
|
+
instances_file = File.expand_path(INSTANCES_FILE)
|
78
|
+
return nil unless File.exists? instances_file
|
79
|
+
contents = File.read(instances_file).strip
|
80
|
+
JSON.parse(contents)
|
81
|
+
end
|
82
|
+
|
83
|
+
def store_instances(instances)
|
84
|
+
instances_file = File.expand_path(INSTANCES_FILE)
|
85
|
+
File.open(instances_file, 'w') { |f| f.write(instances.to_json) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def aliases
|
89
|
+
aliases_file = File.expand_path(ALIASES_FILE)
|
90
|
+
# bacward compatible
|
91
|
+
unless File.exists? aliases_file
|
92
|
+
old_aliases_file = File.expand_path('~/.vmc-aliases')
|
93
|
+
FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file
|
94
|
+
end
|
95
|
+
aliases = YAML.load_file(aliases_file) rescue {}
|
96
|
+
end
|
97
|
+
|
98
|
+
def store_aliases(aliases)
|
99
|
+
aliases_file = File.expand_path(ALIASES_FILE)
|
100
|
+
File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)}
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
def initialize(work_dir = Dir.pwd)
|
106
|
+
@work_dir = work_dir
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|