switchtower 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  require 'erb'
2
2
  require 'switchtower/command'
3
+ require 'switchtower/transfer'
3
4
  require 'switchtower/gateway'
4
5
  require 'switchtower/ssh'
5
6
 
@@ -27,10 +28,12 @@ module SwitchTower
27
28
  class <<self
28
29
  attr_accessor :connection_factory
29
30
  attr_accessor :command_factory
31
+ attr_accessor :transfer_factory
30
32
  end
31
33
 
32
34
  self.connection_factory = DefaultConnectionFactory
33
35
  self.command_factory = Command
36
+ self.transfer_factory = Transfer
34
37
 
35
38
  # The configuration instance associated with this actor.
36
39
  attr_reader :configuration
@@ -63,6 +66,7 @@ module SwitchTower
63
66
 
64
67
  def initialize(name, options)
65
68
  @name, @options = name, options
69
+ @servers = nil
66
70
  end
67
71
 
68
72
  # Returns the list of servers (_not_ connections to servers) that are
@@ -107,7 +111,7 @@ module SwitchTower
107
111
  logger.trace "executing task #{name}"
108
112
  begin
109
113
  push_task_call_frame name
110
- result = instance_eval &block
114
+ result = instance_eval(&block)
111
115
  ensure
112
116
  pop_task_call_frame
113
117
  end
@@ -131,15 +135,7 @@ module SwitchTower
131
135
 
132
136
  logger.debug "executing #{cmd.strip.inspect}"
133
137
 
134
- # get the currently executing task and determine which servers it uses
135
- servers = tasks[task_call_frames.last.name].servers(configuration)
136
- servers = servers.first if options[:once]
137
- logger.trace "servers: #{servers.inspect}"
138
-
139
- if !pretend
140
- # establish connections to those servers, as necessary
141
- establish_connections(servers)
142
-
138
+ execute_on_servers(options) do |servers|
143
139
  # execute the command on each server in parallel
144
140
  command = self.class.command_factory.new(servers, cmd, block, options, self)
145
141
  command.process! # raises an exception if command fails on any server
@@ -158,13 +154,21 @@ module SwitchTower
158
154
  # the current task. If <tt>:mode</tt> is specified it is used to set the
159
155
  # mode on the file.
160
156
  def put(data, path, options={})
161
- # Poor-man's SFTP... just run a cat on the remote end, and send data
162
- # to it.
157
+ if SwitchTower::SFTP
158
+ execute_on_servers(options) do |servers|
159
+ transfer = self.class.transfer_factory.new(servers, self, path, :data => data,
160
+ :mode => options[:mode])
161
+ transfer.process!
162
+ end
163
+ else
164
+ # Poor-man's SFTP... just run a cat on the remote end, and send data
165
+ # to it.
163
166
 
164
- cmd = "cat > #{path}"
165
- cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
166
- run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
167
- logger.important out, "#{stream} :: #{ch[:host]}" if out == :err
167
+ cmd = "cat > #{path}"
168
+ cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
169
+ run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
170
+ logger.important out, "#{stream} :: #{ch[:host]}" if stream == :err
171
+ end
168
172
  end
169
173
  end
170
174
 
@@ -176,9 +180,23 @@ module SwitchTower
176
180
  logger.debug(out, "#{stream} :: #{ch[:host]}")
177
181
  end
178
182
 
183
+ # in order to prevent _each host_ from prompting when the password was
184
+ # wrong, let's track which host prompted first and only allow subsequent
185
+ # prompts from that host.
186
+ prompt_host = nil
187
+
179
188
  run "sudo #{command}", options do |ch, stream, out|
180
189
  if out =~ /^Password:/
181
190
  ch.send_data "#{password}\n"
191
+ elsif out =~ /try again/
192
+ if prompt_host.nil? || prompt_host == ch[:host]
193
+ prompt_host = ch[:host]
194
+ logger.important out, "#{stream} :: #{ch[:host]}"
195
+ # reset the password to it's original value and prepare for another
196
+ # pass (the reset allows the password prompt to be attempted again
197
+ # if the password variable was originally a proc (the default)
198
+ set :password, self[:original_value][:password] || self[:password]
199
+ end
182
200
  else
183
201
  block.call(ch, stream, out)
184
202
  end
@@ -339,6 +357,18 @@ module SwitchTower
339
357
  gateway && !@established_gateway
340
358
  end
341
359
 
360
+ def execute_on_servers(options)
361
+ servers = tasks[task_call_frames.last.name].servers(configuration)
362
+ servers = servers.first if options[:once]
363
+ logger.trace "servers: #{servers.inspect}"
364
+
365
+ if !pretend
366
+ # establish connections to those servers, as necessary
367
+ establish_connections(servers)
368
+ yield servers
369
+ end
370
+ end
371
+
342
372
  def method_missing(sym, *args, &block)
343
373
  if @configuration.respond_to?(sym)
344
374
  @configuration.send(sym, *args, &block)
@@ -2,11 +2,19 @@ require 'optparse'
2
2
  require 'switchtower'
3
3
 
4
4
  module SwitchTower
5
+ # The CLI class encapsulates the behavior of switchtower when it is invoked
6
+ # as a command-line utility. This allows other programs to embed ST and
7
+ # preserve it's command-line semantics.
5
8
  class CLI
9
+ # Invoke switchtower using the ARGV array as the option parameters. This
10
+ # is what the command-line switchtower utility does.
6
11
  def self.execute!
7
12
  new.execute!
8
13
  end
9
14
 
15
+ # The following determines whether or not echo-suppression is available.
16
+ # This requires the termios library to be installed (which, unfortunately,
17
+ # is not available for Windows).
10
18
  begin
11
19
  if !defined?(USE_TERMIOS) || USE_TERMIOS
12
20
  require 'termios'
@@ -15,7 +23,7 @@ module SwitchTower
15
23
  end
16
24
 
17
25
  # Enable or disable stdin echoing to the terminal.
18
- def echo(enable)
26
+ def self.echo(enable)
19
27
  term = Termios::getattr(STDIN)
20
28
 
21
29
  if enable
@@ -27,13 +35,64 @@ module SwitchTower
27
35
  Termios::setattr(STDIN, Termios::TCSANOW, term)
28
36
  end
29
37
  rescue LoadError
30
- def echo(enable)
38
+ def self.echo(enable)
31
39
  end
32
40
  end
33
41
 
34
- attr_reader :options
42
+ # execute the associated block with echo-suppression enabled. Note that
43
+ # if termios is not available, echo suppression will not be available
44
+ # either.
45
+ def self.with_echo
46
+ echo(false)
47
+ yield
48
+ ensure
49
+ echo(true)
50
+ end
51
+
52
+ # Prompt for a password using echo suppression.
53
+ def self.password_prompt(prompt="Password: ")
54
+ sync = STDOUT.sync
55
+ begin
56
+ with_echo do
57
+ STDOUT.sync = true
58
+ print(prompt)
59
+ STDIN.gets.chomp
60
+ end
61
+ ensure
62
+ STDOUT.sync = sync
63
+ puts
64
+ end
65
+ end
66
+
67
+ # The array of (unparsed) command-line options
35
68
  attr_reader :args
36
69
 
70
+ # The hash of (parsed) command-line options
71
+ attr_reader :options
72
+
73
+ # Create a new CLI instance using the given array of command-line parameters
74
+ # to initialize it. By default, +ARGV+ is used, but you can specify a
75
+ # different set of parameters (such as when embedded ST in a program):
76
+ #
77
+ # require 'switchtower/cli'
78
+ # SwitchTower::CLI.new(%w(-vvvv -r config/deploy -a update_code)).execute!
79
+ #
80
+ # Note that you can also embed ST directly by creating a new Configuration
81
+ # instance and setting it up, but you'll often wind up duplicating logic
82
+ # defined in the CLI class. The above snippet, redone using the Configuration
83
+ # class directly, would look like:
84
+ #
85
+ # require 'switchtower'
86
+ # require 'switchtower/cli'
87
+ # config = SwitchTower::Configuration.new
88
+ # config.logger_level = SwitchTower::Logger::TRACE
89
+ # config.set :password, Proc.new { SwitchTower::CLI.password_prompt }
90
+ # config.load "standard", "config/deploy"
91
+ # config.actor.update_code
92
+ #
93
+ # There may be times that you want/need the additional control offered by
94
+ # manipulating the Configuration directly, but generally interfacing with
95
+ # the CLI class is recommended.
37
96
  def initialize(args = ARGV)
38
97
  @args = args
39
98
  @options = { :verbose => 0, :recipes => [], :actions => [], :vars => {},
@@ -120,7 +179,7 @@ module SwitchTower
120
179
  end
121
180
 
122
181
  opts.separator ""
123
- opts.separator <<DETAIL.split(/\n/)
182
+ opts.separator <<-DETAIL.split(/\n/)
124
183
  You can use the --apply-to switch to generate a minimal set of switchtower
125
184
  scripts and recipes for an application. Just specify the path to the application
126
185
  as the argument to --apply-to, like this:
@@ -143,19 +202,7 @@ DETAIL
143
202
 
144
203
  check_options!
145
204
 
146
- password_proc = Proc.new do
147
- sync = STDOUT.sync
148
- begin
149
- echo false
150
- STDOUT.sync = true
151
- print "Password: "
152
- STDIN.gets.chomp
153
- ensure
154
- echo true
155
- STDOUT.sync = sync
156
- puts
157
- end
158
- end
205
+ password_proc = Proc.new { self.class.password_prompt }
159
206
 
160
207
  if !@options.has_key?(:password)
161
208
  @options[:password] = password_proc
@@ -164,6 +211,7 @@ DETAIL
164
211
  end
165
212
  end
166
213
 
214
+ # Beginning running SwitchTower based on the configured options.
167
215
  def execute!
168
216
  if !@options[:recipes].empty?
169
217
  execute_recipes!
@@ -174,6 +222,8 @@ DETAIL
174
222
 
175
223
  private
176
224
 
225
+ # Load the recipes specified by the options, and execute the actions
226
+ # specified.
177
227
  def execute_recipes!
178
228
  config = SwitchTower::Configuration.new
179
229
  config.logger.level = options[:verbose]
@@ -192,6 +242,7 @@ DETAIL
192
242
  options[:actions].each { |action| actor.send action }
193
243
  end
194
244
 
245
+ # Load the Rails generator and apply it to the specified directory.
195
246
  def execute_apply_to!
196
247
  require 'switchtower/generators/rails/loader'
197
248
  Generators::RailsLoader.load! @options
@@ -200,6 +251,7 @@ DETAIL
200
251
  APPLY_TO_OPTIONS = [:apply_to]
201
252
  RECIPE_OPTIONS = [:password]
202
253
 
254
+ # A sanity check to ensure that a valid operation is specified.
203
255
  def check_options!
204
256
  apply_to_given = !(@options.keys & APPLY_TO_OPTIONS).empty?
205
257
  recipe_given = !(@options.keys & RECIPE_OPTIONS).empty? ||
@@ -24,6 +24,7 @@ module SwitchTower
24
24
  def process!
25
25
  logger.debug "processing command"
26
26
 
27
+ since = Time.now
27
28
  loop do
28
29
  active = 0
29
30
  @channels.each do |ch|
@@ -33,6 +34,11 @@ module SwitchTower
33
34
  end
34
35
 
35
36
  break if active == 0
37
+ if Time.now - since >= 1
38
+ since = Time.now
39
+ @channels.each { |ch| ch.connection.ping! }
40
+ end
41
+ sleep 0.01 # a brief respite, to keep the CPU from going crazy
36
42
  end
37
43
 
38
44
  logger.trace "command finished"
@@ -3,7 +3,6 @@ require 'switchtower/logger'
3
3
  require 'switchtower/scm/subversion'
4
4
 
5
5
  module SwitchTower
6
-
7
6
  # Represents a specific SwitchTower configuration. A Configuration instance
8
7
  # may be used to load multiple recipe files, define and describe tasks,
9
8
  # define roles, create an actor, and set configuration variables.
@@ -38,12 +37,17 @@ module SwitchTower
38
37
  @variables = {}
39
38
  @now = Time.now.utc
40
39
 
40
+ # for preserving the original value of Proc-valued variables
41
+ set :original_value, Hash.new
42
+
41
43
  set :application, nil
42
44
  set :repository, nil
43
45
  set :gateway, nil
44
46
  set :user, nil
45
47
  set :password, nil
46
-
48
+
49
+ set :ssh_options, Hash.new
50
+
47
51
  set :deploy_to, Proc.new { "/u/apps/#{application}" }
48
52
 
49
53
  set :version_dir, DEFAULT_VERSION_DIR_NAME
@@ -61,10 +65,14 @@ module SwitchTower
61
65
 
62
66
  alias :[]= :set
63
67
 
64
- # Access a named variable. If the value of the variable is a Proc instance,
65
- # the proc will be invoked and the return value cached and returned.
68
+ # Access a named variable. If the value of the variable responds_to? :call,
69
+ # #call will be invoked (without parameters) and the return value cached
70
+ # and returned.
66
71
  def [](variable)
67
- set variable, @variables[variable].call if Proc === @variables[variable]
72
+ if @variables[variable].respond_to?(:call)
73
+ self[:original_value][variable] = @variables[variable]
74
+ set variable, @variables[variable].call
75
+ end
68
76
  @variables[variable]
69
77
  end
70
78
 
@@ -38,6 +38,12 @@ role :db, "db02.example.com", "db03.example.com"
38
38
  # set :cvs, "/path/to/cvs" # defaults to searching the PATH
39
39
  # set :gateway, "gate.host.com" # default to no gateway
40
40
 
41
+ # =============================================================================
42
+ # SSH OPTIONS
43
+ # =============================================================================
44
+ # ssh_options[:keys] = %w(/path/to/my/key /path/to/another/key)
45
+ # ssh_options[:port] = 25
46
+
41
47
  # =============================================================================
42
48
  # TASKS
43
49
  # =============================================================================
@@ -2,32 +2,47 @@
2
2
  # A set of rake tasks for invoking the SwitchTower automation utility.
3
3
  # =============================================================================
4
4
 
5
- desc "Push the latest revision into production using the release manager"
5
+ # Invoke the given actions via SwitchTower
6
+ def switchtower_invoke(*actions)
7
+ begin
8
+ require 'rubygems'
9
+ rescue LoadError
10
+ # no rubygems to load, so we fail silently
11
+ end
12
+
13
+ require 'switchtower/cli'
14
+
15
+ args = %w[-vvvvv -r config/<%= recipe_file %>]
16
+ args.concat(actions.map { |act| ["-a", act.to_s] }.flatten)
17
+ SwitchTower::CLI.new(args).execute!
18
+ end
19
+
20
+ desc "Push the latest revision into production"
6
21
  task :deploy do
7
- system "switchtower -vvvv -r config/<%= recipe_file %> -a deploy"
22
+ switchtower_invoke :deploy
8
23
  end
9
24
 
10
25
  desc "Rollback to the release before the current release in production"
11
26
  task :rollback do
12
- system "switchtower -vvvv -r config/<%= recipe_file %> -a rollback"
27
+ switchtower_invoke :rollback
13
28
  end
14
29
 
15
30
  desc "Describe the differences between HEAD and the last production release"
16
31
  task :diff_from_last_deploy do
17
- system "switchtower -vvvv -r config/<%= recipe_file %> -a diff_from_last_deploy"
32
+ switchtower_invoke :diff_from_last_deploy
18
33
  end
19
34
 
20
35
  desc "Enumerate all available deployment tasks"
21
36
  task :show_deploy_tasks do
22
- system "switchtower -r config/<%= recipe_file %> -a show_tasks"
37
+ switchtower_invoke :show_tasks
23
38
  end
24
39
 
25
- desc "Execute a specific action using the release manager"
40
+ desc "Execute a specific action using switchtower"
26
41
  task :remote_exec do
27
42
  unless ENV['ACTION']
28
43
  raise "Please specify an action (or comma separated list of actions) via the ACTION environment variable"
29
44
  end
30
45
 
31
- actions = ENV['ACTION'].split(",").map { |a| "-a #{a}" }.join(" ")
32
- system "switchtower -vvvv -r config/<%= recipe_file %> #{actions}"
46
+ actions = ENV['ACTION'].split(",")
47
+ switchtower_invoke(*actions)
33
48
  end
@@ -4,12 +4,18 @@
4
4
  # application servers.
5
5
  # * The :web role has been defined as the set of machines consisting of the
6
6
  # web servers.
7
- # * The Rails spinner and reaper scripts are being used to manage the FCGI
7
+ # * The :db role has been defined as the set of machines consisting of the
8
+ # databases, with exactly one set up as the :primary DB server.
9
+ # * The Rails spawner and reaper scripts are being used to manage the FCGI
8
10
  # processes.
9
- # * There is a script in script/ called "reap" that restarts the FCGI processes
10
11
 
11
12
  set :rake, "rake"
12
13
 
14
+ set :migrate_target, :current
15
+ set :migrate_env, ""
16
+
17
+ set :restart_via, :sudo
18
+
13
19
  desc "Enumerate and describe every available task."
14
20
  task :show_tasks do
15
21
  keys = tasks.keys.sort_by { |a| a.to_s }
@@ -91,14 +97,15 @@ task :symlink, :roles => [:app, :db, :web] do
91
97
  run "ln -nfs #{current_release} #{current_path}"
92
98
  end
93
99
 
94
- desc "Restart the FCGI processes on the app server."
100
+ desc <<-DESC
101
+ Restart the FCGI processes on the app server. This uses the :restart_via
102
+ variable to determine whether to use sudo or not. By default, :restart_via is
103
+ set to :sudo, but you can set it to :run if you are in a shared environment.
104
+ DESC
95
105
  task :restart, :roles => :app do
96
- sudo "#{current_path}/script/process/reaper"
106
+ send(restart_via, "#{current_path}/script/process/reaper")
97
107
  end
98
108
 
99
- set :migrate_target, :current
100
- set :migrate_env, ""
101
-
102
109
  desc <<-DESC
103
110
  Run the migrate rake task. By default, it runs this in the version of the app
104
111
  indicated by the 'current' symlink. (This means you should not invoke this task
@@ -137,7 +144,7 @@ end
137
144
 
138
145
  desc <<-DESC
139
146
  Similar to deploy, but it runs the migrate task on the new release before
140
- updating the symlink. (Note that the update in this case is not atomic,
147
+ updating the symlink. (Note that the update in this case it is not atomic,
141
148
  and transactions are not used, because migrations are not guaranteed to be
142
149
  reversible.)
143
150
  DESC
@@ -173,3 +180,26 @@ task :diff_from_last_deploy do
173
180
  puts diff
174
181
  puts
175
182
  end
183
+
184
+ desc "Update the currently released version of the software directly via an SCM update operation"
185
+ task :update_current do
186
+ source.update(self)
187
+ end
188
+
189
+ desc <<-DESC
190
+ Removes unused releases from the releases directory. By default, the last 5
191
+ releases are retained, but this can be configured with the 'keep_releases'
192
+ variable.
193
+ DESC
194
+ task :cleanup do
195
+ count = (self[:keep_releases] || 5).to_i
196
+ if count >= releases.length
197
+ logger.important "no old releases to clean up"
198
+ else
199
+ logger.info "keeping #{count} of #{releases.length} deployed releases"
200
+ keepers = (releases - releases.last(count)).map { |release|
201
+ File.join(releases_path, release) }.join(" ")
202
+
203
+ sudo "rm -rf #{keepers}"
204
+ end
205
+ end
@@ -21,6 +21,10 @@ module SwitchTower
21
21
  raise "#{self.class} doesn't support diff(from, to)"
22
22
  end
23
23
 
24
+ def update(actor)
25
+ raise "#{self.class} doesn't support update(actor)"
26
+ end
27
+
24
28
  private
25
29
 
26
30
  def run_checkout(actor, guts, &block)
@@ -30,13 +34,28 @@ module SwitchTower
30
34
  command = <<-STR
31
35
  if [[ ! -d #{configuration.release_path} ]]; then
32
36
  #{guts}
33
- echo `date +"%Y-%m-%d %H:%M:%S"` $USER #{configuration.revision} #{directory} >> #{log};
34
- chmod 666 #{log};
37
+ #{logging_commands(directory)}
35
38
  fi
36
39
  STR
37
40
 
38
41
  actor.run(command, &block)
39
42
  end
43
+
44
+ def run_update(actor, guts, &block)
45
+ command = <<-STR
46
+ #{guts}
47
+ #{logging_commands}
48
+ STR
49
+
50
+ actor.run(command, &block)
51
+ end
52
+
53
+ def logging_commands(directory = nil)
54
+ log = "#{configuration.deploy_to}/revisions.log"
55
+
56
+ "echo `date +\"%Y-%m-%d %H:%M:%S\"` $USER #{configuration.revision} #{directory} >> #{log} && " +
57
+ "chmod 666 #{log};"
58
+ end
40
59
  end
41
60
 
42
61
  end
@@ -67,37 +67,50 @@ module SwitchTower
67
67
  # the requested password is the same as the password for logging into the
68
68
  # remote server.)
69
69
  def checkout(actor)
70
- svn = configuration[:svn] ? configuration[:svn] : "svn"
71
-
72
- command = "#{svn} co -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path};"
70
+ op = configuration[:checkout] || "co"
71
+ command = "#{svn} #{op} -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path} &&"
72
+ run_checkout(actor, command, &svn_stream_handler(actor))
73
+ end
73
74
 
74
- run_checkout(actor, command) do |ch, stream, out|
75
- prefix = "#{stream} :: #{ch[:host]}"
76
- actor.logger.info out, prefix
77
- if out =~ /^Password.*:/
78
- actor.logger.info "subversion is asking for a password", prefix
79
- ch.send_data "#{actor.password}\n"
80
- elsif out =~ %r{\(yes/no\)}
81
- actor.logger.info "subversion is asking whether to connect or not",
82
- prefix
83
- ch.send_data "yes\n"
84
- elsif out =~ %r{passphrase}
85
- message = "subversion needs your key's passphrase, sending empty string"
86
- actor.logger.info message, prefix
87
- ch.send_data "\n"
88
- elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
89
- message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
90
- actor.logger.info message, prefix
91
- raise message
92
- end
93
- end
75
+ # Update the current release in-place. This assumes that the original
76
+ # deployment was made using checkout, and not something like export.
77
+ def update(actor)
78
+ command = "cd #{actor.current_path} && #{svn} up -q &&"
79
+ run_update(actor, command, &svn_stream_handler(actor))
94
80
  end
95
81
 
96
82
  private
97
83
 
84
+ def svn
85
+ configuration[:svn] || "svn"
86
+ end
87
+
98
88
  def svn_log(path)
99
89
  `svn log -q -rhead #{path}`
100
90
  end
91
+
92
+ def svn_stream_handler(actor)
93
+ Proc.new do |ch, stream, out|
94
+ prefix = "#{stream} :: #{ch[:host]}"
95
+ actor.logger.info out, prefix
96
+ if out =~ /\bpassword.*:/i
97
+ actor.logger.info "subversion is asking for a password", prefix
98
+ ch.send_data "#{actor.password}\n"
99
+ elsif out =~ %r{\(yes/no\)}
100
+ actor.logger.info "subversion is asking whether to connect or not",
101
+ prefix
102
+ ch.send_data "yes\n"
103
+ elsif out =~ %r{passphrase}
104
+ message = "subversion needs your key's passphrase, sending empty string"
105
+ actor.logger.info message, prefix
106
+ ch.send_data "\n"
107
+ elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
108
+ message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
109
+ actor.logger.info message, prefix
110
+ raise message
111
+ end
112
+ end
113
+ end
101
114
  end
102
115
 
103
116
  end
@@ -1,6 +1,16 @@
1
1
  require 'net/ssh'
2
2
 
3
3
  module SwitchTower
4
+ unless ENV['SKIP_VERSION_CHECK']
5
+ require 'switchtower/version'
6
+ require 'net/ssh/version'
7
+ ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
8
+ required_version = [1,0,5]
9
+ if !Version.check(required_version, ssh_version)
10
+ raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{required_version.join(".")}"
11
+ end
12
+ end
13
+
4
14
  # A helper class for dealing with SSH connections.
5
15
  class SSH
6
16
  # An abstraction to make it possible to connect to the server via public key
@@ -14,12 +24,11 @@ module SwitchTower
14
24
  password_value = nil
15
25
 
16
26
  begin
17
- Net::SSH.start(server,
18
- :username => config.user,
19
- :password => password_value,
20
- :port => port,
21
- :auth_methods => methods.shift,
22
- &block)
27
+ ssh_options = { :username => config.user,
28
+ :password => password_value,
29
+ :port => port,
30
+ :auth_methods => methods.shift }.merge(config.ssh_options)
31
+ Net::SSH.start(server,ssh_options,&block)
23
32
  rescue Net::SSH::AuthenticationFailed
24
33
  raise if methods.empty?
25
34
  password_value = config.password
@@ -0,0 +1,90 @@
1
+ begin
2
+ require 'switchtower/version'
3
+ require 'net/sftp'
4
+ require 'net/sftp/version'
5
+ sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
6
+ required_version = [1,1,0]
7
+ if !SwitchTower::Version.check(required_version, sftp_version)
8
+ warn "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
9
+ SwitchTower::SFTP = false
10
+ else
11
+ SwitchTower::SFTP = true
12
+ end
13
+ rescue LoadError
14
+ SwitchTower::SFTP = false
15
+ end
16
+
17
+ module SwitchTower
18
+
19
+ # This class encapsulates a single file transfer to be performed in parallel
20
+ # across multiple machines, using the SFTP protocol.
21
+ class Transfer
22
+ def initialize(servers, actor, filename, params={}) #:nodoc:
23
+ @servers = servers
24
+ @actor = actor
25
+ @filename = filename
26
+ @params = params
27
+ @completed = 0
28
+ @failed = 0
29
+ @sftps = setup_transfer
30
+ end
31
+
32
+ def logger #:nodoc:
33
+ @actor.logger
34
+ end
35
+
36
+ # Uploads to all specified servers in parallel.
37
+ def process!
38
+ logger.debug "uploading #{@filename}"
39
+
40
+ loop do
41
+ @sftps.each { |sftp| sftp.channel.connection.process(true) }
42
+ break if @completed == @servers.length
43
+ end
44
+
45
+ logger.trace "upload finished"
46
+
47
+ if @failed > 0
48
+ raise "upload of #{@filename} failed on one or more hosts"
49
+ end
50
+
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def setup_transfer
57
+ @servers.map do |server|
58
+ sftp = @actor.sessions[server].sftp
59
+ sftp.connect unless sftp.state == :open
60
+
61
+ sftp.open(@filename, IO::WRONLY | IO::CREAT, @params[:mode] || 0660) do |status, handle|
62
+ break unless check_status("open #{@filename}", server, status)
63
+
64
+ logger.info "uploading data to #{server}:#{@filename}"
65
+ sftp.write(handle, @params[:data] || "") do |status|
66
+ break unless check_status("write to #{server}:#{@filename}", server, status)
67
+ sftp.close_handle(handle) do
68
+ logger.debug "done uploading data to #{server}:#{@filename}"
69
+ @completed += 1
70
+ end
71
+ end
72
+ end
73
+
74
+ sftp
75
+ end
76
+ end
77
+
78
+ def check_status(action, server, status)
79
+ if status.code != Net::SFTP::Session::FX_OK
80
+ logger.error "could not #{action} on #{server} (#{status.message})"
81
+ @failed += 1
82
+ @completed += 1
83
+ return false
84
+ end
85
+
86
+ true
87
+ end
88
+ end
89
+
90
+ end
@@ -1,9 +1,30 @@
1
1
  module SwitchTower
2
2
  module Version #:nodoc:
3
+ # A method for comparing versions of required modules. It expects two
4
+ # arrays as parameters, and returns true if the first is no more than the
5
+ # second.
6
+ def self.check(expected, actual) #:nodoc:
7
+ good = false
8
+ if actual[0] > expected[0]
9
+ good = true
10
+ elsif actual[0] == expected[0]
11
+ if actual[1] > expected[1]
12
+ good = true
13
+ elsif actual[1] == expected[1] && actual[2] >= expected[2]
14
+ good = true
15
+ end
16
+ end
17
+
18
+ good
19
+ end
20
+
3
21
  MAJOR = 0
4
- MINOR = 9
22
+ MINOR = 10
5
23
  TINY = 0
6
24
 
7
25
  STRING = [MAJOR, MINOR, TINY].join(".")
26
+
27
+ SSH_REQUIRED = [1,0,5]
28
+ SFTP_REQUIRED = [1,1,0]
8
29
  end
9
30
  end
@@ -207,4 +207,12 @@ class ConfigurationTest < Test::Unit::TestCase
207
207
  @config.set :scm, :subversion
208
208
  assert_equal "SwitchTower::SCM::Subversion", @config.source.class.name
209
209
  end
210
+
211
+ def test_get_proc_variable_sets_original_value_hash
212
+ @config.set :proc, Proc.new { "foo" }
213
+ assert_nil @config[:original_value][:proc]
214
+ assert_equal "foo", @config[:proc]
215
+ assert_not_nil @config[:original_value][:proc]
216
+ assert @config[:original_value][:proc].respond_to?(:call)
217
+ end
210
218
  end
@@ -51,6 +51,7 @@ class ScmSubversionTest < Test::Unit::TestCase
51
51
 
52
52
  def setup
53
53
  @config = MockConfiguration.new
54
+ @config[:current_path] = "/mwa/ha/ha/current"
54
55
  @config[:repository] = "/hello/world"
55
56
  @config[:svn] = "/path/to/svn"
56
57
  @config[:password] = "chocolatebrownies"
@@ -86,6 +87,21 @@ MSG
86
87
  assert_match %r{/path/to/svn}, @actor.command
87
88
  end
88
89
 
90
+ def test_checkout_via_export
91
+ @actor.story = []
92
+ @config[:checkout] = "export"
93
+ assert_nothing_raised { @scm.checkout(@actor) }
94
+ assert_nil @actor.channels.last.sent_data
95
+ assert_match %r{/path/to/svn export}, @actor.command
96
+ end
97
+
98
+ def test_update
99
+ @actor.story = []
100
+ assert_nothing_raised { @scm.update(@actor) }
101
+ assert_nil @actor.channels.last.sent_data
102
+ assert_match %r{/path/to/svn up}, @actor.command
103
+ end
104
+
89
105
  def test_checkout_needs_ssh_password
90
106
  @actor.story = [[:out, "Password: "]]
91
107
  assert_nothing_raised { @scm.checkout(@actor) }
@@ -97,4 +113,10 @@ MSG
97
113
  assert_nothing_raised { @scm.checkout(@actor) }
98
114
  assert_equal ["chocolatebrownies\n"], @actor.channels.last.sent_data
99
115
  end
116
+
117
+ def test_checkout_needs_alternative_ssh_password
118
+ @actor.story = [[:out, "someone's password: "]]
119
+ assert_nothing_raised { @scm.checkout(@actor) }
120
+ assert_equal ["chocolatebrownies\n"], @actor.channels.last.sent_data
121
+ end
100
122
  end
data/test/utils.rb CHANGED
@@ -25,6 +25,7 @@ class MockConfiguration < Hash
25
25
  def initialize(*args)
26
26
  super
27
27
  self[:release_path] = "/path/to/releases/version"
28
+ self[:ssh_options] = {}
28
29
  end
29
30
 
30
31
  def logger
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.10
2
+ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: switchtower
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.9.0
7
- date: 2005-10-18
6
+ version: 0.10.0
7
+ date: 2006-01-02 00:00:00 -07:00
8
8
  summary: "SwitchTower is a framework and utility for executing commands in parallel on
9
9
  multiple remote machines, via SSH. The primary goal is to simplify and
10
10
  automate the deployment of web applications."
@@ -26,6 +26,8 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
26
26
  version: 0.0.0
27
27
  version:
28
28
  platform: ruby
29
+ signing_key:
30
+ cert_chain:
29
31
  authors:
30
32
  - Jamis Buck
31
33
  files:
@@ -42,6 +44,7 @@ files:
42
44
  - lib/switchtower/recipes
43
45
  - lib/switchtower/scm
44
46
  - lib/switchtower/ssh.rb
47
+ - lib/switchtower/transfer.rb
45
48
  - lib/switchtower/version.rb
46
49
  - lib/switchtower/generators/rails
47
50
  - lib/switchtower/generators/rails/deployment
@@ -84,5 +87,15 @@ dependencies:
84
87
  -
85
88
  - ">="
86
89
  - !ruby/object:Gem::Version
87
- version: 1.0.2
90
+ version: 1.0.5
91
+ version:
92
+ - !ruby/object:Gem::Dependency
93
+ name: net-sftp
94
+ version_requirement:
95
+ version_requirements: !ruby/object:Gem::Version::Requirement
96
+ requirements:
97
+ -
98
+ - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 1.1.0
88
101
  version: