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
@@ -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
|