sprinkle 0.4.2 → 0.5.0.rc1

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.
Files changed (87) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +54 -0
  4. data/README.markdown +178 -166
  5. data/Rakefile +4 -28
  6. data/bin/sprinkle +14 -1
  7. data/lib/sprinkle.rb +5 -1
  8. data/lib/sprinkle/actors/actors.rb +20 -5
  9. data/lib/sprinkle/actors/capistrano.rb +62 -36
  10. data/lib/sprinkle/actors/dummy.rb +127 -0
  11. data/lib/sprinkle/actors/local.rb +59 -17
  12. data/lib/sprinkle/actors/ssh.rb +189 -107
  13. data/lib/sprinkle/actors/vlad.rb +51 -32
  14. data/lib/sprinkle/configurable.rb +2 -1
  15. data/lib/sprinkle/deployment.rb +22 -2
  16. data/lib/sprinkle/errors/pretty_failure.rb +41 -0
  17. data/lib/sprinkle/errors/remote_command_failure.rb +24 -0
  18. data/lib/sprinkle/errors/transfer_failure.rb +28 -0
  19. data/lib/sprinkle/installers/apt.rb +17 -16
  20. data/lib/sprinkle/installers/binary.rb +23 -8
  21. data/lib/sprinkle/installers/brew.rb +17 -10
  22. data/lib/sprinkle/installers/bsd_port.rb +10 -6
  23. data/lib/sprinkle/installers/deb.rb +3 -10
  24. data/lib/sprinkle/installers/freebsd_pkg.rb +5 -11
  25. data/lib/sprinkle/installers/freebsd_portinstall.rb +8 -2
  26. data/lib/sprinkle/installers/gem.rb +9 -3
  27. data/lib/sprinkle/installers/group.rb +28 -4
  28. data/lib/sprinkle/installers/installer.rb +58 -7
  29. data/lib/sprinkle/installers/mac_port.rb +13 -6
  30. data/lib/sprinkle/installers/npm.rb +42 -0
  31. data/lib/sprinkle/installers/openbsd_pkg.rb +4 -11
  32. data/lib/sprinkle/installers/opensolaris_pkg.rb +7 -13
  33. data/lib/sprinkle/installers/package_installer.rb +33 -0
  34. data/lib/sprinkle/installers/pacman.rb +5 -13
  35. data/lib/sprinkle/installers/pear.rb +40 -0
  36. data/lib/sprinkle/installers/push_text.rb +18 -5
  37. data/lib/sprinkle/installers/rake.rb +7 -2
  38. data/lib/sprinkle/installers/reconnect.rb +29 -0
  39. data/lib/sprinkle/installers/replace_text.rb +11 -2
  40. data/lib/sprinkle/installers/rpm.rb +8 -6
  41. data/lib/sprinkle/installers/runner.rb +41 -16
  42. data/lib/sprinkle/installers/smart.rb +6 -17
  43. data/lib/sprinkle/installers/source.rb +22 -10
  44. data/lib/sprinkle/installers/thor.rb +7 -0
  45. data/lib/sprinkle/installers/transfer.rb +62 -41
  46. data/lib/sprinkle/installers/user.rb +34 -4
  47. data/lib/sprinkle/installers/yum.rb +10 -10
  48. data/lib/sprinkle/installers/zypper.rb +4 -15
  49. data/lib/sprinkle/package.rb +81 -98
  50. data/lib/sprinkle/policy.rb +11 -4
  51. data/lib/sprinkle/utility/log_recorder.rb +33 -0
  52. data/lib/sprinkle/verifiers/directory.rb +1 -1
  53. data/lib/sprinkle/verifiers/executable.rb +1 -1
  54. data/lib/sprinkle/verifiers/file.rb +11 -2
  55. data/lib/sprinkle/verifiers/package.rb +2 -14
  56. data/lib/sprinkle/verifiers/permission.rb +40 -0
  57. data/lib/sprinkle/verifiers/symlink.rb +2 -2
  58. data/lib/sprinkle/verifiers/test.rb +21 -0
  59. data/lib/sprinkle/verify.rb +3 -3
  60. data/lib/sprinkle/version.rb +3 -0
  61. data/spec/fixtures/my_file.txt +1 -0
  62. data/spec/sprinkle/actors/capistrano_spec.rb +16 -3
  63. data/spec/sprinkle/actors/local_spec.rb +24 -6
  64. data/spec/sprinkle/actors/ssh_spec.rb +38 -0
  65. data/spec/sprinkle/installers/apt_spec.rb +23 -2
  66. data/spec/sprinkle/installers/binary_spec.rb +22 -14
  67. data/spec/sprinkle/installers/brew_spec.rb +4 -4
  68. data/spec/sprinkle/installers/installer_spec.rb +36 -7
  69. data/spec/sprinkle/installers/npm_spec.rb +16 -0
  70. data/spec/sprinkle/installers/pear_spec.rb +16 -0
  71. data/spec/sprinkle/installers/push_text_spec.rb +23 -1
  72. data/spec/sprinkle/installers/rpm_spec.rb +5 -0
  73. data/spec/sprinkle/installers/runner_spec.rb +27 -11
  74. data/spec/sprinkle/installers/smart_spec.rb +60 -0
  75. data/spec/sprinkle/installers/source_spec.rb +4 -4
  76. data/spec/sprinkle/installers/transfer_spec.rb +31 -16
  77. data/spec/sprinkle/package_spec.rb +10 -2
  78. data/spec/sprinkle/policy_spec.rb +6 -0
  79. data/spec/sprinkle/verify_spec.rb +18 -4
  80. data/sprinkle.gemspec +22 -158
  81. metadata +178 -96
  82. data/TODO +0 -56
  83. data/VERSION +0 -1
  84. data/lib/sprinkle/verifiers/apt.rb +0 -21
  85. data/lib/sprinkle/verifiers/brew.rb +0 -21
  86. data/lib/sprinkle/verifiers/rpm.rb +0 -21
  87. data/lib/sprinkle/verifiers/users_groups.rb +0 -33
@@ -3,148 +3,241 @@ require 'net/scp'
3
3
 
4
4
  module Sprinkle
5
5
  module Actors
6
- class Ssh
7
- attr_accessor :options
8
-
6
+ # The SSH actor requires no additional deployment tools other than the
7
+ # Ruby SSH libraries.
8
+ #
9
+ # deployment do
10
+ # delivery :ssh do
11
+ # user "rails"
12
+ # password "leetz"
13
+ #
14
+ # role :app, "app.myserver.com"
15
+ # end
16
+ # end
17
+ #
18
+ #
19
+ # == Working thru a gateway
20
+ #
21
+ # If you're behind a firewall and need to use a SSH gateway that's fine.
22
+ #
23
+ # deployment do
24
+ # delivery :ssh do
25
+ # gateway "work.sshgateway.com"
26
+ # end
27
+ # end
28
+ class SSH
29
+ attr_accessor :options #:nodoc:
30
+
31
+ class SSHCommandFailure < StandardError #:nodoc:
32
+ attr_accessor :details
33
+ end
34
+
35
+ class SSHConnectionCache
36
+ def initialize; @cache={}; end
37
+ def start(host, user, opts={})
38
+ key="#{host}#{user}#{opts.to_s}"
39
+ @cache[key] ||= Net::SSH.start(host,user,opts)
40
+ end
41
+ end
42
+
43
+
9
44
  def initialize(options = {}, &block) #:nodoc:
10
45
  @options = options.update(:user => 'root')
46
+ @roles = {}
47
+ @connection_cache = SSHConnectionCache.new
11
48
  self.instance_eval &block if block
49
+ raise "You must define at least a single role." if @roles.empty?
12
50
  end
13
51
 
52
+ # Define a whole host of roles at once
53
+ #
54
+ # This is depreciated - you should be using role instead.
14
55
  def roles(roles)
15
- @options[:roles] = roles
56
+ @roles = roles
57
+ end
58
+
59
+ # Define a role and add servers to it
60
+ #
61
+ # role :app, "app.server.com"
62
+ # role :db, "db.server.com"
63
+ def role(role, server)
64
+ @roles[role] ||= []
65
+ @roles[role] << server
16
66
  end
17
67
 
68
+ # Set an optional SSH gateway server - if set all outbound SSH traffic
69
+ # will go thru this gateway
18
70
  def gateway(gateway)
19
71
  @options[:gateway] = gateway
20
72
  end
21
73
 
74
+ # Set the SSH user
22
75
  def user(user)
23
76
  @options[:user] = user
24
77
  end
25
78
 
79
+ # Set the SSH password
26
80
  def password(password)
27
81
  @options[:password] = password
28
82
  end
29
83
 
30
- def process(name, commands, roles, suppress_and_return_failures = false)
31
- return process_with_gateway(name, commands, roles) if gateway_defined?
32
- r = process_direct(name, commands, roles)
33
- logger.debug green "process returning #{r}"
34
- return r
84
+ # Set this to true to prepend 'sudo' to every command.
85
+ def use_sudo(value)
86
+ @options[:use_sudo] = value
35
87
  end
36
88
 
37
- def transfer(name, source, destination, roles, recursive = true, suppress_and_return_failures = false)
38
- return transfer_with_gateway(name, source, destination, roles, recursive) if gateway_defined?
39
- transfer_direct(name, source, destination, roles, recursive)
89
+ def setup_gateway #:nodoc:
90
+ @gateway ||= Net::SSH::Gateway.new(@options[:gateway], @options[:user]) if @options[:gateway]
91
+ end
92
+
93
+ def teardown #:nodoc:
94
+ @gateway.shutdown! if @gateway
95
+ end
96
+
97
+ def verify(verifier, roles, opts = {}) #:nodoc:
98
+ @verifier = verifier
99
+ # issue all the verification steps in a single SSH command
100
+ commands=[verifier.commands.join(" && ")]
101
+ process(verifier.package.name, commands, roles,
102
+ :suppress_and_return_failures => true)
103
+ ensure
104
+ @verifier = nil
105
+ end
106
+
107
+ def install(installer, roles, opts = {}) #:nodoc:
108
+ @installer = installer
109
+ process(installer.package.name, installer.install_sequence, roles)
110
+ rescue SSHCommandFailure => e
111
+ raise_error(e)
112
+ ensure
113
+ @installer = nil
40
114
  end
41
-
115
+
42
116
  protected
43
117
 
44
- def process_with_gateway(name, commands, roles)
45
- res = []
46
- on_gateway do |gateway|
47
- Array(roles).each { |role| res << execute_on_role(commands, role, gateway) }
118
+ def raise_error(e)
119
+ raise Sprinkle::Errors::RemoteCommandFailure.new(@installer, e.details, e)
120
+ end
121
+
122
+ def process(name, commands, roles, opts = {}) #:nodoc:
123
+ opts.reverse_merge!(:suppress_and_return_failures => false)
124
+ setup_gateway
125
+ @suppress = opts[:suppress_and_return_failures]
126
+ r=execute_on_role(commands, roles)
127
+ logger.debug green "process returning #{r}"
128
+ return r
129
+ end
130
+
131
+ def execute_on_role(commands, role) #:nodoc:
132
+ hosts = @roles[role]
133
+ Array(hosts).each do |host|
134
+ success = execute_on_host(commands, host)
135
+ return false unless success
48
136
  end
49
- !(res.include? false)
50
137
  end
51
138
 
52
- def process_direct(name, commands, roles)
53
- res = []
54
- Array(roles).each { |role| res << execute_on_role(commands, role) }
55
- !(res.include? false)
139
+ def prepare_commands(commands)
140
+ return commands unless @options[:use_sudo]
141
+ commands.map do |command|
142
+ next command if command.is_a?(Symbol)
143
+ command.match(/^sudo/) ? command : "sudo #{command}"
144
+ end
56
145
  end
57
146
 
58
- def transfer_with_gateway(name, source, destination, roles, recursive)
59
- on_gateway do |gateway|
60
- Array(roles).each { |role| transfer_to_role(source, destination, role, recursive, gateway) }
147
+ def execute_on_host(commands,host) #:nodoc:
148
+ session = ssh_session(host)
149
+ @log_recorder = Sprinkle::Utility::LogRecorder.new
150
+ prepare_commands(commands).each do |cmd|
151
+ if cmd == :TRANSFER
152
+ transfer_to_host(@installer.sourcepath, @installer.destination, session,
153
+ :recursive => @installer.options[:recursive])
154
+ next
155
+ elsif cmd == :RECONNECT
156
+ session.close # disconnenct
157
+ session = ssh_session(host) # reconnect
158
+ next
159
+ end
160
+ @log_recorder.reset cmd
161
+ res = ssh(session, cmd)
162
+ if res != 0
163
+ if @suppress
164
+ return false
165
+ else
166
+ fail=SSHCommandFailure.new
167
+ fail.details = @log_recorder.hash.merge(:hosts => host)
168
+ raise fail, "#{cmd} failed with error code #{res[:code]}"
169
+ end
170
+ end
61
171
  end
172
+ true
62
173
  end
63
174
 
64
- def transfer_direct(name, source, destination, roles, recursive)
65
- Array(roles).each { |role| transfer_to_role(source, destination, role, recursive) }
66
- end
67
-
68
- def execute_on_role(commands, role, gateway = nil)
69
- hosts = @options[:roles][role]
70
- res = []
71
- Array(hosts).each { |host| res << execute_on_host(commands, host, gateway) }
72
- !(res.include? false)
73
- end
74
-
75
- def transfer_to_role(source, destination, role, gateway = nil)
76
- hosts = @options[:roles][role]
77
- Array(hosts).each { |host| transfer_to_host(source, destination, host, gateway) }
175
+ def ssh(host, cmd, opts={}) #:nodoc:
176
+ logger.debug "ssh: #{cmd}"
177
+ session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
178
+ channel_runner(session, cmd)
78
179
  end
79
180
 
80
- def execute_on_host(commands, host, gateway = nil)
81
- res = nil
82
- logger.debug(blue "executing #{commands.inspect} on #{host}.")
83
- if gateway # SSH connection via gateway
84
- gateway.ssh(host, @options[:user]) do |ssh|
85
- res = execute_on_connection(commands, ssh)
86
- ssh.loop
181
+ def channel_runner(session, command) #:nodoc:
182
+ session.open_channel do |channel|
183
+ channel.on_data do |ch, data|
184
+ @log_recorder.log :out, data
185
+ logger.debug yellow("stdout said-->\n#{data}\n")
87
186
  end
88
- else # direct SSH connection
89
- Net::SSH.start(host, @options[:user], :password => @options[:password]) do |ssh|
90
- res = execute_on_connection(commands, ssh)
91
- ssh.loop
187
+ channel.on_extended_data do |ch, type, data|
188
+ next unless type == 1 # only handle stderr
189
+ @log_recorder.log :err, data
190
+ logger.debug red("stderr said -->\n#{data}\n")
92
191
  end
93
- end
94
- res.detect{|x| x!=0}.nil?
95
- end
96
-
97
- def execute_on_connection(commands, session)
98
- res = []
99
- Array(commands).each do |cmd|
100
- session.open_channel do |channel|
101
- channel.on_data do |ch, data|
102
- logger.debug yellow("stdout said-->\n#{data}\n")
103
- end
104
- channel.on_extended_data do |ch, type, data|
105
- next unless type == 1 # only handle stderr
106
- logger.debug red("stderr said -->\n#{data}\n")
107
- end
108
192
 
109
- channel.on_request("exit-status") do |ch, data|
110
- exit_code = data.read_long
111
- if exit_code == 0
112
- logger.debug(green 'success')
113
- else
114
- logger.debug(red('failed (%d).'%exit_code))
115
- end
116
- res << exit_code
117
- end
118
-
119
- channel.on_request("exit-signal") do |ch, data|
120
- logger.debug red("#{cmd} was signaled!: #{data.read_long}")
121
- end
122
-
123
- channel.exec cmd do |ch, status|
124
- logger.error("couldn't run remote command #{cmd}") unless status
193
+ channel.on_request("exit-status") do |ch, data|
194
+ @log_recorder.code = data.read_long
195
+ if @log_recorder.code == 0
196
+ logger.debug(green 'success')
197
+ else
198
+ logger.debug(red('failed (%d).' % @log_recorder.code))
125
199
  end
126
200
  end
127
- end
128
- res
129
- end
130
201
 
131
- def transfer_to_host(source, destination, host, recursive, gateway = nil)
132
- if gateway # SSH connection via gateway
133
- gateway.ssh(host, @options[:user]) do |ssh|
134
- transfer_on_connection(source, destination, recursive, ssh)
202
+ channel.on_request("exit-signal") do |ch, data|
203
+ logger.debug red("#{cmd} was signaled!: #{data.read_long}")
135
204
  end
136
- else # direct SSH connection
137
- Net::SSH.start(host, @options[:user]) do |ssh|
138
- transfer_on_connection(source, destination, recursive, ssh)
205
+
206
+ channel.exec command do |ch, status|
207
+ logger.error("couldn't run remote command #{cmd}") unless status
208
+ @log_recorder.code = -1
139
209
  end
140
210
  end
211
+ session.loop
212
+ @log_recorder.code
141
213
  end
142
-
143
- def transfer_on_connection(source, destination, recursive, connection)
144
- scp = Net::SCP.new(connection)
145
- scp.upload! source, destination, :recursive => recursive
214
+
215
+ def transfer_to_role(source, destination, role, opts={}) #:nodoc:
216
+ hosts = @roles[role]
217
+ Array(hosts).each { |host| transfer_to_host(source, destination, host, opts) }
146
218
  end
147
-
219
+
220
+ def transfer_to_host(source, destination, host, opts={}) #:nodoc:
221
+ logger.debug "upload: #{destination}"
222
+ session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
223
+ scp = Net::SCP.new(session)
224
+ scp.upload! source, destination, :recursive => opts[:recursive], :chunk_size => 32.kilobytes
225
+ rescue RuntimeError => e
226
+ if e.message =~ /Permission denied/
227
+ raise TransferFailure.no_permission(@installer,e)
228
+ else
229
+ raise e
230
+ end
231
+ end
232
+
233
+ def ssh_session(host)
234
+ if @gateway
235
+ gateway.ssh(host, @options[:user])
236
+ else
237
+ @connection_cache.start(host, @options[:user],:password => @options[:password])
238
+ end
239
+ end
240
+
148
241
  private
149
242
  def color(code, s)
150
243
  "\033[%sm%s\033[0m"%[code,s]
@@ -161,17 +254,6 @@ module Sprinkle
161
254
  def blue(s)
162
255
  color(34, s)
163
256
  end
164
-
165
- def gateway_defined?
166
- !! @options[:gateway]
167
- end
168
-
169
- def on_gateway(&block)
170
- gateway = Net::SSH::Gateway.new(@options[:gateway], @options[:user])
171
- block.call gateway
172
- ensure
173
- gateway.shutdown!
174
- end
175
257
  end
176
258
  end
177
259
  end
@@ -1,9 +1,9 @@
1
+ require 'vlad'
2
+
1
3
  module Sprinkle
2
4
  module Actors
3
- # = Vlad Delivery Method
4
- #
5
- # Vlad is one of the delivery method options available out of the
6
- # box with Sprinkle. If you have the vlad the deployer gem install, you
5
+ # The Vlad actor is one of the delivery method options available out of the
6
+ # box with Sprinkle. If you have the vlad the deployer gem installed, you
7
7
  # may use this delivery. The only configuration option available, and
8
8
  # which is mandatory to include is +script+. An example:
9
9
  #
@@ -17,7 +17,6 @@ module Sprinkle
17
17
  # These recipes are mainly to set variables such as :user, :password, and to
18
18
  # set the app domain which will be sprinkled.
19
19
  class Vlad
20
- require 'vlad'
21
20
  attr_accessor :loaded_recipes #:nodoc:
22
21
 
23
22
  def initialize(&block) #:nodoc:
@@ -39,38 +38,58 @@ module Sprinkle
39
38
  require name
40
39
  @loaded_recipes << name
41
40
  end
42
-
43
- def process(name, commands, roles, suppress_and_return_failures = false) #:nodoc:
44
- commands = Array(commands)
45
- if use_sudo
46
- commands = commands.map{|x| "sudo #{x}"}
41
+
42
+ def install(installer, roles, opts={})
43
+ @installer=installer
44
+ if installer.install_sequence.include?(:TRANSFER)
45
+ process_with_transfer(installer.package.name, installer.install_sequence, roles, opts)
46
+ else
47
+ process(installer.package.name, installer.install_sequence, roles, opts)
47
48
  end
49
+ # recast our rake error to the common sprinkle error type
50
+ rescue ::Rake::CommandFailedError => e
51
+ raise Sprinkle::Errors::RemoteCommandFailure.new(installer, {}, e)
52
+ ensure
53
+ @installer = nil
54
+ end
55
+
56
+ def verify(verifier, roles, opts={})
57
+ process(verifier.package.name, commands, roles,
58
+ :suppress_and_return_failures => true)
59
+ end
60
+
61
+ protected
62
+
63
+ def process(name, commands, roles, opts ={}) #:nodoc:
64
+ commands = commands.map{|x| "sudo #{x}"} if use_sudo
48
65
  commands = commands.join(' && ')
49
66
  puts "executing #{commands}"
50
- t = remote_task(task_sym(name), :roles => roles) { run commands }
51
-
52
- begin
53
- t.invoke
54
- return true
55
- rescue ::Rake::CommandFailedError => e
56
- return false if suppress_and_return_failures
57
-
58
- # Reraise error if we're not suppressing it
59
- raise
60
- end
67
+ task = remote_task(task_sym(name), :roles => roles) { run commands }
68
+ invoke(task)
61
69
  end
62
-
63
- # Sorry, all transfers are recursive
64
- def transfer(name, source, destination, roles, recursive = true, suppress_and_return_failures = false) #:nodoc:
65
- begin
66
- rsync source, destination
67
- return true
68
- rescue ::Rake::CommandFailedError => e
69
- return false if suppress_and_return_failures
70
-
71
- # Reraise error if we're not suppressing it
72
- raise
70
+
71
+ def process_with_transfer(name, commands, roles, opts ={}) #:nodoc:
72
+ raise "cant do non recursive file transfers, sorry" if opts[:recursive] == false
73
+ commands = commands.map{|x| x == :TRANSFER : x ? "sudo #{x}" } if use_sudo
74
+ i = commands.index(:TRANSFER)
75
+ before = commands.first(i).join(" && ")
76
+ after = commands.last(commands.size-i+1).join(" && ")
77
+ inst = @installer
78
+ task = remote_task(task_sym(name), :roles => roles) do
79
+ run before unless before.empty?
80
+ rsync inst.sourcepath, inst.destination
81
+ run after unless after.empty?
73
82
  end
83
+ invoke(task)
84
+ end
85
+
86
+ def invoke(t)
87
+ t.invoke
88
+ return true
89
+ rescue ::Rake::CommandFailedError => e
90
+ return false if opts[:suppress_and_return_failures]
91
+ # Reraise error if we're not suppressing it
92
+ raise e
74
93
  end
75
94
 
76
95
  private