paasio 0.3.16.beta.2

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