switchtower 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/switchtower/actor.rb +46 -16
- data/lib/switchtower/cli.rb +69 -17
- data/lib/switchtower/command.rb +6 -0
- data/lib/switchtower/configuration.rb +13 -5
- data/lib/switchtower/generators/rails/deployment/templates/deploy.rb +6 -0
- data/lib/switchtower/generators/rails/deployment/templates/switchtower.rake +23 -8
- data/lib/switchtower/recipes/standard.rb +38 -8
- data/lib/switchtower/scm/base.rb +21 -2
- data/lib/switchtower/scm/subversion.rb +36 -23
- data/lib/switchtower/ssh.rb +15 -6
- data/lib/switchtower/transfer.rb +90 -0
- data/lib/switchtower/version.rb +22 -1
- data/test/configuration_test.rb +8 -0
- data/test/scm/subversion_test.rb +22 -0
- data/test/utils.rb +1 -0
- metadata +17 -4
data/lib/switchtower/actor.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
162
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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)
|
data/lib/switchtower/cli.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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? ||
|
data/lib/switchtower/command.rb
CHANGED
@@ -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
|
65
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
37
|
+
switchtower_invoke :show_tasks
|
23
38
|
end
|
24
39
|
|
25
|
-
desc "Execute a specific action using
|
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(",")
|
32
|
-
|
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
|
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
|
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
|
-
|
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
|
data/lib/switchtower/scm/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
command
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
data/lib/switchtower/ssh.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
data/lib/switchtower/version.rb
CHANGED
@@ -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 =
|
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
|
data/test/configuration_test.rb
CHANGED
@@ -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
|
data/test/scm/subversion_test.rb
CHANGED
@@ -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
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.8.
|
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.
|
7
|
-
date:
|
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.
|
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:
|