nimbu 0.1

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