shelly 0.1.6 → 0.1.7

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