shelly 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/lib/shelly.rb CHANGED
@@ -14,6 +14,7 @@ module Shelly
14
14
  autoload :App, "shelly/app"
15
15
  autoload :Cloudfile, "shelly/cloudfile"
16
16
  autoload :Client, "shelly/client"
17
+ autoload :StructureValidator, "shelly/structure_validator"
17
18
  autoload :User, "shelly/user"
18
19
  autoload :VERSION, "shelly/version"
19
20
  end
data/lib/shelly/app.rb CHANGED
@@ -4,7 +4,8 @@ require 'shelly/backup'
4
4
 
5
5
  module Shelly
6
6
  class App < Model
7
- DATABASE_KINDS = %w(postgresql mongodb redis none)
7
+ DATABASE_KINDS = %w(postgresql mongodb redis)
8
+ DATABASE_CHOICES = DATABASE_KINDS + %w(none)
8
9
  SERVER_SIZES = %w(small large)
9
10
 
10
11
  attr_accessor :code_name, :databases, :ruby_version, :environment,
@@ -39,18 +40,6 @@ module Shelly
39
40
  system("git remote rm #{code_name} > /dev/null 2>&1")
40
41
  end
41
42
 
42
- def generate_cloudfile
43
- @email = current_user.email
44
- thin = (size == "small" ? 2 : 4)
45
- template = File.read(cloudfile_template_path)
46
- cloudfile = ERB.new(template, 0, "%<>-")
47
- cloudfile.result(binding)
48
- end
49
-
50
- def cloudfile_template_path
51
- File.join(File.dirname(__FILE__), "templates", "Cloudfile.erb")
52
- end
53
-
54
43
  def create
55
44
  attributes = {:code_name => code_name}
56
45
  response = shelly.create_app(attributes)
@@ -60,13 +49,19 @@ module Shelly
60
49
  self.environment = response["environment"]
61
50
  end
62
51
 
63
- def delete
64
- shelly.delete_app(code_name)
52
+ def create_cloudfile
53
+ cloudfile = Cloudfile.new
54
+ cloudfile.code_name = code_name
55
+ cloudfile.ruby_version = ruby_version
56
+ cloudfile.environment = environment
57
+ cloudfile.domains = domains
58
+ cloudfile.size = size
59
+ cloudfile.databases = databases
60
+ cloudfile.create
65
61
  end
66
62
 
67
- def create_cloudfile
68
- content = generate_cloudfile
69
- File.open(cloudfile_path, "a+") { |f| f << content }
63
+ def delete
64
+ shelly.delete_app(code_name)
70
65
  end
71
66
 
72
67
  def deploy_logs
@@ -116,14 +111,11 @@ module Shelly
116
111
  shelly.redeploy(code_name)
117
112
  end
118
113
 
119
- def cloudfile_path
120
- File.join(Dir.pwd, "Cloudfile")
121
- end
122
-
123
114
  def self.guess_code_name
124
115
  guessed = nil
125
- if Cloudfile.present?
126
- clouds = Cloudfile.new.clouds
116
+ cloudfile = Cloudfile.new
117
+ if cloudfile.present?
118
+ clouds = cloudfile.clouds
127
119
  if clouds.grep(/staging/).present?
128
120
  guessed = "production"
129
121
  production_clouds = clouds.grep(/production/)
@@ -185,6 +177,10 @@ module Shelly
185
177
  @attributes ||= shelly.app(code_name)
186
178
  end
187
179
 
180
+ def statistics
181
+ @stats ||= shelly.statistics(code_name)
182
+ end
183
+
188
184
  def web_server_ip
189
185
  attributes["web_server_ip"]
190
186
  end
@@ -17,7 +17,7 @@ module Shelly
17
17
 
18
18
  # FIXME: it should be possible to pass single symbol, instead of one element array
19
19
  before_hook :logged_in?, :only => [:add, :status, :list, :start, :stop, :logs, :delete, :info, :ip, :logout, :execute, :rake, :setup, :console, :upload]
20
- before_hook :inside_git_repository?, :only => [:add, :setup]
20
+ before_hook :inside_git_repository?, :only => [:add, :setup, :check]
21
21
 
22
22
  map %w(-v --version) => :version
23
23
  desc "version", "Display shelly version"
@@ -71,7 +71,7 @@ module Shelly
71
71
  method_option "code-name", :type => :string, :aliases => "-c",
72
72
  :desc => "Unique code-name of your cloud"
73
73
  method_option :databases, :type => :array, :aliases => "-d",
74
- :banner => Shelly::App::DATABASE_KINDS.join(', '),
74
+ :banner => Shelly::App::DATABASE_CHOICES.join(', '),
75
75
  :desc => "List of databases of your choice"
76
76
  method_option :size, :type => :string, :aliases => "-s",
77
77
  :desc => "Server size [large, small]"
@@ -150,9 +150,20 @@ module Shelly
150
150
  print_wrapped "Repository URL: #{app.git_info["repository_url"]}", :ident => 2
151
151
  print_wrapped "Web server IP: #{app.web_server_ip}", :ident => 2
152
152
  print_wrapped "Mail server IP: #{app.mail_server_ip}", :ident => 2
153
+ say_new_line
154
+ if app.statistics.present?
155
+ print_wrapped "Statistics:", :ident => 2
156
+ app.statistics.each do |stat|
157
+ print_wrapped "#{stat['name']}:", :ident => 4
158
+ print_wrapped "Load average: 1m: #{stat['load']['avg01']}, 5m: #{stat['load']['avg05']}, 15m: #{stat['load']['avg15']}", :ident => 6
159
+ print_wrapped "CPU: #{stat['cpu']['wait']}%, MEM: #{stat['memory']['percent']}%, SWAP: #{stat['swap']['percent']}%", :ident => 6
160
+ end
161
+ end
153
162
  rescue Client::NotFoundException => e
154
163
  raise unless e.resource == :cloud
155
164
  say_error "You have no access to '#{app}' cloud defined in Cloudfile"
165
+ rescue Client::GatewayTimeoutException
166
+ say_error "Server statistics temporarily unavailable"
156
167
  end
157
168
 
158
169
  desc "start", "Start the cloud"
@@ -371,6 +382,22 @@ We have been notified about it. We will be adding new resources shortly}
371
382
  say_error "Cloud #{app} is not running. Cannot upload files."
372
383
  end
373
384
 
385
+ require 'bundler'
386
+ desc "check", "List all requirements and check which are fulfilled"
387
+ def check
388
+ s = Shelly::StructureValidator.new
389
+ say "Checking dependencies:", :green
390
+ print_check s.gemfile_exists?, "Gemfile exists"
391
+ print_check s.gems.include?("thin"), "gem 'thin' present in Gemfile"
392
+ print_check s.config_ru_exists?, "config.ru exists"
393
+ print_check !(s.gems.include?("mysql2") or s.gems.include?("mysql")),
394
+ "application doesn't use mysql database"
395
+ rescue Bundler::BundlerError => e
396
+ say_new_line
397
+ say_error e.message, :with_exit => false
398
+ say_error "Try to run `bundle install`"
399
+ end
400
+
374
401
  # FIXME: move to helpers
375
402
  no_tasks do
376
403
  # Returns valid arguments for rake, removes shelly gem arguments
@@ -406,7 +433,7 @@ We have been notified about it. We will be adding new resources shortly}
406
433
 
407
434
  def valid_databases?(databases)
408
435
  return true unless databases.present?
409
- kinds = Shelly::App::DATABASE_KINDS
436
+ kinds = Shelly::App::DATABASE_CHOICES
410
437
  databases.all? { |kind| kinds.include?(kind) }
411
438
  end
412
439
 
@@ -441,7 +468,7 @@ We have been notified about it. We will be adding new resources shortly}
441
468
  end
442
469
 
443
470
  def ask_for_databases
444
- kinds = Shelly::App::DATABASE_KINDS
471
+ kinds = Shelly::App::DATABASE_CHOICES
445
472
  databases = ask("Which database do you want to use #{kinds.join(", ")} (postgresql - default):")
446
473
  begin
447
474
  databases = databases.split(/[\s,]/).reject(&:blank?)
data/lib/shelly/client.rb CHANGED
@@ -21,6 +21,7 @@ module Shelly
21
21
  class UnauthorizedException < APIException; end
22
22
  class ConflictException < APIException; end
23
23
  class GemVersionException < APIException; end
24
+ class GatewayTimeoutException < APIException; end
24
25
  class ValidationException < APIException
25
26
  def errors
26
27
  self[:errors]
@@ -121,6 +122,10 @@ module Shelly
121
122
  get("/apps/#{code_name}")
122
123
  end
123
124
 
125
+ def statistics(code_name)
126
+ get("/apps/#{code_name}/statistics")
127
+ end
128
+
124
129
  def command(cloud, body, type)
125
130
  post("/apps/#{cloud}/command", {:body => body, :type => type})
126
131
  end
@@ -235,6 +240,7 @@ module Shelly
235
240
  when 409; ConflictException
236
241
  when 412; GemVersionException
237
242
  when 422; ValidationException
243
+ when 504; GatewayTimeoutException
238
244
  else; APIException
239
245
  end
240
246
  raise exception_class.new(body, code, response.headers[:x_request_id])
@@ -3,38 +3,56 @@ require "yaml"
3
3
  module Shelly
4
4
  class Cloudfile < Model
5
5
  attr_accessor :content
6
+ # Cloudfile attributes used for generating Cloudfile from a template
7
+ attr_accessor :code_name, :ruby_version, :environment, :domains,
8
+ :databases, :size
6
9
 
7
- def self.present?
8
- File.exists?(File.join(Dir.pwd, "Cloudfile"))
10
+ # Public: Return true if Cloudfile is present in current directory
11
+ def present?
12
+ File.exists?(path)
9
13
  end
10
14
 
11
- def initialize
12
- open if File.exists?(path)
15
+ # Public: Clouds in Cloudfile
16
+ # Returns Array of clouds names from Cloudfile
17
+ # nil if there is no cloudfile
18
+ def clouds
19
+ content.keys.sort if content
13
20
  end
14
21
 
15
- def path
16
- File.join(Dir.pwd, "Cloudfile")
22
+ # Public: Generate example Cloudfile based on object attributes
23
+ # Returns the generated Cloudfile as String
24
+ def generate
25
+ @email = current_user.email
26
+ @thin = @size == "small" ? 2 : 4
27
+ template = File.read(template_path)
28
+ cloudfile = ERB.new(template, 0, "%<>-")
29
+ cloudfile.result(binding)
17
30
  end
18
31
 
19
- def open
20
- @content = YAML.load(File.open(path))
32
+ # Public: Create Cloudfile in current path (or append if exists)
33
+ # File is created based on assigned attributes
34
+ def create
35
+ File.open(path, "a+") { |f| f << generate }
21
36
  end
22
37
 
23
- def write(hash)
24
- @content = hash
25
- File.open(path, "w") do |f|
26
- f.write(yaml(hash))
27
- end
38
+ private
39
+
40
+ # Internal: Load and parse Cloudfile
41
+ def content
42
+ return unless present?
43
+ @content ||= YAML.load(File.open(path))
28
44
  end
29
45
 
30
- def clouds
31
- @content.keys.sort if @content
46
+ # Internal: Path to Cloudfile in current directory
47
+ # Returns path as String
48
+ def path
49
+ File.join(Dir.pwd, "Cloudfile")
32
50
  end
33
51
 
34
- def yaml(hash)
35
- string = hash.deep_stringify_keys.to_yaml
36
- # FIXME: check if it possible to remove sub("---", "") by passing options to_yaml
37
- string.sub("---","").split("\n").map(&:rstrip).join("\n").strip
52
+ # Internal: Return path to Cloudfile template
53
+ # Returns path as String
54
+ def template_path
55
+ File.join(File.dirname(__FILE__), "templates", "Cloudfile.erb")
38
56
  end
39
57
  end
40
58
  end
@@ -64,7 +64,7 @@ module Shelly
64
64
  end
65
65
 
66
66
  def cloudfile_present?
67
- say_error "No Cloudfile found" unless Cloudfile.present?
67
+ say_error "No Cloudfile found" unless Cloudfile.new.present?
68
68
  end
69
69
 
70
70
  def ask_to_restore_database
@@ -103,7 +103,7 @@ module Shelly
103
103
  end
104
104
  exit 1
105
105
  end
106
- unless Cloudfile.present? || cloud
106
+ unless Cloudfile.new.present? || cloud
107
107
  say_error "You have to specify cloud.", :with_exit => false
108
108
  say "Select cloud using `shelly #{action} --cloud CLOUD_NAME`"
109
109
  Shelly::CLI::Main.new.list
@@ -120,5 +120,17 @@ module Shelly
120
120
  say "%8.8s | %s\n" % entry
121
121
  end
122
122
  end
123
+
124
+ def green(string)
125
+ "\e[32m#{string}\e[0m"
126
+ end
127
+
128
+ def red(string)
129
+ "\e[31m#{string}\e[0m"
130
+ end
131
+
132
+ def print_check(checked, string, options = {})
133
+ print_wrapped (checked ? green("+") : red("-")) + " #{string}", :ident => 2
134
+ end
123
135
  end
124
136
  end
@@ -0,0 +1,27 @@
1
+ require 'grit'
2
+
3
+ module Shelly
4
+ class StructureValidator
5
+ attr_reader :gemfile_path, :gemfile_lock_path
6
+
7
+ def initialize(options = {})
8
+ @gemfile_path = options[:gemfile] || "Gemfile"
9
+ @gemfile_lock_path = options[:gemfile_lock] || "Gemfile.lock"
10
+ end
11
+
12
+ def gemfile_exists?
13
+ File.exists?(@gemfile_path)
14
+ end
15
+
16
+ def config_ru_exists?
17
+ repo = Grit::Repo.new(".")
18
+ repo.status.map(&:path).include?("config.ru")
19
+ end
20
+
21
+ def gems
22
+ return [] unless gemfile_exists?
23
+ @d = Bundler::Definition.build(@gemfile_path, @gemfile_lock_path, nil)
24
+ @gems = @d.specs.map(&:name)
25
+ end
26
+ end
27
+ end
@@ -1,6 +1,6 @@
1
1
  <%= @code_name %>:
2
- ruby_version: 1.9.3 # 1.9.3, 1.9.2 or ree-1.8.7
3
- environment: production # RAILS_ENV
2
+ ruby_version: <%= @ruby_version %> # 1.9.3, 1.9.2 or ree-1.8.7
3
+ environment: <%= @environment %> # RAILS_ENV
4
4
  monitoring_email: <%= @email %>
5
5
  domains:
6
6
  <%- @domains.each do |domain| -%>
@@ -8,14 +8,16 @@
8
8
  <%- end -%>
9
9
  servers:
10
10
  app1:
11
- size: <%= size %>
12
- thin: <%= thin %>
11
+ size: <%= @size %>
12
+ thin: <%= @thin %>
13
13
  # whenever: on
14
14
  # delayed_job: 1
15
- <%- @databases.each do |kind| -%>
16
- <%= kind %>:
17
- size: <%= size %>
18
15
  databases:
16
+ <%- if @databases.present? -%>
17
+ <%- @databases.each do |kind| -%>
19
18
  - <%= kind %>
20
- <%- end -%>
21
-
19
+ <%- end -%>
20
+ <%- end -%>
21
+ <%- (Shelly::App::DATABASE_KINDS - @databases).each do |kind| -%>
22
+ # - <%= kind %>
23
+ <%- end -%>
@@ -1,3 +1,3 @@
1
1
  module Shelly
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
data/shelly.gemspec CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  s.add_runtime_dependency "rest-client"
28
28
  s.add_runtime_dependency "json"
29
29
  s.add_runtime_dependency "progressbar"
30
+ s.add_runtime_dependency "grit"
30
31
  s.add_runtime_dependency "launchy"
31
32
  s.add_runtime_dependency "shelly-dependencies", "~> 0.1.1"
32
33
 
@@ -179,95 +179,19 @@ describe Shelly::App do
179
179
  end
180
180
  end
181
181
 
182
- describe "#generate_cloudfile" do
183
- it "should return generated cloudfile for large instance" do
184
- user = mock(:email => "bob@example.com")
185
- @app.stub(:current_user).and_return(user)
186
- @app.databases = %w(postgresql mongodb)
187
- @app.domains = %w(foo-staging.winniecloud.com foo.example.com)
188
- @app.size = "large"
189
- FakeFS.deactivate!
190
- expected = <<-config
191
- foo-staging:
192
- ruby_version: 1.9.3 # 1.9.3, 1.9.2 or ree-1.8.7
193
- environment: production # RAILS_ENV
194
- monitoring_email: bob@example.com
195
- domains:
196
- - foo-staging.winniecloud.com
197
- - foo.example.com
198
- servers:
199
- app1:
200
- size: large
201
- thin: 4
202
- # whenever: on
203
- # delayed_job: 1
204
- postgresql:
205
- size: large
206
- databases:
207
- - postgresql
208
- mongodb:
209
- size: large
210
- databases:
211
- - mongodb
212
- config
213
- @app.generate_cloudfile.strip.should == expected.strip
214
- end
215
-
216
- it "should return generated cloudfile for small instance" do
217
- user = mock(:email => "bob@example.com")
218
- @app.stub(:current_user).and_return(user)
219
- @app.databases = %w(postgresql mongodb)
220
- @app.domains = %w(foo-staging.winniecloud.com foo.example.com)
221
- @app.size = "small"
222
- FakeFS.deactivate!
223
- expected = <<-config
224
- foo-staging:
225
- ruby_version: 1.9.3 # 1.9.3, 1.9.2 or ree-1.8.7
226
- environment: production # RAILS_ENV
227
- monitoring_email: bob@example.com
228
- domains:
229
- - foo-staging.winniecloud.com
230
- - foo.example.com
231
- servers:
232
- app1:
233
- size: small
234
- thin: 2
235
- # whenever: on
236
- # delayed_job: 1
237
- postgresql:
238
- size: small
239
- databases:
240
- - postgresql
241
- mongodb:
242
- size: small
243
- databases:
244
- - mongodb
245
- config
246
- @app.generate_cloudfile.strip.should == expected.strip
247
- end
248
- end
249
-
250
- describe "#create_cloudfile" do
182
+ describe "#statistics" do
251
183
  before do
252
- @app.stub(:generate_cloudfile).and_return("foo-staging:")
253
- end
254
-
255
- it "should create file if Cloudfile doesn't exist" do
256
- File.exists?("/projects/foo/Cloudfile").should be_false
257
- @app.create_cloudfile
258
- File.exists?("/projects/foo/Cloudfile").should be_true
259
- end
260
-
261
- it "should append content if Cloudfile exists" do
262
- File.open("/projects/foo/Cloudfile", "w") { |f| f << "foo-production:\n" }
263
- @app.create_cloudfile
264
- File.read("/projects/foo/Cloudfile").strip.should == "foo-production:\nfoo-staging:"
184
+ @response = [{"name"=>"app1",
185
+ "memory" => {"kilobyte"=>"276756", "percent" => "74.1"},
186
+ "swap" => {"kilobyte" => "44332", "percent" => "2.8"},
187
+ "cpu" => {"wait" => "0.8", "system" => "0.0", "user" => "0.1"},
188
+ "load" => {"avg15" => "0.13", "avg05" => "0.15", "avg01" => "0.04"}}]
189
+ @client.stub(:statistics).and_return(@response)
265
190
  end
266
- end
267
191
 
268
- describe "#cloudfile_path" do
269
- it "should return path to Cloudfile" do
270
- @app.cloudfile_path.should == "/projects/foo/Cloudfile"
192
+ it "should fetch app statistics from API and cache them" do
193
+ @client.should_receive(:statistics).with("foo-staging").exactly(:once).and_return(@response)
194
+ 2.times { @app.statistics }
271
195
  end
272
196
  end
273
197
 
@@ -412,4 +336,24 @@ config
412
336
  @app.upload("/path")
413
337
  end
414
338
  end
339
+
340
+ describe "#create_cloudfile" do
341
+ it "should create cloudfile with app attributes" do
342
+ @app.ruby_version = "1.9.3"
343
+ @app.environment = "production"
344
+ @app.domains = ["example.com", "another.example.com"]
345
+ @app.size = "large"
346
+ @app.databases = []
347
+ cloudfile = mock
348
+ cloudfile.should_receive(:code_name=).with("foo-staging")
349
+ cloudfile.should_receive(:ruby_version=).with("1.9.3")
350
+ cloudfile.should_receive(:environment=).with("production")
351
+ cloudfile.should_receive(:domains=).with(["example.com", "another.example.com"])
352
+ cloudfile.should_receive(:size=).with("large")
353
+ cloudfile.should_receive(:databases=).with([])
354
+ cloudfile.should_receive(:create)
355
+ Shelly::Cloudfile.should_receive(:new).and_return(cloudfile)
356
+ @app.create_cloudfile
357
+ end
358
+ end
415
359
  end