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