switchtower 0.9.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/bin/switchtower +11 -0
- data/examples/sample.rb +113 -0
- data/lib/switchtower.rb +1 -0
- data/lib/switchtower/actor.rb +350 -0
- data/lib/switchtower/cli.rb +220 -0
- data/lib/switchtower/command.rb +85 -0
- data/lib/switchtower/configuration.rb +193 -0
- data/lib/switchtower/gateway.rb +106 -0
- data/lib/switchtower/generators/rails/deployment/deployment_generator.rb +25 -0
- data/lib/switchtower/generators/rails/deployment/templates/deploy.rb +116 -0
- data/lib/switchtower/generators/rails/deployment/templates/switchtower.rake +33 -0
- data/lib/switchtower/generators/rails/loader.rb +20 -0
- data/lib/switchtower/logger.rb +56 -0
- data/lib/switchtower/recipes/standard.rb +175 -0
- data/lib/switchtower/recipes/templates/maintenance.rhtml +53 -0
- data/lib/switchtower/scm/base.rb +43 -0
- data/lib/switchtower/scm/cvs.rb +73 -0
- data/lib/switchtower/scm/darcs.rb +27 -0
- data/lib/switchtower/scm/subversion.rb +104 -0
- data/lib/switchtower/ssh.rb +30 -0
- data/lib/switchtower/version.rb +9 -0
- data/test/actor_test.rb +261 -0
- data/test/command_test.rb +43 -0
- data/test/configuration_test.rb +210 -0
- data/test/fixtures/config.rb +5 -0
- data/test/scm/cvs_test.rb +164 -0
- data/test/scm/subversion_test.rb +100 -0
- data/test/ssh_test.rb +104 -0
- data/test/utils.rb +41 -0
- metadata +88 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'switchtower'
|
|
3
|
+
|
|
4
|
+
module SwitchTower
|
|
5
|
+
class CLI
|
|
6
|
+
def self.execute!
|
|
7
|
+
new.execute!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
if !defined?(USE_TERMIOS) || USE_TERMIOS
|
|
12
|
+
require 'termios'
|
|
13
|
+
else
|
|
14
|
+
raise LoadError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Enable or disable stdin echoing to the terminal.
|
|
18
|
+
def echo(enable)
|
|
19
|
+
term = Termios::getattr(STDIN)
|
|
20
|
+
|
|
21
|
+
if enable
|
|
22
|
+
term.c_lflag |= (Termios::ECHO | Termios::ICANON)
|
|
23
|
+
else
|
|
24
|
+
term.c_lflag &= ~Termios::ECHO
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Termios::setattr(STDIN, Termios::TCSANOW, term)
|
|
28
|
+
end
|
|
29
|
+
rescue LoadError
|
|
30
|
+
def echo(enable)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :options
|
|
35
|
+
attr_reader :args
|
|
36
|
+
|
|
37
|
+
def initialize(args = ARGV)
|
|
38
|
+
@args = args
|
|
39
|
+
@options = { :verbose => 0, :recipes => [], :actions => [], :vars => {},
|
|
40
|
+
:pre_vars => {} }
|
|
41
|
+
|
|
42
|
+
OptionParser.new do |opts|
|
|
43
|
+
opts.banner = "Usage: #{$0} [options] [args]"
|
|
44
|
+
|
|
45
|
+
opts.separator ""
|
|
46
|
+
opts.separator "Recipe Options -----------------------"
|
|
47
|
+
opts.separator ""
|
|
48
|
+
|
|
49
|
+
opts.on("-a", "--action ACTION",
|
|
50
|
+
"An action to execute. Multiple actions may",
|
|
51
|
+
"be specified, and are loaded in the given order."
|
|
52
|
+
) { |value| @options[:actions] << value }
|
|
53
|
+
|
|
54
|
+
opts.on("-p", "--password [PASSWORD]",
|
|
55
|
+
"The password to use when connecting. If the switch",
|
|
56
|
+
"is given without a password, the password will be",
|
|
57
|
+
"prompted for immediately. (Default: prompt for password",
|
|
58
|
+
"the first time it is needed.)"
|
|
59
|
+
) { |value| @options[:password] = value }
|
|
60
|
+
|
|
61
|
+
opts.on("-r", "--recipe RECIPE",
|
|
62
|
+
"A recipe file to load. Multiple recipes may",
|
|
63
|
+
"be specified, and are loaded in the given order."
|
|
64
|
+
) { |value| @options[:recipes] << value }
|
|
65
|
+
|
|
66
|
+
opts.on("-s", "--set NAME=VALUE",
|
|
67
|
+
"Specify a variable and it's value to set. This",
|
|
68
|
+
"will be set after loading all recipe files."
|
|
69
|
+
) do |pair|
|
|
70
|
+
name, value = pair.split(/=/, 2)
|
|
71
|
+
@options[:vars][name.to_sym] = value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opts.on("-S", "--set-before NAME=VALUE",
|
|
75
|
+
"Specify a variable and it's value to set. This",
|
|
76
|
+
"will be set BEFORE loading all recipe files."
|
|
77
|
+
) do |pair|
|
|
78
|
+
name, value = pair.split(/=/, 2)
|
|
79
|
+
@options[:pre_vars][name.to_sym] = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
opts.separator ""
|
|
83
|
+
opts.separator "Framework Integration Options --------"
|
|
84
|
+
opts.separator ""
|
|
85
|
+
|
|
86
|
+
opts.on("-A", "--apply-to DIRECTORY",
|
|
87
|
+
"Create a minimal set of scripts and recipes to use",
|
|
88
|
+
"switchtower with the application at the given",
|
|
89
|
+
"directory. (Currently only works with Rails apps.)"
|
|
90
|
+
) { |value| @options[:apply_to] = value }
|
|
91
|
+
|
|
92
|
+
opts.separator ""
|
|
93
|
+
opts.separator "Miscellaneous Options ----------------"
|
|
94
|
+
opts.separator ""
|
|
95
|
+
|
|
96
|
+
opts.on("-h", "--help", "Display this help message") do
|
|
97
|
+
puts opts
|
|
98
|
+
exit
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
opts.on("-P", "--[no-]pretend",
|
|
102
|
+
"Run the task(s), but don't actually connect to or",
|
|
103
|
+
"execute anything on the servers. (For various reasons",
|
|
104
|
+
"this will not necessarily be an accurate depiction",
|
|
105
|
+
"of the work that will actually be performed.",
|
|
106
|
+
"Default: don't pretend.)"
|
|
107
|
+
) { |value| @options[:pretend] = value }
|
|
108
|
+
|
|
109
|
+
opts.on("-v", "--verbose",
|
|
110
|
+
"Specify the verbosity of the output.",
|
|
111
|
+
"May be given multiple times. (Default: silent)"
|
|
112
|
+
) { @options[:verbose] += 1 }
|
|
113
|
+
|
|
114
|
+
opts.on("-V", "--version",
|
|
115
|
+
"Display the version info for this utility"
|
|
116
|
+
) do
|
|
117
|
+
require 'switchtower/version'
|
|
118
|
+
puts "SwitchTower v#{SwitchTower::Version::STRING}"
|
|
119
|
+
exit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts.separator ""
|
|
123
|
+
opts.separator <<DETAIL.split(/\n/)
|
|
124
|
+
You can use the --apply-to switch to generate a minimal set of switchtower
|
|
125
|
+
scripts and recipes for an application. Just specify the path to the application
|
|
126
|
+
as the argument to --apply-to, like this:
|
|
127
|
+
|
|
128
|
+
switchtower --apply-to ~/projects/myapp
|
|
129
|
+
|
|
130
|
+
You'll wind up with a sample deployment recipe in config/deploy.rb, some new
|
|
131
|
+
rake tasks in config/tasks, and a switchtower script in your script directory.
|
|
132
|
+
|
|
133
|
+
(Currently, --apply-to only works with Rails applications.)
|
|
134
|
+
DETAIL
|
|
135
|
+
|
|
136
|
+
if args.empty?
|
|
137
|
+
puts opts
|
|
138
|
+
exit
|
|
139
|
+
else
|
|
140
|
+
opts.parse!(args)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
check_options!
|
|
145
|
+
|
|
146
|
+
password_proc = Proc.new do
|
|
147
|
+
sync = STDOUT.sync
|
|
148
|
+
begin
|
|
149
|
+
echo false
|
|
150
|
+
STDOUT.sync = true
|
|
151
|
+
print "Password: "
|
|
152
|
+
STDIN.gets.chomp
|
|
153
|
+
ensure
|
|
154
|
+
echo true
|
|
155
|
+
STDOUT.sync = sync
|
|
156
|
+
puts
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if !@options.has_key?(:password)
|
|
161
|
+
@options[:password] = password_proc
|
|
162
|
+
elsif !@options[:password]
|
|
163
|
+
@options[:password] = password_proc.call
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def execute!
|
|
168
|
+
if !@options[:recipes].empty?
|
|
169
|
+
execute_recipes!
|
|
170
|
+
elsif @options[:apply_to]
|
|
171
|
+
execute_apply_to!
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def execute_recipes!
|
|
178
|
+
config = SwitchTower::Configuration.new
|
|
179
|
+
config.logger.level = options[:verbose]
|
|
180
|
+
config.set :password, options[:password]
|
|
181
|
+
config.set :pretend, options[:pretend]
|
|
182
|
+
|
|
183
|
+
options[:pre_vars].each { |name, value| config.set(name, value) }
|
|
184
|
+
|
|
185
|
+
# load the standard recipe definition
|
|
186
|
+
config.load "standard"
|
|
187
|
+
|
|
188
|
+
options[:recipes].each { |recipe| config.load(recipe) }
|
|
189
|
+
options[:vars].each { |name, value| config.set(name, value) }
|
|
190
|
+
|
|
191
|
+
actor = config.actor
|
|
192
|
+
options[:actions].each { |action| actor.send action }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def execute_apply_to!
|
|
196
|
+
require 'switchtower/generators/rails/loader'
|
|
197
|
+
Generators::RailsLoader.load! @options
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
APPLY_TO_OPTIONS = [:apply_to]
|
|
201
|
+
RECIPE_OPTIONS = [:password]
|
|
202
|
+
|
|
203
|
+
def check_options!
|
|
204
|
+
apply_to_given = !(@options.keys & APPLY_TO_OPTIONS).empty?
|
|
205
|
+
recipe_given = !(@options.keys & RECIPE_OPTIONS).empty? ||
|
|
206
|
+
!@options[:recipes].empty? ||
|
|
207
|
+
!@options[:actions].empty?
|
|
208
|
+
|
|
209
|
+
if apply_to_given && recipe_given
|
|
210
|
+
abort "You cannot specify both recipe options and framework integration options."
|
|
211
|
+
elsif !apply_to_given
|
|
212
|
+
abort "You must specify at least one recipe" if @options[:recipes].empty?
|
|
213
|
+
abort "You must specify at least one action" if @options[:actions].empty?
|
|
214
|
+
else
|
|
215
|
+
@options[:application] = args.shift
|
|
216
|
+
@options[:recipe_file] = args.shift
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module SwitchTower
|
|
2
|
+
|
|
3
|
+
# This class encapsulates a single command to be executed on a set of remote
|
|
4
|
+
# machines, in parallel.
|
|
5
|
+
class Command
|
|
6
|
+
attr_reader :servers, :command, :options, :actor
|
|
7
|
+
|
|
8
|
+
def initialize(servers, command, callback, options, actor) #:nodoc:
|
|
9
|
+
@servers = servers
|
|
10
|
+
@command = command.strip.gsub(/\r?\n/, "\\\n")
|
|
11
|
+
@callback = callback
|
|
12
|
+
@options = options
|
|
13
|
+
@actor = actor
|
|
14
|
+
@channels = open_channels
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def logger #:nodoc:
|
|
18
|
+
actor.logger
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Processes the command in parallel on all specified hosts. If the command
|
|
22
|
+
# fails (non-zero return code) on any of the hosts, this will raise a
|
|
23
|
+
# RuntimeError.
|
|
24
|
+
def process!
|
|
25
|
+
logger.debug "processing command"
|
|
26
|
+
|
|
27
|
+
loop do
|
|
28
|
+
active = 0
|
|
29
|
+
@channels.each do |ch|
|
|
30
|
+
next if ch[:closed]
|
|
31
|
+
active += 1
|
|
32
|
+
ch.connection.process(true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
break if active == 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
logger.trace "command finished"
|
|
39
|
+
|
|
40
|
+
if failed = @channels.detect { |ch| ch[:status] != 0 }
|
|
41
|
+
raise "command #{@command.inspect} failed on #{failed[:host]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def open_channels
|
|
50
|
+
@servers.map do |server|
|
|
51
|
+
@actor.sessions[server].open_channel do |channel|
|
|
52
|
+
channel[:host] = server
|
|
53
|
+
channel.request_pty :want_reply => true
|
|
54
|
+
|
|
55
|
+
channel.on_success do |ch|
|
|
56
|
+
logger.trace "executing command", ch[:host]
|
|
57
|
+
ch.exec command
|
|
58
|
+
ch.send_data options[:data] if options[:data]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
channel.on_failure do |ch|
|
|
62
|
+
logger.important "could not open channel", ch[:host]
|
|
63
|
+
ch.close
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
channel.on_data do |ch, data|
|
|
67
|
+
@callback[ch, :out, data] if @callback
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
channel.on_extended_data do |ch, type, data|
|
|
71
|
+
@callback[ch, :err, data] if @callback
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
channel.on_request do |ch, request, reply, data|
|
|
75
|
+
ch[:status] = data.read_long if request == "exit-status"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
channel.on_close do |ch|
|
|
79
|
+
ch[:closed] = true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
require 'switchtower/actor'
|
|
2
|
+
require 'switchtower/logger'
|
|
3
|
+
require 'switchtower/scm/subversion'
|
|
4
|
+
|
|
5
|
+
module SwitchTower
|
|
6
|
+
|
|
7
|
+
# Represents a specific SwitchTower configuration. A Configuration instance
|
|
8
|
+
# may be used to load multiple recipe files, define and describe tasks,
|
|
9
|
+
# define roles, create an actor, and set configuration variables.
|
|
10
|
+
class Configuration
|
|
11
|
+
Role = Struct.new(:host, :options)
|
|
12
|
+
|
|
13
|
+
DEFAULT_VERSION_DIR_NAME = "releases" #:nodoc:
|
|
14
|
+
DEFAULT_CURRENT_DIR_NAME = "current" #:nodoc:
|
|
15
|
+
DEFAULT_SHARED_DIR_NAME = "shared" #:nodoc:
|
|
16
|
+
|
|
17
|
+
# The actor created for this configuration instance.
|
|
18
|
+
attr_reader :actor
|
|
19
|
+
|
|
20
|
+
# The list of Role instances defined for this configuration.
|
|
21
|
+
attr_reader :roles
|
|
22
|
+
|
|
23
|
+
# The logger instance defined for this configuration.
|
|
24
|
+
attr_reader :logger
|
|
25
|
+
|
|
26
|
+
# The load paths used for locating recipe files.
|
|
27
|
+
attr_reader :load_paths
|
|
28
|
+
|
|
29
|
+
# The time (in UTC) at which this configuration was created, used for
|
|
30
|
+
# determining the release path.
|
|
31
|
+
attr_reader :now
|
|
32
|
+
|
|
33
|
+
def initialize(actor_class=Actor) #:nodoc:
|
|
34
|
+
@roles = Hash.new { |h,k| h[k] = [] }
|
|
35
|
+
@actor = actor_class.new(self)
|
|
36
|
+
@logger = Logger.new
|
|
37
|
+
@load_paths = [".", File.join(File.dirname(__FILE__), "recipes")]
|
|
38
|
+
@variables = {}
|
|
39
|
+
@now = Time.now.utc
|
|
40
|
+
|
|
41
|
+
set :application, nil
|
|
42
|
+
set :repository, nil
|
|
43
|
+
set :gateway, nil
|
|
44
|
+
set :user, nil
|
|
45
|
+
set :password, nil
|
|
46
|
+
|
|
47
|
+
set :deploy_to, Proc.new { "/u/apps/#{application}" }
|
|
48
|
+
|
|
49
|
+
set :version_dir, DEFAULT_VERSION_DIR_NAME
|
|
50
|
+
set :current_dir, DEFAULT_CURRENT_DIR_NAME
|
|
51
|
+
set :shared_dir, DEFAULT_SHARED_DIR_NAME
|
|
52
|
+
set :scm, :subversion
|
|
53
|
+
|
|
54
|
+
set :revision, Proc.new { source.latest_revision }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Set a variable to the given value.
|
|
58
|
+
def set(variable, value)
|
|
59
|
+
@variables[variable] = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias :[]= :set
|
|
63
|
+
|
|
64
|
+
# Access a named variable. If the value of the variable is a Proc instance,
|
|
65
|
+
# the proc will be invoked and the return value cached and returned.
|
|
66
|
+
def [](variable)
|
|
67
|
+
set variable, @variables[variable].call if Proc === @variables[variable]
|
|
68
|
+
@variables[variable]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Based on the current value of the <tt>:scm</tt> variable, instantiate and
|
|
72
|
+
# return an SCM module representing the desired source control behavior.
|
|
73
|
+
def source
|
|
74
|
+
@source ||= case scm
|
|
75
|
+
when Class then
|
|
76
|
+
scm.new(self)
|
|
77
|
+
when String, Symbol then
|
|
78
|
+
require "switchtower/scm/#{scm.to_s.downcase}"
|
|
79
|
+
SwitchTower::SCM.const_get(scm.to_s.downcase.capitalize).new(self)
|
|
80
|
+
else
|
|
81
|
+
raise "invalid scm specification: #{scm.inspect}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Load a configuration file or string into this configuration.
|
|
86
|
+
#
|
|
87
|
+
# Usage:
|
|
88
|
+
#
|
|
89
|
+
# load("recipe"):
|
|
90
|
+
# Look for and load the contents of 'recipe.rb' into this
|
|
91
|
+
# configuration.
|
|
92
|
+
#
|
|
93
|
+
# load(:file => "recipe"):
|
|
94
|
+
# same as above
|
|
95
|
+
#
|
|
96
|
+
# load(:string => "set :scm, :subversion"):
|
|
97
|
+
# Load the given string as a configuration specification.
|
|
98
|
+
def load(*args)
|
|
99
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
100
|
+
args.each { |arg| load options.merge(:file => arg) }
|
|
101
|
+
|
|
102
|
+
if options[:file]
|
|
103
|
+
file = options[:file]
|
|
104
|
+
unless file[0] == ?/
|
|
105
|
+
load_paths.each do |path|
|
|
106
|
+
if File.file?(File.join(path, file))
|
|
107
|
+
file = File.join(path, file)
|
|
108
|
+
break
|
|
109
|
+
elsif File.file?(File.join(path, file) + ".rb")
|
|
110
|
+
file = File.join(path, file + ".rb")
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
load :string => File.read(file), :name => options[:name] || file
|
|
117
|
+
elsif options[:string]
|
|
118
|
+
logger.debug "loading configuration #{options[:name] || "<eval>"}"
|
|
119
|
+
instance_eval options[:string], options[:name] || "<eval>"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Define a new role and its associated servers. You must specify at least
|
|
124
|
+
# one host for each role. Also, you can specify additional information
|
|
125
|
+
# (in the form of a Hash) which can be used to more uniquely specify the
|
|
126
|
+
# subset of servers specified by this specific role definition.
|
|
127
|
+
#
|
|
128
|
+
# Usage:
|
|
129
|
+
#
|
|
130
|
+
# role :db, "db1.example.com", "db2.example.com"
|
|
131
|
+
# role :db, "master.example.com", :primary => true
|
|
132
|
+
# role :app, "app1.example.com", "app2.example.com"
|
|
133
|
+
def role(which, *args)
|
|
134
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
135
|
+
raise ArgumentError, "must give at least one host" if args.empty?
|
|
136
|
+
args.each { |host| roles[which] << Role.new(host, options) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Describe the next task to be defined. The given text will be attached to
|
|
140
|
+
# the next task that is defined and used as its description.
|
|
141
|
+
def desc(text)
|
|
142
|
+
@next_description = text
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Define a new task. If a description is active (see #desc), it is added to
|
|
146
|
+
# the options under the <tt>:desc</tt> key. This method ultimately
|
|
147
|
+
# delegates to Actor#define_task.
|
|
148
|
+
def task(name, options={}, &block)
|
|
149
|
+
raise ArgumentError, "expected a block" unless block
|
|
150
|
+
|
|
151
|
+
if @next_description
|
|
152
|
+
options = options.merge(:desc => @next_description)
|
|
153
|
+
@next_description = nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
actor.define_task(name, options, &block)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Return the path into which releases should be deployed.
|
|
160
|
+
def releases_path
|
|
161
|
+
File.join(deploy_to, version_dir)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Return the path identifying the +current+ symlink, used to identify the
|
|
165
|
+
# current release.
|
|
166
|
+
def current_path
|
|
167
|
+
File.join(deploy_to, current_dir)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Return the path into which shared files should be stored.
|
|
171
|
+
def shared_path
|
|
172
|
+
File.join(deploy_to, shared_dir)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Return the full path to the named release. If a release is not specified,
|
|
176
|
+
# +now+ is used (the time at which the configuration was created).
|
|
177
|
+
def release_path(release=now.strftime("%Y%m%d%H%M%S"))
|
|
178
|
+
File.join(releases_path, release)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def respond_to?(sym) #:nodoc:
|
|
182
|
+
@variables.has_key?(sym) || super
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def method_missing(sym, *args, &block) #:nodoc:
|
|
186
|
+
if args.length == 0 && block.nil? && @variables.has_key?(sym)
|
|
187
|
+
self[sym]
|
|
188
|
+
else
|
|
189
|
+
super
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|