switchtower 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|