paasio 0.3.16.beta.2

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,165 @@
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.paas.io'
11
+ DEFAULT_SUGGEST = 'paas.io'
12
+
13
+ TARGET_FILE = '~/.paasio_target'
14
+ TOKEN_FILE = '~/.paasio_token'
15
+ INSTANCES_FILE = '~/.paasio_instances'
16
+ ALIASES_FILE = '~/.paasio_aliases'
17
+ CLIENTS_FILE = '~/.paasio_clients'
18
+
19
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
20
+
21
+ class << self
22
+ attr_accessor :colorize
23
+ attr_accessor :output
24
+ attr_accessor :trace
25
+ attr_accessor :nozip
26
+
27
+ def target_url
28
+ return @target_url if @target_url
29
+ target_file = File.expand_path(TARGET_FILE)
30
+ if File.exists? target_file
31
+ @target_url = lock_and_read(target_file).strip
32
+ else
33
+ @target_url = DEFAULT_TARGET
34
+ end
35
+ @target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
36
+ @target_url = @target_url.gsub(/\/+$/, '')
37
+ @target_url
38
+ end
39
+
40
+ def base_of(url)
41
+ url.sub(/^[^\.]+\./, "")
42
+ end
43
+
44
+ def suggest_url
45
+ return @suggest_url if @suggest_url
46
+ ha = target_url.split('.')
47
+ ha.shift
48
+ @suggest_url = ha.join('.')
49
+ @suggest_url = DEFAULT_SUGGEST if @suggest_url.empty?
50
+ @suggest_url
51
+ end
52
+
53
+ def store_target(target_host)
54
+ target_file = File.expand_path(TARGET_FILE)
55
+ lock_and_write(target_file, target_host)
56
+ end
57
+
58
+ def all_tokens(token_file_path=nil)
59
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
60
+ return nil unless File.exists? token_file
61
+ contents = lock_and_read(token_file).strip
62
+ JSON.parse(contents)
63
+ end
64
+
65
+ alias :targets :all_tokens
66
+
67
+ def auth_token(token_file_path=nil)
68
+ return @token if @token
69
+ tokens = all_tokens(token_file_path)
70
+ @token = tokens[target_url] if tokens
71
+ end
72
+
73
+ def remove_token_file
74
+ FileUtils.rm_f(File.expand_path(TOKEN_FILE))
75
+ end
76
+
77
+ def store_token(token, token_file_path=nil)
78
+ tokens = all_tokens(token_file_path) || {}
79
+ tokens[target_url] = token
80
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
81
+ lock_and_write(token_file, tokens.to_json)
82
+ end
83
+
84
+ def instances
85
+ instances_file = File.expand_path(INSTANCES_FILE)
86
+ return nil unless File.exists? instances_file
87
+ contents = lock_and_read(instances_file).strip
88
+ JSON.parse(contents)
89
+ end
90
+
91
+ def store_instances(instances)
92
+ instances_file = File.expand_path(INSTANCES_FILE)
93
+ lock_and_write(instances_file, instances.to_json)
94
+ end
95
+
96
+ def aliases
97
+ aliases_file = File.expand_path(ALIASES_FILE)
98
+ # bacward compatible
99
+ unless File.exists? aliases_file
100
+ old_aliases_file = File.expand_path('~/.vmc-aliases')
101
+ FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file
102
+ end
103
+ aliases = YAML.load_file(aliases_file) rescue {}
104
+ end
105
+
106
+ def store_aliases(aliases)
107
+ aliases_file = File.expand_path(ALIASES_FILE)
108
+ File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)}
109
+ end
110
+
111
+ def deep_merge(a, b)
112
+ merge = proc do |_, old, new|
113
+ if new.is_a?(Hash) and old.is_a?(Hash)
114
+ old.merge(new, &merge)
115
+ else
116
+ new
117
+ end
118
+ end
119
+
120
+ a.merge(b, &merge)
121
+ end
122
+
123
+ def clients
124
+ return @clients if @clients
125
+
126
+ stock = YAML.load_file(STOCK_CLIENTS)
127
+ if File.exists? CLIENTS_FILE
128
+ user = YAML.load_file(CLIENTS_FILE)
129
+ @clients = deep_merge(stock, user)
130
+ else
131
+ @clients = stock
132
+ end
133
+ end
134
+
135
+ def lock_and_read(file)
136
+ File.open(file, File::RDONLY) {|f|
137
+ if defined? JRUBY_VERSION
138
+ f.flock(File::LOCK_SH)
139
+ else
140
+ f.flock(File::LOCK_EX)
141
+ end
142
+ contents = f.read
143
+ f.flock(File::LOCK_UN)
144
+ contents
145
+ }
146
+ end
147
+
148
+ def lock_and_write(file, contents)
149
+ File.open(file, File::RDWR | File::CREAT, 0600) {|f|
150
+ f.flock(File::LOCK_EX)
151
+ f.rewind
152
+ f.puts contents
153
+ f.flush
154
+ f.truncate(f.pos)
155
+ f.flock(File::LOCK_UN)
156
+ }
157
+ end
158
+ end
159
+
160
+ def initialize(work_dir = Dir.pwd)
161
+ @work_dir = work_dir
162
+ end
163
+
164
+ end
165
+ 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,131 @@
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'}],
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
+ 'Rack' => ['rack', { :mem => '128M', :description => 'Rack Application'}],
16
+ 'Node' => ['node', { :mem => '128M', :description => 'Node.js Application'}],
17
+ 'PHP' => ['php', { :mem => '128M', :description => 'PHP Application'}],
18
+ 'Erlang/OTP Rebar' => ['otp_rebar', { :mem => '128M', :description => 'Erlang/OTP Rebar Application'}],
19
+ 'WSGI' => ['wsgi', { :mem => '128M', :description => 'Python WSGI Application'}],
20
+ 'Django' => ['django', { :mem => '128M', :description => 'Python Django Application'}],
21
+ }
22
+
23
+ class << self
24
+
25
+ def known_frameworks
26
+ FRAMEWORKS.keys
27
+ end
28
+
29
+ def lookup(name)
30
+ return Framework.new(*FRAMEWORKS[name])
31
+ end
32
+
33
+ def detect(path)
34
+ Dir.chdir(path) do
35
+
36
+ # Rails
37
+ if File.exist?('config/environment.rb')
38
+ return Framework.lookup('Rails')
39
+
40
+ # Java
41
+ elsif Dir.glob('*.war').first || File.exist?('WEB-INF/web.xml')
42
+ war_file = Dir.glob('*.war').first
43
+
44
+ if war_file
45
+ contents = ZipUtil.entry_lines(war_file)
46
+ else
47
+ contents = Dir['**/*'].join("\n")
48
+ end
49
+
50
+ # Spring/Lift Variations
51
+ if contents =~ /WEB-INF\/lib\/grails-web.*\.jar/
52
+ return Framework.lookup('Grails')
53
+ elsif contents =~ /WEB-INF\/lib\/lift-webkit.*\.jar/
54
+ return Framework.lookup('Lift')
55
+ elsif contents =~ /WEB-INF\/classes\/org\/springframework/
56
+ return Framework.lookup('Spring')
57
+ elsif contents =~ /WEB-INF\/lib\/spring-core.*\.jar/
58
+ return Framework.lookup('Spring')
59
+ elsif contents =~ /WEB-INF\/lib\/org\.springframework\.core.*\.jar/
60
+ return Framework.lookup('Spring')
61
+ else
62
+ return Framework.lookup('JavaWeb')
63
+ end
64
+
65
+ elsif !Dir.glob('config.ru').empty?
66
+ return Framework.lookup('Rack')
67
+
68
+ # Sinatra-based Apps
69
+ elsif !Dir.glob('*.rb').empty?
70
+ matched_file = nil
71
+ Dir.glob('*.rb').each do |fname|
72
+ next if matched_file
73
+ File.open(fname, 'r') do |f|
74
+ str = f.read # This might want to be limited
75
+ matched_file = fname if (str && str.match(/^\s*require[\s\(]*['"]sinatra['"]/))
76
+ end
77
+ end
78
+ if matched_file
79
+ f = Framework.lookup('Sinatra')
80
+ f.exec = "ruby #{matched_file}"
81
+ return f
82
+ end
83
+
84
+ # Node.js
85
+ elsif !Dir.glob('*.js').empty?
86
+ if File.exist?('server.js') || File.exist?('app.js') || File.exist?('index.js') || File.exist?('main.js')
87
+ return Framework.lookup('Node')
88
+ end
89
+
90
+ # PHP
91
+ elsif !Dir.glob('*.php').empty?
92
+ return Framework.lookup('PHP')
93
+
94
+ # Erlang/OTP using Rebar
95
+ elsif !Dir.glob('releases/*/*.rel').empty? && !Dir.glob('releases/*/*.boot').empty?
96
+ return Framework.lookup('Erlang/OTP Rebar')
97
+
98
+ # Python Django
99
+ # XXX: not all django projects keep settings.py in top-level directory
100
+ elsif File.exist?('manage.py') && File.exist?('settings.py')
101
+ return Framework.lookup('Django')
102
+
103
+ # Python
104
+ elsif !Dir.glob('wsgi.py').empty?
105
+ return Framework.lookup('WSGI')
106
+
107
+ end
108
+ end
109
+ nil
110
+ end
111
+
112
+ end
113
+
114
+ attr_reader :name, :description, :memory
115
+ attr_accessor :exec
116
+
117
+ alias :mem :memory
118
+
119
+ def initialize(framework=nil, opts={})
120
+ @name = framework || DEFAULT_FRAMEWORK
121
+ @memory = opts[:mem] || DEFAULT_MEM
122
+ @description = opts[:description] || 'Unknown Application Type'
123
+ @exec = opts[:exec]
124
+ end
125
+
126
+ def to_s
127
+ description
128
+ end
129
+ end
130
+
131
+ end
@@ -0,0 +1,238 @@
1
+ require "set"
2
+
3
+ module VMC::Cli::ManifestHelper
4
+ include VMC::Cli::ServicesHelper
5
+
6
+ DEFAULTS = {
7
+ "url" => "${name}.${target-base}",
8
+ "mem" => "128M",
9
+ "instances" => 1
10
+ }
11
+
12
+ MANIFEST = "manifest.yml"
13
+
14
+ YES_SET = Set.new(["y", "Y", "yes", "YES"])
15
+
16
+ # take a block and call it once for each app to push/update.
17
+ # with @application and @app_info set appropriately
18
+ def each_app(panic=true)
19
+ if @manifest and all_apps = @manifest["applications"]
20
+ where = File.expand_path(@path)
21
+ single = false
22
+
23
+ all_apps.each do |path, info|
24
+ app = File.expand_path("../" + path, manifest_file)
25
+ if where.start_with?(app)
26
+ @application = app
27
+ @app_info = info
28
+ yield info["name"]
29
+ single = true
30
+ break
31
+ end
32
+ end
33
+
34
+ unless single
35
+ if where == File.expand_path("../", manifest_file)
36
+ ordered_by_deps(all_apps).each do |path, info|
37
+ app = File.expand_path("../" + path, manifest_file)
38
+ @application = app
39
+ @app_info = info
40
+ yield info["name"]
41
+ end
42
+ else
43
+ err "Path '#{@path}' is not known to manifest '#{manifest_file}'."
44
+ end
45
+ end
46
+ else
47
+ @application = @path
48
+ @app_info = @manifest
49
+ if @app_info
50
+ yield @app_info["name"]
51
+ elsif panic
52
+ err "No applications."
53
+ end
54
+ end
55
+
56
+ nil
57
+ ensure
58
+ @application = nil
59
+ @app_info = nil
60
+ end
61
+
62
+ def interact(many=false)
63
+ @manifest ||= {}
64
+ configure_app(many)
65
+ end
66
+
67
+ def target_manifest
68
+ @options[:manifest] || MANIFEST
69
+ end
70
+
71
+ def save_manifest(save_to = nil)
72
+ save_to ||= target_manifest
73
+
74
+ File.open(save_to, "w") do |f|
75
+ f.write @manifest.to_yaml
76
+ end
77
+
78
+ say "Manifest written to #{save_to}."
79
+ end
80
+
81
+ def configure_app(many=false)
82
+ name = manifest("name") ||
83
+ set(ask("Application Name", :default => manifest("name")), "name")
84
+
85
+ url_template = manifest("url") || DEFAULTS["url"]
86
+ url_resolved = url_template.dup
87
+ resolve_lexically(url_resolved)
88
+
89
+ url = ask("Application Deployed URL", :default => url_resolved)
90
+
91
+ url = url_template if url == url_resolved
92
+
93
+ # common error case is for prompted users to answer y or Y or yes or
94
+ # YES to this ask() resulting in an unintended URL of y. Special
95
+ # case this common error
96
+ url = DEFAULTS["url"] if YES_SET.member? url
97
+
98
+ set url, "url"
99
+
100
+ unless manifest "framework"
101
+ framework = detect_framework
102
+ set framework.name, "framework", "name"
103
+ set(
104
+ { "mem" => framework.mem,
105
+ "description" => framework.description,
106
+ "exec" => framework.exec
107
+ },
108
+ "framework",
109
+ "info"
110
+ )
111
+ end
112
+
113
+ set ask(
114
+ "Memory reservation",
115
+ :default =>
116
+ manifest("mem") ||
117
+ manifest("framework", "info", "mem") ||
118
+ DEFAULTS["mem"],
119
+ :choices => ["128M", "256M", "512M", "1G", "2G"]
120
+ ), "mem"
121
+
122
+ set ask(
123
+ "How many instances?",
124
+ :default => manifest("instances") || DEFAULTS["instances"]
125
+ ), "instances"
126
+
127
+ unless manifest "services"
128
+ services = client.services_info
129
+ unless services.empty?
130
+ bind = ask "Would you like to bind any services to '#{name}'?", :default => false
131
+ bind_services(services.values.collect(&:keys).flatten) if bind
132
+ end
133
+ end
134
+
135
+ if many and ask("Configure for another application?", :default => false)
136
+ @application = ask "Application path?"
137
+ configure_app
138
+ end
139
+ end
140
+
141
+ def set(what, *where)
142
+ where.unshift "applications", @application
143
+
144
+ which = @manifest
145
+ where.each_with_index do |k, i|
146
+ if i + 1 == where.size
147
+ which[k] = what
148
+ else
149
+ which = (which[k] ||= {})
150
+ end
151
+ end
152
+
153
+ what
154
+ end
155
+
156
+ # Detect the appropriate framework.
157
+ def detect_framework(prompt_ok = true)
158
+ framework = VMC::Cli::Framework.detect(@application)
159
+ framework_correct = ask("Detected a #{framework}, is this correct?", :default => true) if prompt_ok && framework
160
+ if prompt_ok && (framework.nil? || !framework_correct)
161
+ display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
162
+ framework = nil if !framework_correct
163
+ framework = VMC::Cli::Framework.lookup(
164
+ ask(
165
+ "Select Application Type",
166
+ :indexed => true,
167
+ :default => framework,
168
+ :choices => VMC::Cli::Framework.known_frameworks
169
+ )
170
+ )
171
+ display "Selected #{framework}"
172
+ end
173
+
174
+ framework
175
+ end
176
+
177
+ def bind_services(services)
178
+ svcs = services.collect(&:to_s).sort!
179
+
180
+ display "The following system services are available"
181
+ configure_service(
182
+ ask(
183
+ "Please select the one you wish to provision",
184
+ :indexed => true,
185
+ :choices => svcs
186
+ ).to_sym
187
+ )
188
+
189
+ if ask "Would you like to bind another service?", :default => false
190
+ bind_services(services)
191
+ end
192
+ end
193
+
194
+ def configure_service(vendor)
195
+ default_name = random_service_name(vendor)
196
+ name = ask "Specify the name of the service", :default => default_name
197
+
198
+ set vendor, "services", name, "type"
199
+ end
200
+
201
+ private
202
+ def ordered_by_deps(apps, abspaths = nil, processed = Set[])
203
+ unless abspaths
204
+ abspaths = {}
205
+ apps.each do |p, i|
206
+ ep = File.expand_path("../" + p, manifest_file)
207
+ abspaths[ep] = i
208
+ end
209
+ end
210
+
211
+ ordered = []
212
+ apps.each do |path, info|
213
+ epath = File.expand_path("../" + path, manifest_file)
214
+
215
+ if deps = info["depends-on"]
216
+ dep_apps = {}
217
+ deps.each do |dep|
218
+ edep = File.expand_path("../" + dep, manifest_file)
219
+
220
+ err "Circular dependency detected." if processed.include? edep
221
+
222
+ dep_apps[dep] = abspaths[edep]
223
+ end
224
+
225
+ processed.add(epath)
226
+
227
+ ordered += ordered_by_deps(dep_apps, abspaths, processed)
228
+ ordered << [path, info]
229
+ elsif not processed.include? epath
230
+ ordered << [path, info]
231
+ processed.add(epath)
232
+ end
233
+ end
234
+
235
+ ordered
236
+ end
237
+
238
+ end