vmc-tsuru 0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cli/config.rb ADDED
@@ -0,0 +1,160 @@
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 = 'api.vcap.me'
11
+
12
+ TARGET_FILE = '~/.vmc_target'
13
+ TOKEN_FILE = '~/.vmc_token'
14
+ INSTANCES_FILE = '~/.vmc_instances'
15
+ ALIASES_FILE = '~/.vmc_aliases'
16
+ CLIENTS_FILE = '~/.vmc_clients'
17
+
18
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
19
+
20
+ class << self
21
+ attr_accessor :colorize
22
+ attr_accessor :output
23
+ attr_accessor :trace
24
+ attr_accessor :nozip
25
+
26
+ def target_url
27
+ return @target_url if @target_url
28
+ target_file = File.expand_path(TARGET_FILE)
29
+ if File.exists? target_file
30
+ @target_url = lock_and_read(target_file).strip
31
+ else
32
+ @target_url = DEFAULT_TARGET
33
+ end
34
+ @target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
35
+ @target_url = @target_url.gsub(/\/+$/, '')
36
+ @target_url
37
+ end
38
+
39
+ def base_of(url)
40
+ url.sub(/^[^\.]+\./, "")
41
+ end
42
+
43
+ def suggest_url
44
+ @suggest_url ||= base_of(target_url)
45
+ end
46
+
47
+ def store_target(target_host)
48
+ target_file = File.expand_path(TARGET_FILE)
49
+ lock_and_write(target_file, target_host)
50
+ end
51
+
52
+ def all_tokens(token_file_path=nil)
53
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
54
+ return nil unless File.exists? token_file
55
+ contents = lock_and_read(token_file).strip
56
+ JSON.parse(contents)
57
+ end
58
+
59
+ alias :targets :all_tokens
60
+
61
+ def auth_token(token_file_path=nil)
62
+ return @token if @token
63
+ tokens = all_tokens(token_file_path)
64
+ @token = tokens[target_url] if tokens
65
+ end
66
+
67
+ def remove_token_file
68
+ FileUtils.rm_f(File.expand_path(TOKEN_FILE))
69
+ end
70
+
71
+ def store_token(token, token_file_path=nil)
72
+ tokens = all_tokens(token_file_path) || {}
73
+ tokens[target_url] = token
74
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
75
+ lock_and_write(token_file, tokens.to_json)
76
+ end
77
+
78
+ def instances
79
+ instances_file = File.expand_path(INSTANCES_FILE)
80
+ return nil unless File.exists? instances_file
81
+ contents = lock_and_read(instances_file).strip
82
+ JSON.parse(contents)
83
+ end
84
+
85
+ def store_instances(instances)
86
+ instances_file = File.expand_path(INSTANCES_FILE)
87
+ lock_and_write(instances_file, instances.to_json)
88
+ end
89
+
90
+ def aliases
91
+ aliases_file = File.expand_path(ALIASES_FILE)
92
+ # bacward compatible
93
+ unless File.exists? aliases_file
94
+ old_aliases_file = File.expand_path('~/.vmc-aliases')
95
+ FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file
96
+ end
97
+ aliases = YAML.load_file(aliases_file) rescue {}
98
+ end
99
+
100
+ def store_aliases(aliases)
101
+ aliases_file = File.expand_path(ALIASES_FILE)
102
+ File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)}
103
+ end
104
+
105
+ def deep_merge(a, b)
106
+ merge = proc do |_, old, new|
107
+ if new.is_a?(Hash) and old.is_a?(Hash)
108
+ old.merge(new, &merge)
109
+ else
110
+ new
111
+ end
112
+ end
113
+
114
+ a.merge(b, &merge)
115
+ end
116
+
117
+ def clients
118
+ return @clients if @clients
119
+
120
+ stock = YAML.load_file(STOCK_CLIENTS)
121
+ clients = File.expand_path CLIENTS_FILE
122
+ if File.exists? clients
123
+ user = YAML.load_file(clients)
124
+ @clients = deep_merge(stock, user)
125
+ else
126
+ @clients = stock
127
+ end
128
+ end
129
+
130
+ def lock_and_read(file)
131
+ File.open(file, File::RDONLY) {|f|
132
+ if defined? JRUBY_VERSION
133
+ f.flock(File::LOCK_SH)
134
+ else
135
+ f.flock(File::LOCK_EX)
136
+ end
137
+ contents = f.read
138
+ f.flock(File::LOCK_UN)
139
+ contents
140
+ }
141
+ end
142
+
143
+ def lock_and_write(file, contents)
144
+ File.open(file, File::RDWR | File::CREAT, 0600) {|f|
145
+ f.flock(File::LOCK_EX)
146
+ f.rewind
147
+ f.puts contents
148
+ f.flush
149
+ f.truncate(f.pos)
150
+ f.flock(File::LOCK_UN)
151
+ }
152
+ end
153
+ end
154
+
155
+ def initialize(work_dir = Dir.pwd)
156
+ @work_dir = work_dir
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,157 @@
1
+ require 'net/telnet'
2
+ require 'readline'
3
+
4
+ module VMC::Cli
5
+ module ConsoleHelper
6
+
7
+ def console_connection_info(appname)
8
+ app = client.app_info(appname)
9
+ fw = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model])
10
+ if !fw.console
11
+ err "'#{appname}' is a #{fw.name} application. " +
12
+ "Console access is not supported for #{fw.name} applications."
13
+ end
14
+ instances_info_envelope = client.app_instances(appname)
15
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
16
+
17
+ instances_info = instances_info_envelope[:instances] || []
18
+ err "No running instances for [#{appname}]" if instances_info.empty?
19
+
20
+ entry = instances_info[0]
21
+ if !entry[:console_port]
22
+ begin
23
+ client.app_files(appname, '/app/cf-rails-console')
24
+ err "Console port not provided for [#{appname}]. Try restarting the app."
25
+ rescue VMC::Client::TargetError, VMC::Client::NotFound
26
+ err "Console access not supported for [#{appname}]. " +
27
+ "Please redeploy your app to enable support."
28
+ end
29
+ end
30
+ conn_info = {'hostname' => entry[:console_ip], 'port' => entry[:console_port]}
31
+ end
32
+
33
+ def start_local_console(port, appname)
34
+ auth_info = console_credentials(appname)
35
+ display "Connecting to '#{appname}' console: ", false
36
+ prompt = console_login(auth_info, port)
37
+ display "OK".green
38
+ display "\n"
39
+ initialize_readline
40
+ run_console prompt
41
+ end
42
+
43
+ def console_login(auth_info, port)
44
+ if !auth_info["username"] || !auth_info["password"]
45
+ err "Unable to verify console credentials."
46
+ end
47
+ @telnet_client = telnet_client(port)
48
+ prompt = nil
49
+ err_msg = "Login attempt timed out."
50
+ 5.times do
51
+ begin
52
+ results = @telnet_client.login("Name"=>auth_info["username"],
53
+ "Password"=>auth_info["password"]) {|line|
54
+ if line =~ /[$%#>] \z/n
55
+ prompt = line
56
+ elsif line =~ /Login failed/
57
+ err_msg = line
58
+ end
59
+ }
60
+ break
61
+ rescue TimeoutError
62
+ sleep 1
63
+ rescue EOFError
64
+ #This may happen if we login right after app starts
65
+ close_console
66
+ sleep 5
67
+ @telnet_client = telnet_client(port)
68
+ end
69
+ display ".", false
70
+ end
71
+ unless prompt
72
+ close_console
73
+ err err_msg
74
+ end
75
+ prompt
76
+ end
77
+
78
+ def send_console_command(cmd)
79
+ results = @telnet_client.cmd(cmd)
80
+ results.split("\n")
81
+ end
82
+
83
+ def console_credentials(appname)
84
+ content = client.app_files(appname, '/app/cf-rails-console/.consoleaccess', '0')
85
+ YAML.load(content)
86
+ end
87
+
88
+ def close_console
89
+ @telnet_client.close
90
+ end
91
+
92
+ def console_tab_completion_data(cmd)
93
+ begin
94
+ results = @telnet_client.cmd("String"=> cmd + "\t", "Match"=>/\S*\n$/, "Timeout"=>10)
95
+ results.chomp.split(",")
96
+ rescue TimeoutError
97
+ [] #Just return empty results if timeout occurred on tab completion
98
+ end
99
+ end
100
+
101
+ private
102
+ def telnet_client(port)
103
+ Net::Telnet.new({"Port"=>port, "Prompt"=>/[$%#>] \z|Login failed/n, "Timeout"=>30, "FailEOF"=>true})
104
+ end
105
+
106
+ def readline_with_history(prompt)
107
+ line = Readline.readline(prompt, true)
108
+ return '' if line.nil?
109
+ #Don't keep blank or repeat commands in history
110
+ if line =~ /^\s*$/ or Readline::HISTORY.to_a[-2] == line
111
+ Readline::HISTORY.pop
112
+ end
113
+ line
114
+ end
115
+
116
+ def run_console(prompt)
117
+ while(cmd = readline_with_history(prompt))
118
+ if(cmd == "exit" || cmd == "quit")
119
+ #TimeoutError expected, as exit doesn't return anything
120
+ @telnet_client.cmd("String"=>cmd,"Timeout"=>1) rescue TimeoutError
121
+ close_console
122
+ break
123
+ end
124
+ if !cmd.empty?
125
+ prompt = send_console_command_display_results(cmd, prompt)
126
+ end
127
+ end
128
+ end
129
+
130
+ def send_console_command_display_results(cmd, prompt)
131
+ begin
132
+ lines = send_console_command cmd
133
+ #Assumes the last line is a prompt
134
+ prompt = lines.pop
135
+ lines.each {|line| display line if line != cmd}
136
+ rescue TimeoutError
137
+ display "Timed out sending command to server.".red
138
+ rescue EOFError
139
+ err "The console connection has been terminated. Perhaps the app was stopped or deleted?"
140
+ end
141
+ prompt
142
+ end
143
+
144
+ def initialize_readline
145
+ if Readline.respond_to?("basic_word_break_characters=")
146
+ Readline.basic_word_break_characters= " \t\n`><=;|&{("
147
+ end
148
+ Readline.completion_append_character = nil
149
+ #Assumes that sending a String ending with tab will return a non-empty
150
+ #String of comma-separated completion options, terminated by a new line
151
+ #For example, "app.\t" might result in "to_s,nil?,etc\n"
152
+ Readline.completion_proc = proc {|s|
153
+ console_tab_completion_data s
154
+ }
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,122 @@
1
+ module VMCExtensions
2
+
3
+ def say(message)
4
+ VMC::Cli::Config.output.puts(message) if VMC::Cli::Config.output
5
+ end
6
+
7
+ def header(message, filler = '-')
8
+ say "\n"
9
+ say message
10
+ say filler.to_s * message.size
11
+ end
12
+
13
+ def banner(message)
14
+ say "\n"
15
+ say message
16
+ end
17
+
18
+ def display(message, nl=true)
19
+ if nl
20
+ say message
21
+ else
22
+ if VMC::Cli::Config.output
23
+ VMC::Cli::Config.output.print(message)
24
+ VMC::Cli::Config.output.flush
25
+ end
26
+ end
27
+ end
28
+
29
+ def clear(size=80)
30
+ return unless VMC::Cli::Config.output
31
+ VMC::Cli::Config.output.print("\r")
32
+ VMC::Cli::Config.output.print(" " * size)
33
+ VMC::Cli::Config.output.print("\r")
34
+ #VMC::Cli::Config.output.flush
35
+ end
36
+
37
+ def err(message, prefix='Error: ')
38
+ raise VMC::Cli::CliExit, "#{prefix}#{message}"
39
+ end
40
+
41
+ def warn(msg)
42
+ say "#{"[WARNING]".yellow} #{msg}"
43
+ end
44
+
45
+ def quit(message = nil)
46
+ raise VMC::Cli::GracefulExit, message
47
+ end
48
+
49
+ def blank?
50
+ self.to_s.blank?
51
+ end
52
+
53
+ def uptime_string(delta)
54
+ num_seconds = delta.to_i
55
+ days = num_seconds / (60 * 60 * 24);
56
+ num_seconds -= days * (60 * 60 * 24);
57
+ hours = num_seconds / (60 * 60);
58
+ num_seconds -= hours * (60 * 60);
59
+ minutes = num_seconds / 60;
60
+ num_seconds -= minutes * 60;
61
+ "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
62
+ end
63
+
64
+ def pretty_size(size, prec=1)
65
+ return 'NA' unless size
66
+ return "#{size}B" if size < 1024
67
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
68
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
69
+ return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
70
+ end
71
+ end
72
+
73
+ module VMCStringExtensions
74
+
75
+ def red
76
+ colorize("\e[0m\e[31m")
77
+ end
78
+
79
+ def green
80
+ colorize("\e[0m\e[32m")
81
+ end
82
+
83
+ def yellow
84
+ colorize("\e[0m\e[33m")
85
+ end
86
+
87
+ def bold
88
+ colorize("\e[0m\e[1m")
89
+ end
90
+
91
+ def colorize(color_code)
92
+ if VMC::Cli::Config.colorize
93
+ "#{color_code}#{self}\e[0m"
94
+ else
95
+ self
96
+ end
97
+ end
98
+
99
+ def blank?
100
+ self =~ /^\s*$/
101
+ end
102
+
103
+ def truncate(limit = 30)
104
+ return "" if self.blank?
105
+ etc = "..."
106
+ stripped = self.strip[0..limit]
107
+ if stripped.length > limit
108
+ stripped.gsub(/\s+?(\S+)?$/, "") + etc
109
+ else
110
+ stripped
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+ class Object
117
+ include VMCExtensions
118
+ end
119
+
120
+ class String
121
+ include VMCStringExtensions
122
+ end
data/lib/cli/errors.rb ADDED
@@ -0,0 +1,19 @@
1
+ module VMC::Cli
2
+
3
+ class CliError < StandardError
4
+ def self.error_code(code = nil)
5
+ define_method(:error_code) { code }
6
+ end
7
+ end
8
+
9
+ class UnknownCommand < CliError; error_code(100); end
10
+ class TargetMissing < CliError; error_code(102); end
11
+ class TargetInaccessible < CliError; error_code(103); end
12
+
13
+ class TargetError < CliError; error_code(201); end
14
+ class AuthError < TargetError; error_code(202); end
15
+
16
+ class CliExit < CliError; error_code(400); end
17
+ class GracefulExit < CliExit; error_code(401); end
18
+
19
+ end
@@ -0,0 +1,133 @@
1
+ module VMC::Cli
2
+
3
+ class Framework
4
+
5
+ DEFAULT_FRAMEWORK = "http://b20nine.com/unknown"
6
+ DEFAULT_MEM = '256M'
7
+
8
+ FRAMEWORKS = {
9
+ 'Rails' => ['rails3', { :mem => '256M', :description => 'Rails Application', :console=>true}],
10
+ 'Spring' => ['spring', { :mem => '512M', :description => 'Java SpringSource Spring Application'}],
11
+ 'Grails' => ['grails', { :mem => '512M', :description => 'Java SpringSource Grails Application'}],
12
+ 'Lift' => ['lift', { :mem => '512M', :description => 'Scala Lift Application'}],
13
+ 'JavaWeb' => ['java_web',{ :mem => '512M', :description => 'Java Web Application'}],
14
+ 'Sinatra' => ['sinatra', { :mem => '128M', :description => 'Sinatra Application'}],
15
+ 'Node' => ['node', { :mem => '64M', :description => 'Node.js Application'}],
16
+ 'PHP' => ['php', { :mem => '128M', :description => 'PHP Application'}],
17
+ 'Erlang/OTP Rebar' => ['otp_rebar', { :mem => '64M', :description => 'Erlang/OTP Rebar Application'}],
18
+ 'WSGI' => ['wsgi', { :mem => '64M', :description => 'Python WSGI Application'}],
19
+ 'Django' => ['django', { :mem => '128M', :description => 'Python Django Application'}],
20
+ }
21
+
22
+ class << self
23
+
24
+ def known_frameworks
25
+ FRAMEWORKS.keys
26
+ end
27
+
28
+ def lookup(name)
29
+ return Framework.new(*FRAMEWORKS[name])
30
+ end
31
+
32
+ def lookup_by_framework(name)
33
+ FRAMEWORKS.each do |key,fw|
34
+ return Framework.new(fw[0], fw[1]) if fw[0] == name
35
+ end
36
+ end
37
+
38
+ def detect(path)
39
+ Dir.chdir(path) do
40
+
41
+ # Rails
42
+ if File.exist?('config/environment.rb')
43
+ return Framework.lookup('Rails')
44
+
45
+ # Java
46
+ elsif Dir.glob('*.war').first || File.exist?('WEB-INF/web.xml')
47
+ war_file = Dir.glob('*.war').first
48
+
49
+ if war_file
50
+ contents = ZipUtil.entry_lines(war_file)
51
+ else
52
+ contents = Dir['**/*'].join("\n")
53
+ end
54
+
55
+ # Spring/Lift Variations
56
+ if contents =~ /WEB-INF\/lib\/grails-web.*\.jar/
57
+ return Framework.lookup('Grails')
58
+ elsif contents =~ /WEB-INF\/lib\/lift-webkit.*\.jar/
59
+ return Framework.lookup('Lift')
60
+ elsif contents =~ /WEB-INF\/classes\/org\/springframework/
61
+ return Framework.lookup('Spring')
62
+ elsif contents =~ /WEB-INF\/lib\/spring-core.*\.jar/
63
+ return Framework.lookup('Spring')
64
+ elsif contents =~ /WEB-INF\/lib\/org\.springframework\.core.*\.jar/
65
+ return Framework.lookup('Spring')
66
+ else
67
+ return Framework.lookup('JavaWeb')
68
+ end
69
+ # Simple Ruby Apps
70
+ elsif !Dir.glob('*.rb').empty?
71
+ matched_file = nil
72
+ Dir.glob('*.rb').each do |fname|
73
+ next if matched_file
74
+ File.open(fname, 'r') do |f|
75
+ str = f.read # This might want to be limited
76
+ matched_file = fname if (str && str.match(/^\s*require[\s\(]*['"]sinatra['"]/))
77
+ end
78
+ end
79
+ if matched_file
80
+ f = Framework.lookup('Sinatra')
81
+ f.exec = "ruby #{matched_file}"
82
+ return f
83
+ end
84
+
85
+ # Node.js
86
+ elsif !Dir.glob('*.js').empty?
87
+ if File.exist?('server.js') || File.exist?('app.js') || File.exist?('index.js') || File.exist?('main.js')
88
+ return Framework.lookup('Node')
89
+ end
90
+
91
+ # PHP
92
+ elsif !Dir.glob('*.php').empty?
93
+ return Framework.lookup('PHP')
94
+
95
+ # Erlang/OTP using Rebar
96
+ elsif !Dir.glob('releases/*/*.rel').empty? && !Dir.glob('releases/*/*.boot').empty?
97
+ return Framework.lookup('Erlang/OTP Rebar')
98
+
99
+ # Python Django
100
+ # XXX: not all django projects keep settings.py in top-level directory
101
+ elsif File.exist?('manage.py') && File.exist?('settings.py')
102
+ return Framework.lookup('Django')
103
+
104
+ # Python
105
+ elsif !Dir.glob('wsgi.py').empty?
106
+ return Framework.lookup('WSGI')
107
+
108
+ end
109
+ end
110
+ nil
111
+ end
112
+
113
+ end
114
+
115
+ attr_reader :name, :description, :memory, :console
116
+ attr_accessor :exec
117
+
118
+ alias :mem :memory
119
+
120
+ def initialize(framework=nil, opts={})
121
+ @name = framework || DEFAULT_FRAMEWORK
122
+ @memory = opts[:mem] || DEFAULT_MEM
123
+ @description = opts[:description] || 'Unknown Application Type'
124
+ @exec = opts[:exec]
125
+ @console = opts[:console] || false
126
+ end
127
+
128
+ def to_s
129
+ description
130
+ end
131
+ end
132
+
133
+ end