ridley 0.5.2 → 0.6.0

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.
@@ -2,50 +2,97 @@ module Ridley
2
2
  class SSH
3
3
  # @author Jamie Winsor <jamie@vialstudios.com>
4
4
  class ResponseSet
5
- # @return [Array<SSH::Response>]
6
- attr_reader :oks
7
- # @return [Array<SSH::Response>]
8
- attr_reader :errors
5
+ class << self
6
+ # Merges the responses of the other ResponseSet with the target ResponseSet
7
+ # and returns the mutated target
8
+ #
9
+ # @param [SSH::ResponseSet] target
10
+ # @param [SSH::ResponseSet] other
11
+ #
12
+ # @return [SSH::ResponseSet]
13
+ def merge!(target, other)
14
+ if other.is_a?(self)
15
+ target.add_response(other.responses)
16
+ end
9
17
 
10
- def initialize
11
- @oks = Array.new
12
- @errors = Array.new
18
+ target
19
+ end
13
20
  end
14
21
 
15
- # Add an "OK" response to the ResponseSet
16
- #
17
- # @param [SSH::Response] response
18
- def add_ok(response)
19
- self.oks << response
22
+ extend Forwardable
23
+ include Enumerable
24
+
25
+ attr_reader :failures
26
+ attr_reader :successes
27
+
28
+ def_delegator :responses, :each
29
+
30
+ def initialize(responses = Array.new)
31
+ @failures = Array.new
32
+ @successes = Array.new
33
+
34
+ add_response Array(responses)
20
35
  end
21
36
 
22
- # Add an "Error" response to the ResponseSet
37
+ # @param [SSH::Response, Array<SSH::Response>] response
23
38
  #
24
- # @param [SSH::Response] response
25
- def add_error(response)
26
- self.errors << response
39
+ # @return [Array<SSH::Response>]
40
+ def add_response(response)
41
+ if response.is_a?(Array)
42
+ until response.empty?
43
+ add_response(response.pop)
44
+ end
45
+ return responses
46
+ end
47
+
48
+ response.error? ? add_failure(response) : add_success(response)
49
+ responses
50
+ end
51
+ alias_method :<<, :add_response
52
+
53
+ def responses
54
+ successes + failures
27
55
  end
28
56
 
29
57
  # Return true if the response set contains any errors
30
58
  #
31
59
  # @return [Boolean]
32
60
  def has_errors?
33
- self.errors.any?
61
+ self.failures.any?
34
62
  end
35
63
 
36
- # Return one of the responses
64
+ # Merges the responses of another ResponseSet with self and returns
65
+ # a new instance of ResponseSet
66
+ #
67
+ # @param [Ridley::SSH::ResponseSet] other
37
68
  #
38
- # @return [SSH::Response]
39
- def first
40
- (self.oks + self.errors).first
69
+ # @return [Ridley::SSH::ResponseSet]
70
+ def merge(other)
71
+ target = self.class.new(self.responses) # Why the fuck can't I use #dup here?
72
+ self.class.merge!(target, other)
41
73
  end
42
74
 
43
- # Returns how many responses are in the set
75
+ # Merges the respones of another ResponseSet with self and returns
76
+ # mutated self
44
77
  #
45
- # @return [Integer]
46
- def length
47
- self.oks.length + self.errors.length
78
+ # @param [Ridley::SSH::ResponseSet] other
79
+ #
80
+ # @return [self]
81
+ def merge!(other)
82
+ self.class.merge!(self, other)
48
83
  end
84
+
85
+ private
86
+
87
+ # @param [SSH::Response] response
88
+ def add_failure(response)
89
+ self.failures << response
90
+ end
91
+
92
+ # @param [SSH::Response] response
93
+ def add_success(response)
94
+ self.successes << response
95
+ end
49
96
  end
50
97
  end
51
98
  end
@@ -5,11 +5,18 @@ module Ridley
5
5
  class Worker
6
6
  include Celluloid
7
7
  include Celluloid::Logger
8
+
9
+ attr_reader :sudo
10
+ attr_reader :user
11
+ attr_reader :options
8
12
 
9
13
  # @param [Hash] options
10
14
  def initialize(options = {})
11
- @options = options
12
- @user = options.fetch(:user)
15
+ @options = options.dup
16
+ @sudo = @options[:sudo]
17
+ @user = @options[:user]
18
+
19
+ @options[:paranoid] = false
13
20
  end
14
21
 
15
22
  # @param [String] host
@@ -17,11 +24,15 @@ module Ridley
17
24
  #
18
25
  # @return [Array]
19
26
  def run(host, command)
20
- response = Response.new("", "")
27
+ response = Response.new(host)
21
28
  debug "Running SSH command: '#{command}' on: '#{host}' as: '#{user}'"
22
29
 
23
- Net::SSH.start(host, user, options) do |ssh|
30
+ Net::SSH.start(host, user, options.slice(*Net::SSH::VALID_OPTIONS)) do |ssh|
24
31
  ssh.open_channel do |channel|
32
+ if self.sudo
33
+ channel.request_pty
34
+ end
35
+
25
36
  channel.exec(command) do |ch, success|
26
37
  unless success
27
38
  raise "FAILURE: could not execute command"
@@ -29,10 +40,12 @@ module Ridley
29
40
 
30
41
  channel.on_data do |ch, data|
31
42
  response.stdout += data
43
+ info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
32
44
  end
33
45
 
34
46
  channel.on_extended_data do |ch, type, data|
35
47
  response.stderr += data
48
+ info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
36
49
  end
37
50
 
38
51
  channel.on_request("exit-status") do |ch, data|
@@ -50,22 +63,24 @@ module Ridley
50
63
 
51
64
  case response.exit_code
52
65
  when 0
53
- debug "Successfully ran SSH command: '#{command}' on: '#{host}' as: '#{user}' and it succeeded"
66
+ debug "Successfully ran SSH command on: '#{host}' as: '#{user}'"
54
67
  [ :ok, response ]
55
68
  else
56
- debug "Successfully ran SSH command: '#{command}' on: '#{host}' as: '#{user}' but it failed"
69
+ error "Successfully ran SSH command on: '#{host}' as: '#{user}', but it failed"
70
+ error response.stdout
57
71
  [ :error, response ]
58
72
  end
59
73
  rescue => e
60
- debug "Failed to run SSH command: '#{command}' on: '#{host}' as: '#{user}'"
61
- [ :error, e.message ]
74
+ error "Failed to run SSH command on: '#{host}' as: '#{user}'"
75
+ error "#{e.class}: #{e.message}"
76
+ response.exit_code = -1
77
+ response.stderr = e.message
78
+ [ :error, response ]
62
79
  end
63
80
 
64
81
  private
65
82
 
66
83
  attr_reader :runner
67
- attr_reader :user
68
- attr_reader :options
69
84
  end
70
85
  end
71
86
  end
data/lib/ridley/ssh.rb CHANGED
@@ -29,46 +29,28 @@ module Ridley
29
29
  # @param [Hash] options
30
30
  # @see Net::SSH
31
31
  def initialize(nodes, options = {})
32
- @nodes = nodes
32
+ @nodes = Array(nodes)
33
33
  @options = options
34
-
35
- self.options[:timeout] ||= 1.5
36
- end
37
-
38
- # @return [Array<SSH::Worker>]
39
- def workers
40
- @workers ||= Array(nodes).collect do |node|
41
- Worker.new_link(current_actor, node.public_hostname, options)
42
- end
43
34
  end
44
35
 
45
36
  # @param [String] command
46
37
  #
47
38
  # @return [Array]
48
39
  def run(command)
49
- workers.collect { |worker| worker.async.run(command) }
40
+ workers = Array.new
41
+ futures = self.nodes.collect do |node|
42
+ workers << worker = Worker.new_link(self.options.freeze)
43
+ worker.future.run(node.public_hostname, command)
44
+ end
50
45
 
51
- ResponseSet.new.tap do |responses|
52
- until responses.length == workers.length
53
- receive { |msg|
54
- status, response = msg
55
-
56
- case status
57
- when :ok
58
- responses.add_ok(response)
59
- when :error
60
- responses.add_error(response)
61
- else
62
- error "SSH Failure: #{command}. terminating..."
63
- terminate
64
- end
65
- }
46
+ ResponseSet.new.tap do |response_set|
47
+ futures.each do |future|
48
+ status, response = future.value
49
+ response_set.add_response(response)
66
50
  end
67
51
  end
68
- end
69
-
70
- def finalize
71
- workers.collect(&:terminate)
52
+ ensure
53
+ workers.map(&:terminate)
72
54
  end
73
55
  end
74
56
  end
@@ -1,3 +1,3 @@
1
1
  module Ridley
2
- VERSION = "0.5.2"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/ridley.rb CHANGED
@@ -8,6 +8,7 @@ require 'active_model'
8
8
  require 'active_support/inflector'
9
9
  require 'forwardable'
10
10
  require 'thread'
11
+ require 'pathname'
11
12
 
12
13
  if jruby?
13
14
  require 'json/pure'
@@ -41,7 +42,13 @@ module Ridley
41
42
  autoload :SSH, 'ridley/ssh'
42
43
 
43
44
  class << self
44
- attr_accessor :logger
45
+ extend Forwardable
46
+
47
+ def_delegator "Ridley::Logging", :logger
48
+ alias_method :log, :logger
49
+
50
+ def_delegator "Ridley::Logging", :logger=
51
+ def_delegator "Ridley::Logging", :set_logger
45
52
 
46
53
  def connection(*args)
47
54
  Connection.new(*args)
@@ -51,19 +58,6 @@ module Ridley
51
58
  Connection.sync(*args, &block)
52
59
  end
53
60
 
54
- # @return [Logger]
55
- def logger
56
- Ridley::Logging.logger
57
- end
58
- alias_method :log, :logger
59
-
60
- # @param [Logger, nil] obj
61
- #
62
- # @return [Logger]
63
- def set_logger(obj)
64
- Ridley::Logging.set_logger(obj)
65
- end
66
-
67
61
  # @return [Pathname]
68
62
  def root
69
63
  @root ||= Pathname.new(File.expand_path('../', File.dirname(__FILE__)))
@@ -71,6 +65,4 @@ module Ridley
71
65
  end
72
66
  end
73
67
 
74
- Celluloid.logger = Ridley.logger
75
-
76
68
  require 'ridley/middleware'
data/ridley.gemspec CHANGED
@@ -7,6 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.description = %q{A reliable Chef API client with a clean syntax}
8
8
  s.summary = s.description
9
9
  s.homepage = "https://github.com/reset/ridley"
10
+ s.license = "Apache 2.0"
10
11
 
11
12
  s.files = `git ls-files`.split($\)
12
13
  s.executables = Array.new
@@ -0,0 +1,9 @@
1
+ RSpec.configuration.before(:each) do
2
+ class Celluloid::ActorProxy
3
+ unless @rspec_compatible
4
+ @rspec_compatible = true
5
+ undef_method :should_receive
6
+ undef_method :stub
7
+ end
8
+ end
9
+ end
@@ -146,12 +146,12 @@ shared_examples_for "a Ridley Resource" do |resource_klass|
146
146
  end
147
147
 
148
148
  context "when there is an HTTPConflict" do
149
- it "sends an update message to the implemeneting class" do
149
+ it "sends the update message to self" do
150
150
  updated = double('updated')
151
151
  updated.stub(:[]).and_return(Hash.new)
152
152
  updated.stub(:attributes).and_return(Hash.new)
153
153
  subject.class.should_receive(:create).and_raise(Ridley::Errors::HTTPConflict.new(updated))
154
- subject.class.should_receive(:update).with(connection, subject).and_return(updated)
154
+ subject.should_receive(:update).and_return(updated)
155
155
 
156
156
  subject.save
157
157
  end
@@ -161,7 +161,7 @@ shared_examples_for "a Ridley Resource" do |resource_klass|
161
161
  context "when the object is invalid" do
162
162
  before(:each) { subject.stub(:valid?).and_return(false) }
163
163
 
164
- it "raises an InvalidObject error" do
164
+ it "raises an InvalidResource error" do
165
165
  lambda {
166
166
  subject.save
167
167
  }.should raise_error(Ridley::Errors::InvalidResource)
@@ -169,6 +169,39 @@ shared_examples_for "a Ridley Resource" do |resource_klass|
169
169
  end
170
170
  end
171
171
 
172
+ describe "#update" do
173
+ context "when the object is valid" do
174
+ let(:updated) do
175
+ updated = double('updated')
176
+ updated.stub(:[]).and_return(Hash.new)
177
+ updated.stub(:attributes).and_return(Hash.new)
178
+ updated
179
+ end
180
+
181
+ before(:each) { subject.stub(:valid?).and_return(true) }
182
+
183
+ it "sends an update message to the implementing class" do
184
+ subject.class.should_receive(:update).with(anything, subject).and_return(updated)
185
+ subject.update
186
+ end
187
+
188
+ it "returns true" do
189
+ subject.class.should_receive(:update).with(anything, subject).and_return(updated)
190
+ subject.update.should eql(true)
191
+ end
192
+ end
193
+
194
+ context "when the object is invalid" do
195
+ before(:each) { subject.stub(:valid?).and_return(false) }
196
+
197
+ it "raises an InvalidResource error" do
198
+ lambda {
199
+ subject.update
200
+ }.should raise_error(Ridley::Errors::InvalidResource)
201
+ end
202
+ end
203
+ end
204
+
172
205
  describe "#chef_id" do
173
206
  it "returns the value of the chef_id attribute" do
174
207
  subject.class.attribute(:name)
@@ -17,6 +17,10 @@ describe Ridley::Connection do
17
17
  }
18
18
  end
19
19
 
20
+ it "exposes its options publicly" do
21
+ described_class::OPTIONS.should be_a Array
22
+ end
23
+
20
24
  describe "ClassMethods" do
21
25
  subject { Ridley::Connection }
22
26
 
@@ -35,6 +35,17 @@ describe Ridley::Node do
35
35
  subject.bootstrap(connection, "33.33.33.10", "33.33.33.11", boot_options)
36
36
  end
37
37
  end
38
+
39
+ describe "::merge_data" do
40
+ it "finds the target node and sends it the merge_data message" do
41
+ data = double('data')
42
+ node = double('node')
43
+ node.should_receive(:merge_data).with(data)
44
+ subject.should_receive(:find!).and_return(node)
45
+
46
+ subject.merge_data(connection, node, data)
47
+ end
48
+ end
38
49
  end
39
50
 
40
51
  subject { Ridley::Node.new(connection) }
@@ -285,4 +296,50 @@ describe Ridley::Node do
285
296
  describe "#chef_client" do
286
297
  pending
287
298
  end
299
+
300
+ describe "#put_secret" do
301
+ pending
302
+ end
303
+
304
+ describe "#merge_data" do
305
+ before(:each) do
306
+ subject.name = "reset.riotgames.com"
307
+ subject.should_receive(:update)
308
+ end
309
+
310
+ it "appends items to the run_list" do
311
+ subject.merge_data(run_list: ["cook::one", "cook::two"])
312
+
313
+ subject.run_list.should =~ ["cook::one", "cook::two"]
314
+ end
315
+
316
+ it "ensures the run_list is unique if identical items are given" do
317
+ subject.run_list = [ "cook::one" ]
318
+ subject.merge_data(run_list: ["cook::one", "cook::two"])
319
+
320
+ subject.run_list.should =~ ["cook::one", "cook::two"]
321
+ end
322
+
323
+ it "deep merges attributes into the normal attributes" do
324
+ subject.normal = {
325
+ one: {
326
+ two: {
327
+ four: :deep
328
+ }
329
+ }
330
+ }
331
+ subject.merge_data(attributes: {
332
+ one: {
333
+ two: {
334
+ three: :deep
335
+ }
336
+ }
337
+ })
338
+
339
+ subject.normal[:one][:two].should have_key(:four)
340
+ subject.normal[:one][:two][:four].should eql(:deep)
341
+ subject.normal[:one][:two].should have_key(:three)
342
+ subject.normal[:one][:two][:three].should eql(:deep)
343
+ end
344
+ end
288
345
  end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ridley::SSH::ResponseSet do
4
+ describe "ClassMethods" do
5
+ subject { described_class }
6
+
7
+ describe "::merge!" do
8
+ let(:target) { Ridley::SSH::ResponseSet.new }
9
+ let(:other) { Ridley::SSH::ResponseSet.new }
10
+
11
+ before(:each) do
12
+ other.add_response(Ridley::SSH::Response.new('host.local'))
13
+ end
14
+
15
+ it "returns the mutated target" do
16
+ result = subject.merge!(target, other)
17
+
18
+ result.should eql(target)
19
+ result.should have(1).item
20
+ end
21
+ end
22
+ end
23
+
24
+ subject { described_class.new }
25
+
26
+ describe "#add_response" do
27
+ it "accepts an array of responses" do
28
+ responses = [
29
+ Ridley::SSH::Response.new("one.riotgames.com"),
30
+ Ridley::SSH::Response.new("two.riotgames.com")
31
+ ]
32
+ subject.add_response(responses)
33
+
34
+ subject.responses.should have(2).items
35
+ end
36
+
37
+ it "accepts a single response" do
38
+ response = Ridley::SSH::Response.new("one.riotgames.com")
39
+ subject.add_response(response)
40
+
41
+ subject.responses.should have(1).item
42
+ end
43
+ end
44
+
45
+ describe "#responses" do
46
+ it "returns an array of Ridley::SSH::Response objects including both failures and successes" do
47
+ responses = [
48
+ double('success', error?: false),
49
+ double('failure', error?: true)
50
+ ]
51
+ subject.add_response(responses)
52
+
53
+ subject.responses.should have(2).items
54
+ end
55
+ end
56
+
57
+ describe "#successes" do
58
+ it "returns an array of Ridley::SSH::Response objects only including the successes" do
59
+ responses = [
60
+ double('success', error?: false),
61
+ double('failure', error?: true)
62
+ ]
63
+ subject.add_response(responses)
64
+
65
+ subject.successes.should have(1).item
66
+ end
67
+ end
68
+
69
+ describe "#failures" do
70
+ it "returns an array of Ridley::SSH::Response objects only including the failures" do
71
+ responses = [
72
+ double('success', error?: false),
73
+ double('failure', error?: true)
74
+ ]
75
+ subject.add_response(responses)
76
+
77
+ subject.failures.should have(1).item
78
+ end
79
+ end
80
+
81
+ describe "#merge" do
82
+ let(:target) { Ridley::SSH::ResponseSet.new }
83
+ let(:other) { Ridley::SSH::ResponseSet.new }
84
+
85
+ before(:each) do
86
+ other.add_response(Ridley::SSH::Response.new('host.local'))
87
+ end
88
+
89
+ it "returns a new Ridley::SSH::ResponseSet object" do
90
+ result = target.merge(other)
91
+
92
+ result.should have(1).item
93
+ target.should have(0).items
94
+ end
95
+ end
96
+
97
+ describe "#merge!" do
98
+ let(:target) { Ridley::SSH::ResponseSet.new }
99
+ let(:other) { Ridley::SSH::ResponseSet.new }
100
+
101
+ before(:each) do
102
+ other.add_response(Ridley::SSH::Response.new('host.local'))
103
+ end
104
+
105
+ it "returns the mutated target" do
106
+ result = target.merge!(other)
107
+
108
+ result.should have(1).item
109
+ target.should have(1).item
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ridley::SSH::Worker do
4
+ describe "ClassMethods" do
5
+ subject { described_class }
6
+
7
+ describe "::new" do
8
+ it { subject.new(sudo: true).sudo.should be_true }
9
+ it { subject.new(sudo: false).sudo.should be_false }
10
+ it { subject.new().sudo.should be_false }
11
+ end
12
+ end
13
+ end
@@ -15,17 +15,34 @@ describe Ridley::SSH do
15
15
  subject { Ridley::SSH }
16
16
 
17
17
  describe "::start" do
18
- pending
18
+ let(:options) do
19
+ {
20
+ user: "vagrant",
21
+ password: "vagrant"
22
+ }
23
+ end
24
+
25
+ it "evaluates within the context of a new SSH and returns the last item in the block" do
26
+ result = subject.start([node_one, node_two], options) do |ssh|
27
+ ssh.run("ls")
28
+ end
29
+
30
+ result.should be_a(Ridley::SSH::ResponseSet)
31
+ end
32
+
33
+ it "raises a LocalJumpError if a block is not provided" do
34
+ expect {
35
+ subject.start([node_one, node_two], options)
36
+ }.to raise_error(LocalJumpError)
37
+ end
19
38
  end
20
39
  end
21
40
 
22
41
  subject { Ridley::SSH.new([node_one, node_two], user: "vagrant", password: "vagrant") }
23
42
 
24
- describe "#workers" do
25
- pending
26
- end
27
-
28
43
  describe "#run" do
29
- pending
44
+ it "returns an SSH::ResponseSet" do
45
+ subject.run("ls").should be_a(Ridley::SSH::ResponseSet)
46
+ end
30
47
  end
31
48
  end