switchtower 0.10.0 → 1.0.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 +51 -20
- data/lib/switchtower/cli.rb +1 -1
- data/lib/switchtower/command.rb +1 -2
- data/lib/switchtower/configuration.rb +49 -7
- data/lib/switchtower/extensions.rb +38 -0
- data/lib/switchtower/gateway.rb +15 -3
- data/lib/switchtower/generators/rails/deployment/templates/switchtower.rake +6 -2
- data/lib/switchtower/logger.rb +8 -5
- data/lib/switchtower/recipes/standard.rb +49 -10
- data/lib/switchtower/scm/base.rb +2 -2
- data/lib/switchtower/scm/baz.rb +118 -0
- data/lib/switchtower/scm/bzr.rb +70 -0
- data/lib/switchtower/scm/cvs.rb +57 -6
- data/lib/switchtower/scm/perforce.rb +139 -0
- data/lib/switchtower/scm/subversion.rb +8 -3
- data/lib/switchtower/transfer.rb +1 -1
- data/lib/switchtower/utils.rb +26 -0
- data/lib/switchtower/version.rb +3 -3
- data/test/actor_test.rb +34 -1
- data/test/configuration_test.rb +16 -1
- data/test/fixtures/custom.rb +3 -0
- data/test/scm/cvs_test.rb +24 -2
- data/test/scm/subversion_test.rb +17 -2
- data/test/utils.rb +8 -0
- metadata +9 -3
data/lib/switchtower/actor.rb
CHANGED
@@ -3,6 +3,7 @@ require 'switchtower/command'
|
|
3
3
|
require 'switchtower/transfer'
|
4
4
|
require 'switchtower/gateway'
|
5
5
|
require 'switchtower/ssh'
|
6
|
+
require 'switchtower/utils'
|
6
7
|
|
7
8
|
module SwitchTower
|
8
9
|
|
@@ -29,12 +30,18 @@ module SwitchTower
|
|
29
30
|
attr_accessor :connection_factory
|
30
31
|
attr_accessor :command_factory
|
31
32
|
attr_accessor :transfer_factory
|
33
|
+
attr_accessor :default_io_proc
|
32
34
|
end
|
33
35
|
|
34
36
|
self.connection_factory = DefaultConnectionFactory
|
35
37
|
self.command_factory = Command
|
36
38
|
self.transfer_factory = Transfer
|
37
39
|
|
40
|
+
self.default_io_proc = Proc.new do |ch, stream, out|
|
41
|
+
level = out == :error ? :important : :info
|
42
|
+
ch[:actor].logger.send(level, out, "#{stream} :: #{ch[:host]}")
|
43
|
+
end
|
44
|
+
|
38
45
|
# The configuration instance associated with this actor.
|
39
46
|
attr_reader :configuration
|
40
47
|
|
@@ -62,18 +69,22 @@ module SwitchTower
|
|
62
69
|
|
63
70
|
# Represents the definition of a single task.
|
64
71
|
class Task #:nodoc:
|
65
|
-
attr_reader :name, :options
|
72
|
+
attr_reader :name, :actor, :options
|
66
73
|
|
67
|
-
def initialize(name, options)
|
68
|
-
@name, @options = name, options
|
74
|
+
def initialize(name, actor, options)
|
75
|
+
@name, @actor, @options = name, actor, options
|
69
76
|
@servers = nil
|
70
77
|
end
|
71
78
|
|
72
79
|
# Returns the list of servers (_not_ connections to servers) that are
|
73
80
|
# the target of this task.
|
74
|
-
def servers
|
81
|
+
def servers
|
75
82
|
unless @servers
|
76
|
-
roles = [*(@options[:roles] || configuration.roles.keys)].
|
83
|
+
roles = [*(@options[:roles] || actor.configuration.roles.keys)].
|
84
|
+
map { |name|
|
85
|
+
actor.configuration.roles[name] or
|
86
|
+
raise ArgumentError, "task #{self.name.inspect} references non-existant role #{name.inspect}"
|
87
|
+
}.flatten
|
77
88
|
only = @options[:only] || {}
|
78
89
|
|
79
90
|
unless only.empty?
|
@@ -105,10 +116,10 @@ module SwitchTower
|
|
105
116
|
# Define a new task for this actor. The block will be invoked when this
|
106
117
|
# task is called.
|
107
118
|
def define_task(name, options={}, &block)
|
108
|
-
@tasks[name] = Task.new(name, options)
|
119
|
+
@tasks[name] = (options[:task_class] || Task).new(name, self, options)
|
109
120
|
define_method(name) do
|
110
121
|
send "before_#{name}" if respond_to? "before_#{name}"
|
111
|
-
logger.
|
122
|
+
logger.debug "executing task #{name}"
|
112
123
|
begin
|
113
124
|
push_task_call_frame name
|
114
125
|
result = instance_eval(&block)
|
@@ -129,10 +140,7 @@ module SwitchTower
|
|
129
140
|
#
|
130
141
|
# If +pretend+ mode is active, this does nothing.
|
131
142
|
def run(cmd, options={}, &block)
|
132
|
-
block ||=
|
133
|
-
logger.debug(out, "#{stream} :: #{ch[:host]}")
|
134
|
-
end
|
135
|
-
|
143
|
+
block ||= default_io_proc
|
136
144
|
logger.debug "executing #{cmd.strip.inspect}"
|
137
145
|
|
138
146
|
execute_on_servers(options) do |servers|
|
@@ -176,9 +184,7 @@ module SwitchTower
|
|
176
184
|
# the sudo password (if required) is the same as the password for logging
|
177
185
|
# in to the server.
|
178
186
|
def sudo(command, options={}, &block)
|
179
|
-
block ||=
|
180
|
-
logger.debug(out, "#{stream} :: #{ch[:host]}")
|
181
|
-
end
|
187
|
+
block ||= default_io_proc
|
182
188
|
|
183
189
|
# in order to prevent _each host_ from prompting when the password was
|
184
190
|
# wrong, let's track which host prompted first and only allow subsequent
|
@@ -320,11 +326,29 @@ module SwitchTower
|
|
320
326
|
task_call_frames.last.rollback = block
|
321
327
|
end
|
322
328
|
|
323
|
-
|
329
|
+
# An instance-level reader for the class' #default_io_proc attribute.
|
330
|
+
def default_io_proc
|
331
|
+
self.class.default_io_proc
|
332
|
+
end
|
324
333
|
|
325
|
-
|
326
|
-
|
327
|
-
|
334
|
+
# Used to force connections to be made to the current task's servers.
|
335
|
+
# Connections are normally made lazily in SwitchTower--you can use this
|
336
|
+
# to force them open before performing some operation that might be
|
337
|
+
# time-sensitive.
|
338
|
+
def connect!(options={})
|
339
|
+
execute_on_servers(options) { }
|
340
|
+
end
|
341
|
+
|
342
|
+
def current_task
|
343
|
+
return nil if task_call_frames.empty?
|
344
|
+
tasks[task_call_frames.last.name]
|
345
|
+
end
|
346
|
+
|
347
|
+
def metaclass
|
348
|
+
class << self; self; end
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
328
352
|
|
329
353
|
def define_method(name, &block)
|
330
354
|
metaclass.send(:define_method, name, &block)
|
@@ -358,8 +382,14 @@ module SwitchTower
|
|
358
382
|
end
|
359
383
|
|
360
384
|
def execute_on_servers(options)
|
361
|
-
|
362
|
-
servers = servers
|
385
|
+
task = current_task
|
386
|
+
servers = task.servers
|
387
|
+
|
388
|
+
if servers.empty?
|
389
|
+
raise "The #{task.name} task is only run for servers matching #{task.options.inspect}, but no servers matched"
|
390
|
+
end
|
391
|
+
|
392
|
+
servers = [servers.first] if options[:once]
|
363
393
|
logger.trace "servers: #{servers.inspect}"
|
364
394
|
|
365
395
|
if !pretend
|
@@ -376,5 +406,6 @@ module SwitchTower
|
|
376
406
|
super
|
377
407
|
end
|
378
408
|
end
|
409
|
+
|
379
410
|
end
|
380
411
|
end
|
data/lib/switchtower/cli.rb
CHANGED
@@ -86,7 +86,7 @@ module SwitchTower
|
|
86
86
|
# require 'switchtower/cli'
|
87
87
|
# config = SwitchTower::Configuration.new
|
88
88
|
# config.logger_level = SwitchTower::Logger::TRACE
|
89
|
-
# config.set
|
89
|
+
# config.set(:password) { SwitchTower::CLI.password_prompt }
|
90
90
|
# config.load "standard", "config/deploy"
|
91
91
|
# config.actor.update_code
|
92
92
|
#
|
data/lib/switchtower/command.rb
CHANGED
@@ -22,8 +22,6 @@ module SwitchTower
|
|
22
22
|
# fails (non-zero return code) on any of the hosts, this will raise a
|
23
23
|
# RuntimeError.
|
24
24
|
def process!
|
25
|
-
logger.debug "processing command"
|
26
|
-
|
27
25
|
since = Time.now
|
28
26
|
loop do
|
29
27
|
active = 0
|
@@ -56,6 +54,7 @@ module SwitchTower
|
|
56
54
|
@servers.map do |server|
|
57
55
|
@actor.sessions[server].open_channel do |channel|
|
58
56
|
channel[:host] = server
|
57
|
+
channel[:actor] = @actor # so callbacks can access the actor instance
|
59
58
|
channel.request_pty :want_reply => true
|
60
59
|
|
61
60
|
channel.on_success do |ch|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'switchtower/actor'
|
2
2
|
require 'switchtower/logger'
|
3
3
|
require 'switchtower/scm/subversion'
|
4
|
+
require 'switchtower/extensions'
|
4
5
|
|
5
6
|
module SwitchTower
|
6
7
|
# Represents a specific SwitchTower configuration. A Configuration instance
|
@@ -29,6 +30,9 @@ module SwitchTower
|
|
29
30
|
# determining the release path.
|
30
31
|
attr_reader :now
|
31
32
|
|
33
|
+
# The has of variables currently known by the configuration
|
34
|
+
attr_reader :variables
|
35
|
+
|
32
36
|
def initialize(actor_class=Actor) #:nodoc:
|
33
37
|
@roles = Hash.new { |h,k| h[k] = [] }
|
34
38
|
@actor = actor_class.new(self)
|
@@ -48,18 +52,28 @@ module SwitchTower
|
|
48
52
|
|
49
53
|
set :ssh_options, Hash.new
|
50
54
|
|
51
|
-
set
|
55
|
+
set(:deploy_to) { "/u/apps/#{application}" }
|
52
56
|
|
53
57
|
set :version_dir, DEFAULT_VERSION_DIR_NAME
|
54
58
|
set :current_dir, DEFAULT_CURRENT_DIR_NAME
|
55
59
|
set :shared_dir, DEFAULT_SHARED_DIR_NAME
|
56
60
|
set :scm, :subversion
|
57
61
|
|
58
|
-
set
|
62
|
+
set(:revision) { source.latest_revision }
|
59
63
|
end
|
60
64
|
|
61
65
|
# Set a variable to the given value.
|
62
|
-
def set(variable, value)
|
66
|
+
def set(variable, value=nil, &block)
|
67
|
+
# if the variable is uppercase, then we add it as a constant to the
|
68
|
+
# actor. This is to allow uppercase "variables" to be set and referenced
|
69
|
+
# in recipes.
|
70
|
+
if variable.to_s[0].between?(?A, ?Z)
|
71
|
+
klass = @actor.metaclass
|
72
|
+
klass.send(:remove_const, variable) if klass.const_defined?(variable)
|
73
|
+
klass.const_set(variable, value)
|
74
|
+
end
|
75
|
+
|
76
|
+
value = block if value.nil? && block_given?
|
63
77
|
@variables[variable] = value
|
64
78
|
end
|
65
79
|
|
@@ -103,11 +117,19 @@ module SwitchTower
|
|
103
117
|
#
|
104
118
|
# load(:string => "set :scm, :subversion"):
|
105
119
|
# Load the given string as a configuration specification.
|
106
|
-
|
120
|
+
#
|
121
|
+
# load { ... }
|
122
|
+
# Load the block in the context of the configuration.
|
123
|
+
def load(*args, &block)
|
107
124
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
108
125
|
args.each { |arg| load options.merge(:file => arg) }
|
126
|
+
return unless args.empty?
|
127
|
+
|
128
|
+
if block
|
129
|
+
raise "loading a block requires 0 parameters" unless args.empty?
|
130
|
+
load(options.merge(:proc => block))
|
109
131
|
|
110
|
-
|
132
|
+
elsif options[:file]
|
111
133
|
file = options[:file]
|
112
134
|
unless file[0] == ?/
|
113
135
|
load_paths.each do |path|
|
@@ -122,9 +144,17 @@ module SwitchTower
|
|
122
144
|
end
|
123
145
|
|
124
146
|
load :string => File.read(file), :name => options[:name] || file
|
147
|
+
|
125
148
|
elsif options[:string]
|
126
|
-
logger.
|
127
|
-
instance_eval
|
149
|
+
logger.trace "loading configuration #{options[:name] || "<eval>"}"
|
150
|
+
instance_eval(options[:string], options[:name] || "<eval>")
|
151
|
+
|
152
|
+
elsif options[:proc]
|
153
|
+
logger.trace "loading configuration #{options[:proc].inspect}"
|
154
|
+
instance_eval(&options[:proc])
|
155
|
+
|
156
|
+
else
|
157
|
+
raise ArgumentError, "don't know how to load #{options.inspect}"
|
128
158
|
end
|
129
159
|
end
|
130
160
|
|
@@ -164,6 +194,18 @@ module SwitchTower
|
|
164
194
|
actor.define_task(name, options, &block)
|
165
195
|
end
|
166
196
|
|
197
|
+
# Require another file. This is identical to the standard require method,
|
198
|
+
# with the exception that it sets the reciever as the "current" configuration
|
199
|
+
# so that third-party task bundles can include themselves relative to
|
200
|
+
# that configuration.
|
201
|
+
def require(*args) #:nodoc:
|
202
|
+
original, SwitchTower.configuration = SwitchTower.configuration, self
|
203
|
+
super
|
204
|
+
ensure
|
205
|
+
# restore the original, so that require's can be nested
|
206
|
+
SwitchTower.configuration = original
|
207
|
+
end
|
208
|
+
|
167
209
|
# Return the path into which releases should be deployed.
|
168
210
|
def releases_path
|
169
211
|
File.join(deploy_to, version_dir)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'switchtower/actor'
|
2
|
+
|
3
|
+
module SwitchTower
|
4
|
+
class ExtensionProxy
|
5
|
+
def initialize(actor, mod)
|
6
|
+
@actor = actor
|
7
|
+
extend(mod)
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(sym, *args, &block)
|
11
|
+
@actor.send(sym, *args, &block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
EXTENSIONS = {}
|
16
|
+
|
17
|
+
def self.plugin(name, mod)
|
18
|
+
return false if EXTENSIONS.has_key?(name)
|
19
|
+
|
20
|
+
SwitchTower::Actor.class_eval <<-STR, __FILE__, __LINE__+1
|
21
|
+
def #{name}
|
22
|
+
@__#{name}_proxy ||= SwitchTower::ExtensionProxy.new(self, SwitchTower::EXTENSIONS[#{name.inspect}])
|
23
|
+
end
|
24
|
+
STR
|
25
|
+
|
26
|
+
EXTENSIONS[name] = mod
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.remove_plugin(name)
|
31
|
+
if EXTENSIONS.delete(name)
|
32
|
+
SwitchTower::Actor.send(:remove_method, name)
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
end
|
data/lib/switchtower/gateway.rb
CHANGED
@@ -25,11 +25,14 @@ module SwitchTower
|
|
25
25
|
# The Net::SSH session representing the gateway connection.
|
26
26
|
attr_reader :session
|
27
27
|
|
28
|
+
MAX_PORT = 65535
|
29
|
+
MIN_PORT = 1024
|
30
|
+
|
28
31
|
def initialize(server, config) #:nodoc:
|
29
32
|
@config = config
|
30
33
|
@pending_forward_requests = {}
|
31
34
|
@mutex = Mutex.new
|
32
|
-
@next_port =
|
35
|
+
@next_port = MAX_PORT
|
33
36
|
@terminate_thread = false
|
34
37
|
|
35
38
|
waiter = ConditionVariable.new
|
@@ -79,6 +82,13 @@ module SwitchTower
|
|
79
82
|
|
80
83
|
private
|
81
84
|
|
85
|
+
def next_port
|
86
|
+
port = @next_port
|
87
|
+
@next_port -= 1
|
88
|
+
@next_port = MAX_PORT if @next_port < MIN_PORT
|
89
|
+
port
|
90
|
+
end
|
91
|
+
|
82
92
|
def process_next_pending_connection_request
|
83
93
|
@mutex.synchronize do
|
84
94
|
key = @pending_forward_requests.keys.detect { |k| ConditionVariable === @pending_forward_requests[k] } or return
|
@@ -86,14 +96,16 @@ module SwitchTower
|
|
86
96
|
|
87
97
|
@config.logger.trace "establishing connection to #{key} via gateway"
|
88
98
|
|
89
|
-
port =
|
90
|
-
@next_port += 1
|
99
|
+
port = next_port
|
91
100
|
|
92
101
|
begin
|
93
102
|
@session.forward.local(port, key, 22)
|
94
103
|
@pending_forward_requests[key] = SSH.connect('127.0.0.1', @config,
|
95
104
|
port)
|
96
105
|
@config.logger.trace "connection to #{key} via gateway established"
|
106
|
+
rescue Errno::EADDRINUSE
|
107
|
+
port = next_port
|
108
|
+
retry
|
97
109
|
rescue Object
|
98
110
|
@pending_forward_requests[key] = nil
|
99
111
|
raise
|
@@ -10,7 +10,11 @@ def switchtower_invoke(*actions)
|
|
10
10
|
# no rubygems to load, so we fail silently
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
options = actions.last.is_a?(Hash) ? actions.pop : {}
|
14
|
+
|
15
|
+
args = %w[-r config/deploy]
|
16
|
+
verbose = options[:verbose] || "-vvvvv"
|
17
|
+
args << verbose
|
14
18
|
|
15
19
|
args = %w[-vvvvv -r config/<%= recipe_file %>]
|
16
20
|
args.concat(actions.map { |act| ["-a", act.to_s] }.flatten)
|
@@ -34,7 +38,7 @@ end
|
|
34
38
|
|
35
39
|
desc "Enumerate all available deployment tasks"
|
36
40
|
task :show_deploy_tasks do
|
37
|
-
switchtower_invoke :show_tasks
|
41
|
+
switchtower_invoke :show_tasks, :verbose => ""
|
38
42
|
end
|
39
43
|
|
40
44
|
desc "Execute a specific action using switchtower"
|
data/lib/switchtower/logger.rb
CHANGED
@@ -6,6 +6,8 @@ module SwitchTower
|
|
6
6
|
INFO = 1
|
7
7
|
DEBUG = 2
|
8
8
|
TRACE = 3
|
9
|
+
|
10
|
+
MAX_LEVEL = 3
|
9
11
|
|
10
12
|
def initialize(options={})
|
11
13
|
output = options[:output] || STDERR
|
@@ -27,12 +29,13 @@ module SwitchTower
|
|
27
29
|
|
28
30
|
def log(level, message, line_prefix=nil)
|
29
31
|
if level <= self.level
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)]
|
33
|
+
message.split(/\r?\n/).each do |line|
|
34
|
+
if line_prefix
|
35
|
+
@device.print "#{indent} [#{line_prefix}] #{line.strip}\n"
|
36
|
+
else
|
37
|
+
@device.puts "#{indent} #{line.strip}\n"
|
33
38
|
end
|
34
|
-
else
|
35
|
-
@device.puts message.strip
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
@@ -11,10 +11,15 @@
|
|
11
11
|
|
12
12
|
set :rake, "rake"
|
13
13
|
|
14
|
+
set :rails_env, :production
|
15
|
+
|
14
16
|
set :migrate_target, :current
|
15
17
|
set :migrate_env, ""
|
16
18
|
|
17
|
-
set :
|
19
|
+
set :use_sudo, true
|
20
|
+
set(:run_method) { use_sudo ? :sudo : :run }
|
21
|
+
|
22
|
+
set :spinner_user, :app
|
18
23
|
|
19
24
|
desc "Enumerate and describe every available task."
|
20
25
|
task :show_tasks do
|
@@ -98,12 +103,12 @@ task :symlink, :roles => [:app, :db, :web] do
|
|
98
103
|
end
|
99
104
|
|
100
105
|
desc <<-DESC
|
101
|
-
Restart the FCGI processes on the app server. This uses the :
|
102
|
-
variable to determine whether to use sudo or not. By default, :
|
103
|
-
set to
|
106
|
+
Restart the FCGI processes on the app server. This uses the :use_sudo
|
107
|
+
variable to determine whether to use sudo or not. By default, :use_sudo is
|
108
|
+
set to true, but you can set it to false if you are in a shared environment.
|
104
109
|
DESC
|
105
110
|
task :restart, :roles => :app do
|
106
|
-
send(
|
111
|
+
send(run_method, "#{current_path}/script/process/reaper")
|
107
112
|
end
|
108
113
|
|
109
114
|
desc <<-DESC
|
@@ -126,7 +131,7 @@ task :migrate, :roles => :db, :only => { :primary => true } do
|
|
126
131
|
end
|
127
132
|
|
128
133
|
run "cd #{directory} && " +
|
129
|
-
"#{rake} RAILS_ENV
|
134
|
+
"#{rake} RAILS_ENV=#{rails_env} #{migrate_env} migrate"
|
130
135
|
end
|
131
136
|
|
132
137
|
desc <<-DESC
|
@@ -189,7 +194,8 @@ end
|
|
189
194
|
desc <<-DESC
|
190
195
|
Removes unused releases from the releases directory. By default, the last 5
|
191
196
|
releases are retained, but this can be configured with the 'keep_releases'
|
192
|
-
variable.
|
197
|
+
variable. This will use sudo to do the delete by default, but you can specify
|
198
|
+
that run should be used by setting the :use_sudo variable to false.
|
193
199
|
DESC
|
194
200
|
task :cleanup do
|
195
201
|
count = (self[:keep_releases] || 5).to_i
|
@@ -197,9 +203,42 @@ task :cleanup do
|
|
197
203
|
logger.important "no old releases to clean up"
|
198
204
|
else
|
199
205
|
logger.info "keeping #{count} of #{releases.length} deployed releases"
|
200
|
-
|
206
|
+
directories = (releases - releases.last(count)).map { |release|
|
201
207
|
File.join(releases_path, release) }.join(" ")
|
202
208
|
|
203
|
-
|
209
|
+
send(run_method, "rm -rf #{directories}")
|
204
210
|
end
|
205
|
-
end
|
211
|
+
end
|
212
|
+
|
213
|
+
desc <<-DESC
|
214
|
+
Start the spinner daemon for the application (requires script/spin). This will
|
215
|
+
use sudo to start the spinner by default, unless :use_sudo is false. If using
|
216
|
+
sudo, you can specify the user that the spinner ought to run as by setting the
|
217
|
+
:spinner_user variable (defaults to :app).
|
218
|
+
DESC
|
219
|
+
task :spinner, :roles => :app do
|
220
|
+
user = (use_sudo && spinner_user) ? "-u #{spinner_user} " : ""
|
221
|
+
send(run_method, "#{user}#{current_path}/script/spin")
|
222
|
+
end
|
223
|
+
|
224
|
+
desc <<-DESC
|
225
|
+
Used only for deploying when the spinner isn't running. It invokes deploy,
|
226
|
+
and when it finishes it then invokes the spinner task (to start the spinner).
|
227
|
+
DESC
|
228
|
+
task :cold_deploy do
|
229
|
+
deploy
|
230
|
+
spinner
|
231
|
+
end
|
232
|
+
|
233
|
+
desc <<-DESC
|
234
|
+
A simple task for performing one-off commands that may not require a full task
|
235
|
+
to be written for them. Simply specify the command to execute via the COMMAND
|
236
|
+
environment variable. To execute the command only on certain roles, specify
|
237
|
+
the ROLES environment variable as a comma-delimited list of role names. Lastly,
|
238
|
+
if you want to execute the command via sudo, specify a non-empty value for the
|
239
|
+
SUDO environment variable.
|
240
|
+
DESC
|
241
|
+
task :invoke, :roles => SwitchTower.str2roles(ENV["ROLES"] || "") do
|
242
|
+
method = ENV["SUDO"] ? :sudo : :run
|
243
|
+
send(method, ENV["COMMAND"])
|
244
|
+
end
|