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.
@@ -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(configuration)
81
+ def servers
75
82
  unless @servers
76
- roles = [*(@options[:roles] || configuration.roles.keys)].map { |name| configuration.roles[name] or raise ArgumentError, "task #{self.name.inspect} references non-existant role #{name.inspect}" }.flatten
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.trace "executing task #{name}"
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 ||= Proc.new do |ch, stream, out|
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 ||= Proc.new do |ch, stream, out|
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
- private
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
- def metaclass
326
- class << self; self; end
327
- end
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
- servers = tasks[task_call_frames.last.name].servers(configuration)
362
- servers = servers.first if options[:once]
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
@@ -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 :password, Proc.new { SwitchTower::CLI.password_prompt }
89
+ # config.set(:password) { SwitchTower::CLI.password_prompt }
90
90
  # config.load "standard", "config/deploy"
91
91
  # config.actor.update_code
92
92
  #
@@ -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 :deploy_to, Proc.new { "/u/apps/#{application}" }
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 :revision, Proc.new { source.latest_revision }
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
- def load(*args)
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
- if options[:file]
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.debug "loading configuration #{options[:name] || "<eval>"}"
127
- instance_eval options[:string], options[:name] || "<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
@@ -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 = 31310
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 = @next_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
- require 'switchtower/cli'
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"
@@ -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
- if line_prefix
31
- message.split(/\r?\n/).each do |line|
32
- @device.print "[#{line_prefix}] #{line.strip}\n"
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 :restart_via, :sudo
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 :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.
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(restart_via, "#{current_path}/script/process/reaper")
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=production #{migrate_env} migrate"
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
- keepers = (releases - releases.last(count)).map { |release|
206
+ directories = (releases - releases.last(count)).map { |release|
201
207
  File.join(releases_path, release) }.join(" ")
202
208
 
203
- sudo "rm -rf #{keepers}"
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