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.
@@ -0,0 +1,106 @@
1
+ require 'thread'
2
+ require 'switchtower/ssh'
3
+
4
+ Thread.abort_on_exception = true
5
+
6
+ module SwitchTower
7
+
8
+ # Black magic. It uses threads and Net::SSH to set up a connection to a
9
+ # gateway server, through which connections to other servers may be
10
+ # tunnelled.
11
+ #
12
+ # It is used internally by Actor, but may be useful on its own, as well.
13
+ #
14
+ # Usage:
15
+ #
16
+ # config = SwitchTower::Configuration.new
17
+ # gateway = SwitchTower::Gateway.new('gateway.example.com', config)
18
+ #
19
+ # sess1 = gateway.connect_to('hidden.example.com')
20
+ # sess2 = gateway.connect_to('other.example.com')
21
+ class Gateway
22
+ # The thread inside which the gateway connection itself is running.
23
+ attr_reader :thread
24
+
25
+ # The Net::SSH session representing the gateway connection.
26
+ attr_reader :session
27
+
28
+ def initialize(server, config) #:nodoc:
29
+ @config = config
30
+ @pending_forward_requests = {}
31
+ @mutex = Mutex.new
32
+ @next_port = 31310
33
+ @terminate_thread = false
34
+
35
+ waiter = ConditionVariable.new
36
+
37
+ @thread = Thread.new do
38
+ @config.logger.trace "starting connection to gateway #{server}"
39
+ SSH.connect(server, @config) do |@session|
40
+ @config.logger.trace "gateway connection established"
41
+ @mutex.synchronize { waiter.signal }
42
+ connection = @session.registry[:connection][:driver]
43
+ loop do
44
+ break if @terminate_thread
45
+ sleep 0.1 unless connection.reader_ready?
46
+ connection.process true
47
+ Thread.new { process_next_pending_connection_request }
48
+ end
49
+ end
50
+ end
51
+
52
+ @mutex.synchronize { waiter.wait(@mutex) }
53
+ end
54
+
55
+ # Shuts down all forwarded connections and terminates the gateway.
56
+ def shutdown!
57
+ # cancel all active forward channels
58
+ @session.forward.active_locals.each do |lport, host, port|
59
+ @session.forward.cancel_local(lport)
60
+ end
61
+
62
+ # terminate the gateway thread
63
+ @terminate_thread = true
64
+
65
+ # wait for the gateway thread to stop
66
+ @thread.join
67
+ end
68
+
69
+ # Connects to the given server by opening a forwarded port from the local
70
+ # host to the server, via the gateway, and then opens and returns a new
71
+ # Net::SSH connection via that port.
72
+ def connect_to(server)
73
+ @mutex.synchronize do
74
+ @pending_forward_requests[server] = ConditionVariable.new
75
+ @pending_forward_requests[server].wait(@mutex)
76
+ @pending_forward_requests.delete(server)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def process_next_pending_connection_request
83
+ @mutex.synchronize do
84
+ key = @pending_forward_requests.keys.detect { |k| ConditionVariable === @pending_forward_requests[k] } or return
85
+ var = @pending_forward_requests[key]
86
+
87
+ @config.logger.trace "establishing connection to #{key} via gateway"
88
+
89
+ port = @next_port
90
+ @next_port += 1
91
+
92
+ begin
93
+ @session.forward.local(port, key, 22)
94
+ @pending_forward_requests[key] = SSH.connect('127.0.0.1', @config,
95
+ port)
96
+ @config.logger.trace "connection to #{key} via gateway established"
97
+ rescue Object
98
+ @pending_forward_requests[key] = nil
99
+ raise
100
+ ensure
101
+ var.signal
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,25 @@
1
+ class DeploymentGenerator < Rails::Generator::NamedBase
2
+ attr_reader :recipe_file
3
+
4
+ def initialize(runtime_args, runtime_options = {})
5
+ super
6
+ @recipe_file = @args.shift || "deploy"
7
+ end
8
+
9
+ def manifest
10
+ record do |m|
11
+ m.directory "config"
12
+ m.template "deploy.rb", File.join("config", "#{recipe_file}.rb")
13
+ m.directory "lib/tasks"
14
+ m.template "switchtower.rake", File.join("lib", "tasks", "switchtower.rake")
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ # Override with your own usage banner.
21
+ def banner
22
+ "Usage: #{$0} deployment ApplicationName [recipe-name]\n" +
23
+ " (recipe-name defaults to \"deploy\")"
24
+ end
25
+ end
@@ -0,0 +1,116 @@
1
+ # This defines a deployment "recipe" that you can feed to switchtower
2
+ # (http://manuals.rubyonrails.com/read/book/17). It allows you to automate
3
+ # (among other things) the deployment of your application.
4
+
5
+ # =============================================================================
6
+ # REQUIRED VARIABLES
7
+ # =============================================================================
8
+ # You must always specify the application and repository for every recipe. The
9
+ # repository must be the URL of the repository you want this recipe to
10
+ # correspond to. The deploy_to path must be the path on each machine that will
11
+ # form the root of the application path.
12
+
13
+ set :application, "<%= singular_name %>"
14
+ set :repository, "http://svn.yourhost.com/#{application}/trunk"
15
+
16
+ # =============================================================================
17
+ # ROLES
18
+ # =============================================================================
19
+ # You can define any number of roles, each of which contains any number of
20
+ # machines. Roles might include such things as :web, or :app, or :db, defining
21
+ # what the purpose of each machine is. You can also specify options that can
22
+ # be used to single out a specific subset of boxes in a particular role, like
23
+ # :primary => true.
24
+
25
+ role :web, "www01.example.com", "www02.example.com"
26
+ role :app, "app01.example.com", "app02.example.com", "app03.example.com"
27
+ role :db, "db01.example.com", :primary => true
28
+ role :db, "db02.example.com", "db03.example.com"
29
+
30
+ # =============================================================================
31
+ # OPTIONAL VARIABLES
32
+ # =============================================================================
33
+ # set :deploy_to, "/path/to/app" # defaults to "/u/apps/#{application}"
34
+ # set :user, "flippy" # defaults to the currently logged in user
35
+ # set :scm, :darcs # defaults to :subversion
36
+ # set :svn, "/path/to/svn" # defaults to searching the PATH
37
+ # set :darcs, "/path/to/darcs" # defaults to searching the PATH
38
+ # set :cvs, "/path/to/cvs" # defaults to searching the PATH
39
+ # set :gateway, "gate.host.com" # default to no gateway
40
+
41
+ # =============================================================================
42
+ # TASKS
43
+ # =============================================================================
44
+ # Define tasks that run on all (or only some) of the machines. You can specify
45
+ # a role (or set of roles) that each task should be executed on. You can also
46
+ # narrow the set of servers to a subset of a role by specifying options, which
47
+ # must match the options given for the servers to select (like :primary => true)
48
+
49
+ desc <<DESC
50
+ An imaginary backup task. (Execute the 'show_tasks' task to display all
51
+ available tasks.)
52
+ DESC
53
+ task :backup, :roles => :db, :only => { :primary => true } do
54
+ # the on_rollback handler is only executed if this task is executed within
55
+ # a transaction (see below), AND it or a subsequent task fails.
56
+ on_rollback { delete "/tmp/dump.sql" }
57
+
58
+ run "mysqldump -u theuser -p thedatabase > /tmp/dump.sql" do |ch, stream, out|
59
+ ch.send_data "thepassword\n" if out =~ /^Enter password:/
60
+ end
61
+ end
62
+
63
+ # Tasks may take advantage of several different helper methods to interact
64
+ # with the remote server(s). These are:
65
+ #
66
+ # * run(command, options={}, &block): execute the given command on all servers
67
+ # associated with the current task, in parallel. The block, if given, should
68
+ # accept three parameters: the communication channel, a symbol identifying the
69
+ # type of stream (:err or :out), and the data. The block is invoked for all
70
+ # output from the command, allowing you to inspect output and act
71
+ # accordingly.
72
+ # * sudo(command, options={}, &block): same as run, but it executes the command
73
+ # via sudo.
74
+ # * delete(path, options={}): deletes the given file or directory from all
75
+ # associated servers. If :recursive => true is given in the options, the
76
+ # delete uses "rm -rf" instead of "rm -f".
77
+ # * put(buffer, path, options={}): creates or overwrites a file at "path" on
78
+ # all associated servers, populating it with the contents of "buffer". You
79
+ # can specify :mode as an integer value, which will be used to set the mode
80
+ # on the file.
81
+ # * render(template, options={}) or render(options={}): renders the given
82
+ # template and returns a string. Alternatively, if the :template key is given,
83
+ # it will be treated as the contents of the template to render. Any other keys
84
+ # are treated as local variables, which are made available to the (ERb)
85
+ # template.
86
+
87
+ desc "Demonstrates the various helper methods available to recipes."
88
+ task :helper_demo do
89
+ # "setup" is a standard task which sets up the directory structure on the
90
+ # remote servers. It is a good idea to run the "setup" task at least once
91
+ # at the beginning of your app's lifetime (it is non-destructive).
92
+ setup
93
+
94
+ buffer = render("maintenance.rhtml", :deadline => ENV['UNTIL'])
95
+ put buffer, "#{shared_path}/system/maintenance.html", :mode => 0644
96
+ sudo "killall -USR1 dispatch.fcgi"
97
+ run "#{release_path}/script/spin"
98
+ delete "#{shared_path}/system/maintenance.html"
99
+ end
100
+
101
+ # You can use "transaction" to indicate that if any of the tasks within it fail,
102
+ # all should be rolled back (for each task that specifies an on_rollback
103
+ # handler).
104
+
105
+ desc "A task demonstrating the use of transactions."
106
+ task :long_deploy do
107
+ transaction do
108
+ update_code
109
+ disable_web
110
+ symlink
111
+ migrate
112
+ end
113
+
114
+ restart
115
+ enable_web
116
+ end
@@ -0,0 +1,33 @@
1
+ # =============================================================================
2
+ # A set of rake tasks for invoking the SwitchTower automation utility.
3
+ # =============================================================================
4
+
5
+ desc "Push the latest revision into production using the release manager"
6
+ task :deploy do
7
+ system "switchtower -vvvv -r config/<%= recipe_file %> -a deploy"
8
+ end
9
+
10
+ desc "Rollback to the release before the current release in production"
11
+ task :rollback do
12
+ system "switchtower -vvvv -r config/<%= recipe_file %> -a rollback"
13
+ end
14
+
15
+ desc "Describe the differences between HEAD and the last production release"
16
+ task :diff_from_last_deploy do
17
+ system "switchtower -vvvv -r config/<%= recipe_file %> -a diff_from_last_deploy"
18
+ end
19
+
20
+ desc "Enumerate all available deployment tasks"
21
+ task :show_deploy_tasks do
22
+ system "switchtower -r config/<%= recipe_file %> -a show_tasks"
23
+ end
24
+
25
+ desc "Execute a specific action using the release manager"
26
+ task :remote_exec do
27
+ unless ENV['ACTION']
28
+ raise "Please specify an action (or comma separated list of actions) via the ACTION environment variable"
29
+ end
30
+
31
+ actions = ENV['ACTION'].split(",").map { |a| "-a #{a}" }.join(" ")
32
+ system "switchtower -vvvv -r config/<%= recipe_file %> #{actions}"
33
+ end
@@ -0,0 +1,20 @@
1
+ module SwitchTower
2
+ module Generators
3
+ class RailsLoader
4
+ def self.load!(options)
5
+ require "#{options[:apply_to]}/config/environment"
6
+ require "rails_generator"
7
+ require "rails_generator/scripts/generate"
8
+
9
+ Rails::Generator::Base.sources << Rails::Generator::PathSource.new(
10
+ :switchtower, File.dirname(__FILE__))
11
+
12
+ args = ["deployment"]
13
+ args << (options[:application] || "Application")
14
+ args << (options[:recipe_file] || "deploy")
15
+
16
+ Rails::Generator::Scripts::Generate.new.run(args)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ module SwitchTower
2
+ class Logger #:nodoc:
3
+ attr_accessor :level
4
+
5
+ IMPORTANT = 0
6
+ INFO = 1
7
+ DEBUG = 2
8
+ TRACE = 3
9
+
10
+ def initialize(options={})
11
+ output = options[:output] || STDERR
12
+ case
13
+ when output.respond_to?(:puts)
14
+ @device = output
15
+ else
16
+ @device = File.open(output.to_str, "a")
17
+ @needs_close = true
18
+ end
19
+
20
+ @options = options
21
+ @level = 0
22
+ end
23
+
24
+ def close
25
+ @device.close if @needs_close
26
+ end
27
+
28
+ def log(level, message, line_prefix=nil)
29
+ if level <= self.level
30
+ if line_prefix
31
+ message.split(/\r?\n/).each do |line|
32
+ @device.print "[#{line_prefix}] #{line.strip}\n"
33
+ end
34
+ else
35
+ @device.puts message.strip
36
+ end
37
+ end
38
+ end
39
+
40
+ def important(message, line_prefix=nil)
41
+ log(IMPORTANT, message, line_prefix)
42
+ end
43
+
44
+ def info(message, line_prefix=nil)
45
+ log(INFO, message, line_prefix)
46
+ end
47
+
48
+ def debug(message, line_prefix=nil)
49
+ log(DEBUG, message, line_prefix)
50
+ end
51
+
52
+ def trace(message, line_prefix=nil)
53
+ log(TRACE, message, line_prefix)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,175 @@
1
+ # Standard tasks that are useful for most recipes. It makes a few assumptions:
2
+ #
3
+ # * The :app role has been defined as the set of machines consisting of the
4
+ # application servers.
5
+ # * The :web role has been defined as the set of machines consisting of the
6
+ # web servers.
7
+ # * The Rails spinner and reaper scripts are being used to manage the FCGI
8
+ # processes.
9
+ # * There is a script in script/ called "reap" that restarts the FCGI processes
10
+
11
+ set :rake, "rake"
12
+
13
+ desc "Enumerate and describe every available task."
14
+ task :show_tasks do
15
+ keys = tasks.keys.sort_by { |a| a.to_s }
16
+ longest = keys.inject(0) { |len,key| key.to_s.length > len ? key.to_s.length : len } + 2
17
+
18
+ puts "Available tasks"
19
+ puts "---------------"
20
+ tasks.keys.sort_by { |a| a.to_s }.each do |key|
21
+ desc = (tasks[key].options[:desc] || "").strip.split(/\r?\n/)
22
+ puts "%-#{longest}s %s" % [key, desc.shift]
23
+ puts "%#{longest}s %s" % ["", desc.shift] until desc.empty?
24
+ puts
25
+ end
26
+ end
27
+
28
+ desc "Set up the expected application directory structure on all boxes"
29
+ task :setup, :roles => [:app, :db, :web] do
30
+ run <<-CMD
31
+ mkdir -p -m 775 #{releases_path} #{shared_path}/system &&
32
+ mkdir -p -m 777 #{shared_path}/log
33
+ CMD
34
+ end
35
+
36
+ desc <<-DESC
37
+ Disable the web server by writing a "maintenance.html" file to the web
38
+ servers. The servers must be configured to detect the presence of this file,
39
+ and if it is present, always display it instead of performing the request.
40
+ DESC
41
+ task :disable_web, :roles => :web do
42
+ on_rollback { delete "#{shared_path}/system/maintenance.html" }
43
+
44
+ maintenance = render("maintenance", :deadline => ENV['UNTIL'],
45
+ :reason => ENV['REASON'])
46
+ put maintenance, "#{shared_path}/system/maintenance.html", :mode => 0644
47
+ end
48
+
49
+ desc %(Re-enable the web server by deleting any "maintenance.html" file.)
50
+ task :enable_web, :roles => :web do
51
+ delete "#{shared_path}/system/maintenance.html"
52
+ end
53
+
54
+ desc <<-DESC
55
+ Update all servers with the latest release of the source code. All this does
56
+ is do a checkout (as defined by the selected scm module).
57
+ DESC
58
+ task :update_code, :roles => [:app, :db, :web] do
59
+ on_rollback { delete release_path, :recursive => true }
60
+
61
+ source.checkout(self)
62
+
63
+ run <<-CMD
64
+ rm -rf #{release_path}/log #{release_path}/public/system &&
65
+ ln -nfs #{shared_path}/log #{release_path}/log &&
66
+ ln -nfs #{shared_path}/system #{release_path}/public/system
67
+ CMD
68
+ end
69
+
70
+ desc <<-DESC
71
+ Rollback the latest checked-out version to the previous one by fixing the
72
+ symlinks and deleting the current release from all servers.
73
+ DESC
74
+ task :rollback_code, :roles => [:app, :db, :web] do
75
+ if releases.length < 2
76
+ raise "could not rollback the code because there is no prior release"
77
+ else
78
+ run <<-CMD
79
+ ln -nfs #{previous_release} #{current_path} &&
80
+ rm -rf #{current_release}
81
+ CMD
82
+ end
83
+ end
84
+
85
+ desc <<-DESC
86
+ Update the 'current' symlink to point to the latest version of
87
+ the application's code.
88
+ DESC
89
+ task :symlink, :roles => [:app, :db, :web] do
90
+ on_rollback { run "ln -nfs #{previous_release} #{current_path}" }
91
+ run "ln -nfs #{current_release} #{current_path}"
92
+ end
93
+
94
+ desc "Restart the FCGI processes on the app server."
95
+ task :restart, :roles => :app do
96
+ sudo "#{current_path}/script/process/reaper"
97
+ end
98
+
99
+ set :migrate_target, :current
100
+ set :migrate_env, ""
101
+
102
+ desc <<-DESC
103
+ Run the migrate rake task. By default, it runs this in the version of the app
104
+ indicated by the 'current' symlink. (This means you should not invoke this task
105
+ until the symlink has been updated to the most recent version.) However, you
106
+ can specify a different release via the migrate_target variable, which must be
107
+ one of "current" (for the default behavior), or "latest" (for the latest release
108
+ to be deployed with the update_code task). You can also specify additional
109
+ environment variables to pass to rake via the migrate_env variable. Finally, you
110
+ can specify the full path to the rake executable by setting the rake variable.
111
+ DESC
112
+ task :migrate, :roles => :db, :only => { :primary => true } do
113
+ directory = case migrate_target.to_sym
114
+ when :current then current_path
115
+ when :latest then current_release
116
+ else
117
+ raise ArgumentError,
118
+ "you must specify one of current or latest for migrate_target"
119
+ end
120
+
121
+ run "cd #{directory} && " +
122
+ "#{rake} RAILS_ENV=production #{migrate_env} migrate"
123
+ end
124
+
125
+ desc <<-DESC
126
+ A macro-task that updates the code, fixes the symlink, and restarts the
127
+ application servers.
128
+ DESC
129
+ task :deploy do
130
+ transaction do
131
+ update_code
132
+ symlink
133
+ end
134
+
135
+ restart
136
+ end
137
+
138
+ desc <<-DESC
139
+ 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,
141
+ and transactions are not used, because migrations are not guaranteed to be
142
+ reversible.)
143
+ DESC
144
+ task :deploy_with_migrations do
145
+ update_code
146
+
147
+ begin
148
+ old_migrate_target = migrate_target
149
+ set :migrate_target, :latest
150
+ migrate
151
+ ensure
152
+ set :migrate_target, old_migrate_target
153
+ end
154
+
155
+ symlink
156
+
157
+ restart
158
+ end
159
+
160
+ desc "A macro-task that rolls back the code and restarts the application servers."
161
+ task :rollback do
162
+ rollback_code
163
+ restart
164
+ end
165
+
166
+ desc <<-DESC
167
+ Displays the diff between HEAD and what was last deployed. (Not available
168
+ with all SCM's.)
169
+ DESC
170
+ task :diff_from_last_deploy do
171
+ diff = source.diff(self)
172
+ puts
173
+ puts diff
174
+ puts
175
+ end