m7d-vlad 1.2.0.4

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,91 @@
1
+ require 'rubygems'
2
+ require 'thread'
3
+ require 'rake_remote_task'
4
+
5
+ $TESTING ||= false
6
+
7
+ ##
8
+ # Vlad the Deployer - Pragmatic application deployment automation, without mercy.
9
+ #
10
+ # Please read doco/getting_started.txt or http://rubyhitsquad.com/
11
+ #
12
+ # === Basic scenario:
13
+ #
14
+ # 1. rake vlad:setup (first time only)
15
+ # 2. rake vlad:update
16
+ # 3. rake vlad:migrate (optional)
17
+ # 4. rake vlad:start
18
+
19
+ module Vlad
20
+
21
+ ##
22
+ # This is the version of Vlad you are running.
23
+ VERSION = '1.2.0.3'
24
+
25
+ ##
26
+ # Base error class for all Vlad errors.
27
+ class Error < RuntimeError; end
28
+
29
+ ##
30
+ # Raised when you have incorrectly configured Vlad.
31
+ class ConfigurationError < Error; end
32
+
33
+ ##
34
+ # Raised when a remote command fails.
35
+ class CommandFailedError < Error; end
36
+
37
+ ##
38
+ # Raised when an environment variable hasn't been set.
39
+ class FetchError < Error; end
40
+
41
+ ##
42
+ # Loads tasks file +tasks_file+ and various recipe styles as a hash
43
+ # of category/style pairs. Recipes default to:
44
+ #
45
+ # :app => :mongrel
46
+ # :config => 'config/deploy.rb'
47
+ # :core => :core
48
+ # :scm => :subversion
49
+ # :web => :apache
50
+ #
51
+ # You can override individual values and/or set to nil to
52
+ # deactivate. :config will get loaded last to ensure that user
53
+ # variables override default values.
54
+ #
55
+ # And by all means, feel free to skip this entirely if it doesn't
56
+ # fit for you. All it does is a fancy-pants require. Require
57
+ # whatever files you need as you see fit straight from your
58
+ # Rakefile. YAY for simple and clean!
59
+ def self.load options = {}
60
+ options = {:config => options} if String === options
61
+
62
+ recipes = {
63
+ :app => 'merb',
64
+ :config => 'config/deploy.rb',
65
+ :core => :core,
66
+ :scm => :git,
67
+ :web => :maintenance,
68
+ }.merge(options)
69
+
70
+ # be sure core comes first so base tasks aren't clobbered
71
+ if core = recipes.delete(:core)
72
+ require "vlad/#{core}"
73
+ end
74
+ recipes.each do |flavor, recipe|
75
+ next if recipe.nil? or flavor == :config
76
+ require "vlad/#{recipe}"
77
+ end
78
+
79
+ Kernel.load recipes[:config]
80
+ end
81
+ end
82
+
83
+ class String #:nodoc:
84
+ def cleanup
85
+ if ENV['FULL'] then
86
+ gsub(/\s+/, ' ').strip
87
+ else
88
+ self[/\A.*?\./]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,175 @@
1
+ require 'vlad'
2
+
3
+ ##
4
+ # used by update, out here so we can ensure all threads have the same value
5
+ def now
6
+ @now ||= Time.now.utc.strftime("%Y%m%d%H%M.%S")
7
+ end
8
+
9
+ namespace :vlad do
10
+ desc "Show the vlad setup. This is all the default variables for vlad
11
+ tasks.".cleanup
12
+
13
+ task :debug do
14
+ require 'yaml'
15
+
16
+ # force them into values
17
+ Rake::RemoteTask.env.keys.each do |key|
18
+ next if key =~ /_release|releases|sudo_password/
19
+ Rake::RemoteTask.fetch key
20
+ end
21
+
22
+ puts "# Environment:"
23
+ puts
24
+ y Rake::RemoteTask.env
25
+ puts "# Roles:"
26
+ y Rake::RemoteTask.roles
27
+ end
28
+
29
+ desc "Setup your servers. Before you can use any of the deployment
30
+ tasks with your project, you will need to make sure all of your
31
+ servers have been prepared with 'rake vlad:setup'. It is safe to
32
+ run this task on servers that have already been set up; it will
33
+ not destroy any deployed revisions or data.".cleanup
34
+
35
+ task :setup do
36
+ Rake::Task['vlad:setup_app'].invoke
37
+ end
38
+
39
+ desc "Prepares application servers for deployment.".cleanup
40
+
41
+ remote_task :setup_app, :roles => :app do
42
+ dirs = [deploy_to, releases_path, scm_path, shared_path]
43
+ dirs += %w(system log pids).map { |d| File.join(shared_path, d) }
44
+ run "umask 02 && mkdir -p #{dirs.join(' ')}"
45
+ end
46
+
47
+ desc "Updates your application server to the latest revision. Syncs
48
+ a copy of the repository, exports it as the latest release, fixes
49
+ up your symlinks, symlinks the latest revision to current and logs
50
+ the update.".cleanup
51
+
52
+ remote_task :update, :roles => :app do
53
+ symlink = false
54
+ begin
55
+ run [ "cd #{scm_path}",
56
+ "#{source.checkout revision, '.'}",
57
+ "#{source.export ".", release_path}",
58
+ "chmod -R g+w #{latest_release}",
59
+ "rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids",
60
+ "mkdir -p #{latest_release}/db #{latest_release}/tmp",
61
+ "ln -s #{shared_path}/log #{latest_release}/log",
62
+ "ln -s #{shared_path}/system #{latest_release}/public/system",
63
+ "ln -s #{shared_path}/pids #{latest_release}/tmp/pids",
64
+ ].join(" && ")
65
+
66
+ symlink = true
67
+ run "rm -f #{current_path} && ln -s #{latest_release} #{current_path}"
68
+
69
+ run "echo #{now} $USER #{revision} #{File.basename release_path} >> #{deploy_to}/revisions.log"
70
+ rescue => e
71
+ run "rm -f #{current_path} && ln -s #{previous_release} #{current_path}" if
72
+ symlink
73
+ run "rm -rf #{release_path}"
74
+ raise e
75
+ end
76
+ end
77
+
78
+ desc "Run the migrate rake task for the the app. By default this is run in
79
+ the latest app directory. You can run migrations for the current app
80
+ directory by setting :migrate_target to :current. Additional environment
81
+ variables can be passed to rake via the migrate_env variable.".cleanup
82
+
83
+ # No application files are on the DB machine, also migrations should only be
84
+ # run once.
85
+ remote_task :migrate, :roles => :app do
86
+ break unless target_host == Rake::RemoteTask.hosts_for(:app).first
87
+
88
+ directory = case migrate_target.to_sym
89
+ when :current then current_path
90
+ when :latest then current_release
91
+ else raise ArgumentError, "unknown migration target #{migrate_target.inspect}"
92
+ end
93
+
94
+ run "cd #{current_path}; #{rake_cmd} RAILS_ENV=#{rails_env} db:migrate #{migrate_args}"
95
+ end
96
+
97
+ desc "Invoke a single command on every remote server. This is useful for
98
+ performing one-off commands that may not require a full task to be written
99
+ for them. Simply specify the command to execute via the COMMAND
100
+ environment variable. To execute the command only on certain roles,
101
+ specify the ROLES environment variable as a comma-delimited list of role
102
+ names.
103
+
104
+ $ rake vlad:invoke COMMAND='uptime'".cleanup
105
+
106
+ remote_task :invoke do
107
+ command = ENV["COMMAND"]
108
+ abort "Please specify a command to execute on the remote servers (via the COMMAND environment variable)" unless command
109
+ puts run(command)
110
+ end
111
+
112
+ desc "Copy arbitrary files to the currently deployed version using
113
+ FILES=a,b,c. This is useful for updating files piecemeal when you
114
+ need to quickly deploy only a single file.
115
+
116
+ To use this task, specify the files and directories you want to copy as a
117
+ comma-delimited list in the FILES environment variable. All directories
118
+ will be processed recursively, with all files being pushed to the
119
+ deployment servers. Any file or directory starting with a '.' character
120
+ will be ignored.
121
+
122
+ $ rake vlad:upload FILES=templates,controller.rb".cleanup
123
+
124
+ remote_task :upload do
125
+ file_list = (ENV["FILES"] || "").split(",")
126
+
127
+ files = file_list.map do |f|
128
+ f = f.strip
129
+ File.directory?(f) ? Dir["#{f}/**/*"] : f
130
+ end.flatten
131
+
132
+ files = files.reject { |f| File.directory?(f) || File.basename(f)[0] == ?. }
133
+
134
+ abort "Please specify at least one file to update (via the FILES environment variable)" if files.empty?
135
+
136
+ files.each do |file|
137
+ rsync file, File.join(current_path, file)
138
+ end
139
+ end
140
+
141
+ desc "Rolls back to a previous version and restarts. This is handy if you
142
+ ever discover that you've deployed a lemon; 'rake vlad:rollback' and
143
+ you're right back where you were, on the previously deployed
144
+ version.".cleanup
145
+
146
+ remote_task :rollback do
147
+ if releases.length < 2 then
148
+ abort "could not rollback the code because there is no prior release"
149
+ else
150
+ run "rm #{current_path}; ln -s #{previous_release} #{current_path} && rm -rf #{current_release}"
151
+ end
152
+
153
+ Rake::Task['vlad:start'].invoke
154
+ end
155
+
156
+ desc "Clean up old releases. By default, the last 5 releases are kept on
157
+ each server (though you can change this with the keep_releases variable).
158
+ All other deployed revisions are removed from the servers.".cleanup
159
+
160
+ remote_task :cleanup do
161
+ max = keep_releases
162
+ if releases.length <= max then
163
+ puts "no old releases to clean up #{releases.length} <= #{max}"
164
+ else
165
+ puts "keeping #{max} of #{releases.length} deployed releases"
166
+
167
+ directories = (releases - releases.last(max)).map { |release|
168
+ File.join(releases_path, release)
169
+ }.join(" ")
170
+
171
+ run "rm -rf #{directories}"
172
+ end
173
+ end
174
+
175
+ end # namespace vlad
@@ -0,0 +1,43 @@
1
+ class Vlad::Git
2
+
3
+ set :source, Vlad::Git.new
4
+ set :git_cmd, "git"
5
+
6
+ ##
7
+ # Returns the command that will check out +revision+ from the
8
+ # code repo into directory +destination+. +revision+ can be any
9
+ # SHA1 or equivalent (e.g. branch, tag, etc...)
10
+
11
+ def checkout(revision, destination)
12
+ destination = 'repo' if destination == '.'
13
+ revision = 'HEAD' if revision =~ /head/i
14
+
15
+ [ "rm -rf #{destination}",
16
+ "#{git_cmd} clone #{code_repo} #{destination}",
17
+ "cd #{destination}",
18
+ "#{git_cmd} checkout -f -b deployed-#{revision} #{revision}"
19
+ ].join(" && ")
20
+ end
21
+
22
+ ##
23
+ # Returns the command that will export +revision+ from the code repo into
24
+ # the directory +destination+.
25
+
26
+ def export(revision, destination)
27
+ revision = 'HEAD' if revision == "."
28
+
29
+ [ "mkdir -p #{destination}",
30
+ "#{git_cmd} archive --format=tar #{revision} | (cd #{destination} && tar xf -)"
31
+ ].join(" && ")
32
+ end
33
+
34
+ ##
35
+ # Returns a command that maps human-friendly revision identifier +revision+
36
+ # into a git SHA1.
37
+
38
+ def revision(revision)
39
+ revision = 'HEAD' if revision =~ /head/i
40
+
41
+ "`#{git_cmd} rev-parse #{revision}`"
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ namespace :vlad do
2
+ desc "Remove the maintenance page"
3
+ remote_task :start_web, :roles => [:web] do
4
+ run "if [ -f #{shared_path}/system/maintenance.html ]; then rm -f #{shared_path}/system/maintenance.html; fi"
5
+ end
6
+
7
+ desc "Put the maintenance page in place"
8
+ remote_task :stop_web, :roles => [:web] do
9
+ run "cp -f #{shared_path}/config/maintenance.html #{shared_path}/system/"
10
+ end
11
+
12
+ desc "(Re)Start the web and app servers"
13
+ remote_task :start do
14
+ #Rake::Task['vlad:start_app'].invoke
15
+ Rake::Task['vlad:start_web'].invoke
16
+ end
17
+
18
+ desc "Stop the web and app servers"
19
+ remote_task :stop do
20
+ #Rake::Task['vlad:stop_app'].invoke
21
+ Rake::Task['vlad:stop_web'].invoke
22
+ end
23
+ end
@@ -0,0 +1,186 @@
1
+ require 'test/vlad_test_case'
2
+ require 'vlad'
3
+
4
+ class TestRakeRemoteTask < VladTestCase
5
+ def test_enhance
6
+ util_set_hosts
7
+ body = Proc.new { 5 }
8
+ task = @vlad.remote_task(:some_task => :foo, &body)
9
+ action = Rake::RemoteTask::Action.new(task, body)
10
+ assert_equal [action], task.remote_actions
11
+ assert_equal task, action.task
12
+ assert_equal ["foo"], task.prerequisites
13
+ end
14
+
15
+ def test_enhance_with_no_task_body
16
+ util_set_hosts
17
+ util_setup_task
18
+ assert_equal [], @task.remote_actions
19
+ assert_equal [], @task.prerequisites
20
+ end
21
+
22
+ def test_execute
23
+ util_set_hosts
24
+ set :some_variable, 1
25
+ x = 5
26
+ task = @vlad.remote_task(:some_task) { x += some_variable }
27
+ task.execute nil
28
+ assert_equal 1, task.some_variable
29
+ assert_equal 2, task.remote_actions.first.workers.size
30
+ assert_equal 7, x
31
+ end
32
+
33
+ def test_execute_exposes_target_host
34
+ host "app.example.com", :app
35
+ task = remote_task(:target_task) { set(:test_target_host, target_host) }
36
+ task.execute nil
37
+ assert_equal "app.example.com", Rake::RemoteTask.fetch(:test_target_host)
38
+ end
39
+
40
+ def test_execute_with_no_hosts
41
+ @vlad.host "app.example.com", :app
42
+ t = @vlad.remote_task(:flunk, :roles => :db) { flunk "should not have run" }
43
+ e = assert_raise(Vlad::ConfigurationError) { t.execute nil }
44
+ assert_equal "No target hosts specified for task: flunk", e.message
45
+ end
46
+
47
+ def test_execute_with_no_roles
48
+ t = @vlad.remote_task(:flunk, :roles => :junk) { flunk "should not have run" }
49
+ e = assert_raise(Vlad::ConfigurationError) { t.execute nil }
50
+ assert_equal "No target hosts specified for task: flunk", e.message
51
+ end
52
+
53
+ def test_execute_with_roles
54
+ util_set_hosts
55
+ set :some_variable, 1
56
+ x = 5
57
+ task = @vlad.remote_task(:some_task, :roles => :db) { x += some_variable }
58
+ task.execute nil
59
+ assert_equal 1, task.some_variable
60
+ assert_equal 6, x
61
+ end
62
+
63
+ def test_rsync
64
+ util_setup_task
65
+ @task.target_host = "app.example.com"
66
+
67
+ @task.rsync 'localfile', 'remotefile'
68
+
69
+ commands = @task.commands
70
+
71
+ assert_equal 1, commands.size, 'not enough commands'
72
+ assert_equal %w[rsync -azP --delete localfile app.example.com:remotefile],
73
+ commands.first, 'rsync'
74
+ end
75
+
76
+ def test_rsync_fail
77
+ util_setup_task
78
+ @task.target_host = "app.example.com"
79
+ @task.action = lambda { false }
80
+
81
+ e = assert_raise(Vlad::CommandFailedError) { @task.rsync 'local', 'remote' }
82
+ assert_equal "execution failed: rsync -azP --delete local app.example.com:remote", e.message
83
+ end
84
+
85
+ def test_run
86
+ util_setup_task
87
+ @task.output << "file1\nfile2\n"
88
+ @task.target_host = "app.example.com"
89
+ result = nil
90
+
91
+ out, err = util_capture do
92
+ result = @task.run("ls")
93
+ end
94
+
95
+ commands = @task.commands
96
+
97
+ assert_equal 1, commands.size, 'not enough commands'
98
+ assert_equal ["ssh", "app.example.com", "ls"],
99
+ commands.first, 'app'
100
+ assert_equal "file1\nfile2\n", result
101
+
102
+ assert_equal "file1\nfile2\n", out.read
103
+ assert_equal '', err.read
104
+ end
105
+
106
+ def test_run_failing_command
107
+ util_set_hosts
108
+ util_setup_task
109
+ @task.input = StringIO.new "file1\nfile2\n"
110
+ @task.target_host = 'app.example.com'
111
+ @task.action = lambda { 1 }
112
+
113
+ e = assert_raise(Vlad::CommandFailedError) { @task.run("ls") }
114
+ assert_equal "execution failed with status 1: ssh app.example.com ls", e.message
115
+
116
+ assert_equal 1, @task.commands.size
117
+ end
118
+
119
+ def test_run_sudo
120
+ util_setup_task
121
+ @task.output << "file1\nfile2\n"
122
+ @task.error << 'Password:'
123
+ @task.target_host = "app.example.com"
124
+ def @task.sudo_password() "my password" end # gets defined by set
125
+ result = nil
126
+
127
+ out, err = util_capture do
128
+ result = @task.run("sudo ls")
129
+ end
130
+
131
+ commands = @task.commands
132
+
133
+ assert_equal 1, commands.size, 'not enough commands'
134
+ assert_equal ['ssh', 'app.example.com', 'sudo ls'],
135
+ commands.first
136
+
137
+ assert_equal "my password\n", @task.input.string
138
+
139
+ # WARN: Technically incorrect, the password line should be
140
+ # first... this is an artifact of changes to the IO code in run
141
+ # and the fact that we have a very simplistic (non-blocking)
142
+ # testing model.
143
+ assert_equal "file1\nfile2\nPassword:\n", result
144
+
145
+ assert_equal "file1\nfile2\n", out.read
146
+ assert_equal "Password:\n", err.read
147
+ end
148
+
149
+ def test_sudo
150
+ util_setup_task
151
+ @task.target_host = "app.example.com"
152
+ @task.sudo "ls"
153
+
154
+ commands = @task.commands
155
+
156
+ assert_equal 1, commands.size, 'wrong number of commands'
157
+ assert_equal ["ssh", "app.example.com", "sudo ls"],
158
+ commands.first, 'app'
159
+ end
160
+
161
+ def util_capture
162
+ require 'stringio'
163
+ orig_stdout = $stdout.dup
164
+ orig_stderr = $stderr.dup
165
+ captured_stdout = StringIO.new
166
+ captured_stderr = StringIO.new
167
+ $stdout = captured_stdout
168
+ $stderr = captured_stderr
169
+ yield
170
+ captured_stdout.rewind
171
+ captured_stderr.rewind
172
+ return captured_stdout, captured_stderr
173
+ ensure
174
+ $stdout = orig_stdout
175
+ $stderr = orig_stderr
176
+ end
177
+
178
+ def util_setup_task(options = {})
179
+ @task = @vlad.remote_task :test_task, options
180
+ @task.commands = []
181
+ @task.output = []
182
+ @task.error = []
183
+ @task.action = nil
184
+ @task
185
+ end
186
+ end