nimbu 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/nimbu ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # resolve bin path, ignoring symlinks
4
+ require "pathname"
5
+ bin_file = Pathname.new(__FILE__).realpath
6
+
7
+ # add self to libpath
8
+ $:.unshift File.expand_path("../../lib", bin_file)
9
+
10
+ # start up the CLI
11
+ require "nimbu/cli"
12
+ Nimbu::CLI.start(*ARGV)
data/lib/nimbu.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "nimbu/version"
2
+ require "nimbu/client"
3
+
4
+ module Nimbu
5
+ # Your code goes here...
6
+ end
data/lib/nimbu/auth.rb ADDED
@@ -0,0 +1,268 @@
1
+ require "yaml"
2
+ require "nimbu"
3
+ require "nimbu/client"
4
+ require "nimbu/helpers"
5
+
6
+ class Nimbu::Auth
7
+ class << self
8
+
9
+ include Nimbu::Helpers
10
+ attr_accessor :credentials
11
+ attr_accessor :configuration
12
+
13
+ def client
14
+ @client ||= begin
15
+ client = Nimbu::Client.new(user, password, host)
16
+ client.on_warning { |msg| self.display("\n#{msg}\n\n") }
17
+ client
18
+ end
19
+ end
20
+
21
+ def login
22
+ delete_credentials
23
+ get_credentials
24
+ end
25
+
26
+ def logout
27
+ delete_credentials
28
+ end
29
+
30
+ # just a stub; will raise if not authenticated
31
+ def check
32
+ client.list
33
+ end
34
+
35
+ def default_host
36
+ "getnimbu.com"
37
+ end
38
+
39
+ def host
40
+ @host ||= ENV['NIMBU_HOST'] || get_nimbu_host
41
+ end
42
+
43
+ def theme
44
+ @theme ||= ENV['NIMBU_THEME'] || get_nimbu_theme
45
+ end
46
+
47
+ def get_nimbu_host
48
+ get_configuration[:hostname]
49
+ end
50
+
51
+ def get_nimbu_theme
52
+ get_configuration[:theme]
53
+ end
54
+
55
+ def get_configuration # :nodoc:
56
+ @configuration ||= (read_configuration || ask_for_and_save_configuration)
57
+ end
58
+
59
+ def ask_for_and_save_configuration
60
+ @configuration = ask_for_configuration
61
+ write_configuration
62
+ @configuration
63
+ end
64
+
65
+ def configuration_file
66
+ "#{Dir.pwd}/nimbu.yml"
67
+ end
68
+
69
+ def delete_configuration
70
+ FileUtils.rm_f(configuration_file)
71
+ @host = nil
72
+ end
73
+
74
+ def ask_for_configuration
75
+ puts "What is the hostname for this Nimbu site?"
76
+ print "Hostname: "
77
+ hostname = ask
78
+
79
+ puts "What is the theme you are developing in this directory?"
80
+ print "Theme (i.e. default): "
81
+ theme = ask
82
+
83
+ {:hostname => hostname, :theme => theme}
84
+ end
85
+
86
+ def read_configuration
87
+ File.exists?(configuration_file) and YAML::load(File.open( configuration_file ))
88
+ end
89
+
90
+ def write_configuration
91
+ FileUtils.mkdir_p(File.dirname(configuration_file))
92
+ File.open(configuration_file, 'w') {|credentials| credentials.puts(YAML.dump(self.configuration))}
93
+ FileUtils.chmod(0700, File.dirname(configuration_file))
94
+ FileUtils.chmod(0600, configuration_file)
95
+ end
96
+
97
+ def reauthorize
98
+ @credentials = ask_for_and_save_credentials
99
+ end
100
+
101
+ def user # :nodoc:
102
+ get_credentials[0]
103
+ end
104
+
105
+ def password # :nodoc:
106
+ get_credentials[1]
107
+ end
108
+
109
+ def api_key
110
+ Nimbu::Client.auth(user, password)["api_key"]
111
+ end
112
+
113
+ def credentials_file
114
+ if host == default_host
115
+ "#{home_directory}/.nimbu/credentials"
116
+ else
117
+ "#{home_directory}/.nimbu/credentials.#{CGI.escape(host)}"
118
+ end
119
+ end
120
+
121
+ def get_credentials # :nodoc:
122
+ @credentials ||= (read_credentials || ask_for_and_save_credentials)
123
+ end
124
+
125
+ def delete_credentials
126
+ FileUtils.rm_f(credentials_file)
127
+ @client, @credentials = nil, nil
128
+ end
129
+
130
+ def read_credentials
131
+ if ENV['NIMBU_API_KEY']
132
+ ['', ENV['NIMBU_API_KEY']]
133
+ else
134
+ File.exists?(credentials_file) and File.read(credentials_file).split("\n")
135
+ end
136
+ end
137
+
138
+ def write_credentials
139
+ FileUtils.mkdir_p(File.dirname(credentials_file))
140
+ File.open(credentials_file, 'w') {|credentials| credentials.puts(self.credentials)}
141
+ FileUtils.chmod(0700, File.dirname(credentials_file))
142
+ FileUtils.chmod(0600, credentials_file)
143
+ end
144
+
145
+ def echo_off
146
+ with_tty do
147
+ system "stty -echo"
148
+ end
149
+ end
150
+
151
+ def echo_on
152
+ with_tty do
153
+ system "stty echo"
154
+ end
155
+ end
156
+
157
+ def ask_for_credentials
158
+ puts "Enter your Nimbu credentials."
159
+
160
+ print "Email: "
161
+ user = ask
162
+
163
+ print "Password: "
164
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
165
+ api_key = Nimbu::Client.auth(user, password)['api_key']
166
+
167
+ [user, api_key]
168
+ end
169
+
170
+ def ask_for_password_on_windows
171
+ require "Win32API"
172
+ char = nil
173
+ password = ''
174
+
175
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
176
+ break if char == 10 || char == 13 # received carriage return or newline
177
+ if char == 127 || char == 8 # backspace and delete
178
+ password.slice!(-1, 1)
179
+ else
180
+ # windows might throw a -1 at us so make sure to handle RangeError
181
+ (password << char.chr) rescue RangeError
182
+ end
183
+ end
184
+ puts
185
+ return password
186
+ end
187
+
188
+ def ask_for_password
189
+ echo_off
190
+ trap("INT") do
191
+ echo_on
192
+ exit
193
+ end
194
+ password = ask
195
+ puts
196
+ echo_on
197
+ return password
198
+ end
199
+
200
+ def ask_for_and_save_credentials
201
+ begin
202
+ @credentials = ask_for_credentials
203
+ write_credentials
204
+ check
205
+ rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
206
+ delete_credentials
207
+ display "Authentication failed."
208
+ retry if retry_login?
209
+ exit 1
210
+ rescue Exception => e
211
+ delete_credentials
212
+ raise e
213
+ end
214
+ @credentials
215
+ end
216
+
217
+ def check_for_associated_ssh_key
218
+ return unless client.keys.empty?
219
+ associate_or_generate_ssh_key
220
+ end
221
+
222
+ def associate_or_generate_ssh_key
223
+ public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort
224
+
225
+ case public_keys.length
226
+ when 0 then
227
+ display "Could not find an existing public key."
228
+ display "Would you like to generate one? [Yn] ", false
229
+ unless ask.strip.downcase == "n"
230
+ display "Generating new SSH public key."
231
+ generate_ssh_key("id_rsa")
232
+ associate_key("#{home_directory}/.ssh/id_rsa.pub")
233
+ end
234
+ when 1 then
235
+ display "Found existing public key: #{public_keys.first}"
236
+ associate_key(public_keys.first)
237
+ else
238
+ display "Found the following SSH public keys:"
239
+ public_keys.each_with_index do |key, index|
240
+ display "#{index+1}) #{File.basename(key)}"
241
+ end
242
+ display "Which would you like to use with your Nimbu account? ", false
243
+ chosen = public_keys[ask.to_i-1] rescue error("Invalid choice")
244
+ associate_key(chosen)
245
+ end
246
+ end
247
+
248
+ def generate_ssh_key(keyfile)
249
+ ssh_dir = File.join(home_directory, ".ssh")
250
+ unless File.exists?(ssh_dir)
251
+ FileUtils.mkdir_p ssh_dir
252
+ File.chmod(0700, ssh_dir)
253
+ end
254
+ `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
255
+ end
256
+
257
+ def associate_key(key)
258
+ display "Uploading SSH public key #{key}"
259
+ client.add_key(File.read(key))
260
+ end
261
+
262
+ def retry_login?
263
+ @login_attempts ||= 0
264
+ @login_attempts += 1
265
+ @login_attempts < 3
266
+ end
267
+ end
268
+ end
data/lib/nimbu/cli.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "nimbu"
2
+ require "nimbu/command"
3
+
4
+ class Nimbu::CLI
5
+
6
+ def self.start(*args)
7
+ command = args.shift.strip rescue "help"
8
+ Nimbu::Command.load
9
+ Nimbu::Command.run(command, args)
10
+ end
11
+
12
+ end
@@ -0,0 +1,269 @@
1
+ require 'rexml/document'
2
+ require 'rest_client'
3
+ require 'uri'
4
+ require 'time'
5
+ require 'nimbu/auth'
6
+ require 'nimbu/helpers'
7
+ require 'nimbu/version'
8
+
9
+ # A Ruby class to call the Nimbu REST API. You might use this if you want to
10
+ # manage your Nimbu apps from within a Ruby program, such as Capistrano.
11
+ #
12
+ # Example:
13
+ #
14
+ # require 'nimbu'
15
+ # nimbu = Nimbu::Client.new('me@example.com', 'mypass')
16
+ # nimbu.create('myapp')
17
+ #
18
+ class Nimbu::Client
19
+
20
+ include Nimbu::Helpers
21
+ extend Nimbu::Helpers
22
+
23
+ def self.version
24
+ Nimbu::VERSION
25
+ end
26
+
27
+ def self.gem_version_string
28
+ "nimbu-gem/#{version}"
29
+ end
30
+
31
+ attr_accessor :host, :user, :password
32
+
33
+ def self.auth(user, password, host=Nimbu::Auth.host)
34
+ client = new(user, password, host)
35
+ json_decode client.post('/login', { :user => {:email => user, :password => password }}, :accept => 'json').to_s
36
+ end
37
+
38
+ def initialize(user, password, host=Nimbu::Auth.host)
39
+ @user = user
40
+ @password = password
41
+ @host = host
42
+ end
43
+
44
+ # Show a list of sites
45
+ def list
46
+ doc = xml(get('/sites').to_s)
47
+ doc.elements.to_a("//sites/site").map do |a|
48
+ name = a.elements.to_a("name").first
49
+ owner = a.elements.to_a("domain").first
50
+ [name.text, owner.text]
51
+ end
52
+ end
53
+
54
+ # Show info such as mode, custom domain, and collaborators on an app.
55
+ def info(name_or_domain)
56
+ raise ArgumentError.new("name_or_domain is required for info") unless name_or_domain
57
+ name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
58
+ doc = xml(get("/apps/#{name_or_domain}").to_s)
59
+ attrs = hash_from_xml_doc(doc)[:app]
60
+ attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
61
+ attrs.merge!(:addons => installed_addons(attrs[:name]))
62
+ end
63
+
64
+ def on_warning(&blk)
65
+ @warning_callback = blk
66
+ end
67
+
68
+ def get_template(params)
69
+ post("/engine/template",params)
70
+ end
71
+
72
+ def get_page(params)
73
+ post("/engine/render",params)
74
+ end
75
+
76
+ def list_themes
77
+ get("/themes")
78
+ end
79
+
80
+ def show_theme_contents(theme)
81
+ get("/themes/#{theme}/list")
82
+ end
83
+
84
+ def fetch_theme_layout(theme,id)
85
+ get("/themes/#{theme}/layouts/#{id}")
86
+ end
87
+
88
+ def fetch_theme_template(theme,id)
89
+ get("/themes/#{theme}/templates/#{id}")
90
+ end
91
+
92
+ def fetch_theme_assets(theme,id)
93
+ get("/themes/#{theme}/assets/#{id}")
94
+ end
95
+
96
+
97
+ ##################
98
+
99
+ def resource(uri, options={})
100
+ if http_proxy
101
+ RestClient.proxy = http_proxy
102
+ end
103
+ resource = RestClient::Resource.new(realize_full_uri(uri), options.merge(:user => password))
104
+ resource
105
+ end
106
+
107
+ def get(uri, extra_headers={}) # :nodoc:
108
+ process(:get, uri, extra_headers)
109
+ end
110
+
111
+ def post(uri, payload="", extra_headers={}) # :nodoc:
112
+ process(:post, uri, extra_headers, payload)
113
+ end
114
+
115
+ def put(uri, payload, extra_headers={}) # :nodoc:
116
+ process(:put, uri, extra_headers, payload)
117
+ end
118
+
119
+ def delete(uri, extra_headers={}) # :nodoc:
120
+ process(:delete, uri, extra_headers)
121
+ end
122
+
123
+ def process(method, uri, extra_headers={}, payload=nil)
124
+ headers = nimbu_headers.merge(extra_headers)
125
+ args = [method, payload, headers].compact
126
+
127
+ resource_options = default_resource_options_for_uri(uri)
128
+
129
+ begin
130
+ response = resource(uri, resource_options).send(*args)
131
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
132
+ host = URI.parse(realize_full_uri(uri)).host
133
+ error "Unable to connect to #{host}"
134
+ rescue RestClient::SSLCertificateNotVerified => ex
135
+ host = URI.parse(realize_full_uri(uri)).host
136
+ error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with HEROKU_SSL_VERIFY=disable"
137
+ end
138
+
139
+ extract_warning(response)
140
+ response
141
+ end
142
+
143
+ def extract_warning(response)
144
+ return unless response
145
+ if response.headers[:x_nimbu_warning] && @warning_callback
146
+ warning = response.headers[:x_nimbu_warning]
147
+ @displayed_warnings ||= {}
148
+ unless @displayed_warnings[warning]
149
+ @warning_callback.call(warning)
150
+ @displayed_warnings[warning] = true
151
+ end
152
+ end
153
+ end
154
+
155
+ def nimbu_headers # :nodoc:
156
+ {
157
+ 'X-Nimbu-API-Version' => '1',
158
+ 'User-Agent' => self.class.gem_version_string,
159
+ 'X-Ruby-Version' => RUBY_VERSION,
160
+ 'X-Ruby-Platform' => RUBY_PLATFORM
161
+ }
162
+ end
163
+
164
+ def xml(raw) # :nodoc:
165
+ REXML::Document.new(raw)
166
+ end
167
+
168
+ def escape(value) # :nodoc:
169
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
170
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
171
+ end
172
+
173
+ module JSON
174
+ def self.parse(json)
175
+ json_decode(json)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def configure_addon(action, app_name, addon, config = {})
182
+ response = update_addon action,
183
+ addon_path(app_name, addon),
184
+ config
185
+
186
+ json_decode(response.to_s) unless response.to_s.empty?
187
+ end
188
+
189
+ def addon_path(app_name, addon)
190
+ "/apps/#{app_name}/addons/#{escape(addon)}"
191
+ end
192
+
193
+ def update_addon(action, path, config)
194
+ params = { :config => config }
195
+ app = params[:config].delete(:confirm)
196
+ headers = { :accept => 'application/json' }
197
+ params.merge!(:confirm => app) if app
198
+
199
+ case action
200
+ when :install
201
+ post path, params, headers
202
+ when :upgrade
203
+ put path, params, headers
204
+ when :uninstall
205
+ confirm = app ? "confirm=#{app}" : ''
206
+ delete "#{path}?#{confirm}", headers
207
+ end
208
+ end
209
+
210
+ def realize_full_uri(given)
211
+ full_host = (host =~ /^http/) ? host : "http://#{host}"
212
+ host = URI.parse(full_host)
213
+ uri = URI.parse(given)
214
+ uri.host ||= host.host
215
+ uri.scheme ||= host.scheme || "http"
216
+ uri.path = "/api/v1" + ((uri.path[0..0] == "/") ? uri.path : "/#{uri.path}")
217
+ uri.port = host.port if full_host =~ /\:\d+/
218
+ uri.to_s
219
+ end
220
+
221
+ def default_resource_options_for_uri(uri)
222
+ if ENV["HEROKU_SSL_VERIFY"] == "disable"
223
+ {}
224
+ elsif realize_full_uri(uri) =~ %r|^https://api.getnimbu.com|
225
+ { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file }
226
+ else
227
+ {}
228
+ end
229
+ end
230
+
231
+ def local_ca_file
232
+ File.expand_path("../../../data/cacert.pem", __FILE__)
233
+ end
234
+
235
+ def hash_from_xml_doc(elements)
236
+ elements.inject({}) do |hash, e|
237
+ next(hash) unless e.respond_to?(:children)
238
+ hash.update(e.name.gsub("-","_").to_sym => case e.children.length
239
+ when 0 then nil
240
+ when 1 then e.text
241
+ else hash_from_xml_doc(e.children)
242
+ end)
243
+ end
244
+ end
245
+
246
+ def http_proxy
247
+ proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
248
+ if proxy && !proxy.empty?
249
+ unless /^[^:]+:\/\// =~ proxy
250
+ proxy = "http://" + proxy
251
+ end
252
+ proxy
253
+ else
254
+ nil
255
+ end
256
+ end
257
+
258
+ def https_proxy
259
+ proxy = ENV['HTTPS_PROXY'] || ENV['https_proxy']
260
+ if proxy && !proxy.empty?
261
+ unless /^[^:]+:\/\// =~ proxy
262
+ proxy = "https://" + proxy
263
+ end
264
+ proxy
265
+ else
266
+ nil
267
+ end
268
+ end
269
+ end