sprinkle 0.4.2 → 0.5.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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