felix-vlad 1.2.0.3
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/History.txt +49 -0
- data/Manifest.txt +31 -0
- data/README.txt +78 -0
- data/Rakefile +51 -0
- data/considerations.txt +91 -0
- data/doco/faq.txt +75 -0
- data/doco/getting_started.txt +41 -0
- data/doco/migration.txt +43 -0
- data/doco/perforce.txt +5 -0
- data/doco/variables.txt +77 -0
- data/lib/rake_remote_task.rb +566 -0
- data/lib/vlad.rb +91 -0
- data/lib/vlad/apache.rb +37 -0
- data/lib/vlad/core.rb +175 -0
- data/lib/vlad/git.rb +43 -0
- data/lib/vlad/lighttpd.rb +85 -0
- data/lib/vlad/maintenance.rb +23 -0
- data/lib/vlad/merb.god.rb +21 -0
- data/lib/vlad/mercurial.rb +34 -0
- data/lib/vlad/mongrel.rb +61 -0
- data/lib/vlad/perforce.rb +117 -0
- data/lib/vlad/subversion.rb +34 -0
- data/test/test_rake_remote_task.rb +186 -0
- data/test/test_vlad.rb +211 -0
- data/test/test_vlad_git.rb +39 -0
- data/test/test_vlad_mercurial.rb +26 -0
- data/test/test_vlad_perforce.rb +37 -0
- data/test/test_vlad_subversion.rb +27 -0
- data/test/vlad_test_case.rb +71 -0
- data/vladdemo.sh +64 -0
- metadata +124 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'vlad'
|
2
|
+
namespace :vlad do
|
3
|
+
set :merb_env, 'production'
|
4
|
+
|
5
|
+
remote_task :stop_app, :roles => [:app] do
|
6
|
+
run "sudo /usr/bin/god stop #{application}"
|
7
|
+
end
|
8
|
+
remote_task :start_app, :roles => [:app] do
|
9
|
+
run "sudo /usr/bin/god restart #{application}"
|
10
|
+
end
|
11
|
+
|
12
|
+
remote_task :symlink_configs, :roles => [:app] do
|
13
|
+
run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml && mkdir -p #{release_path}/tmp/cache"
|
14
|
+
end
|
15
|
+
|
16
|
+
namespace :dm do
|
17
|
+
remote_task :migrate, :roles => [:db] do
|
18
|
+
run "cd #{current_path}; MERB_ENV=#{merb_env} rake dm:db:migrate"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Vlad::Mercurial
|
2
|
+
|
3
|
+
set :source, Vlad::Mercurial.new
|
4
|
+
|
5
|
+
##
|
6
|
+
# Returns the command that will check out +revision+ from the code repo
|
7
|
+
# into directory +destination+
|
8
|
+
|
9
|
+
def checkout(revision, destination)
|
10
|
+
revision = 'tip' if revision =~ /^head$/i
|
11
|
+
"hg pull -r #{revision} -R #{destination} #{code_repo}"
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns the command that will export +revision+ from the code repo into
|
16
|
+
# the directory +destination+.
|
17
|
+
|
18
|
+
def export(revision_or_source, destination)
|
19
|
+
revision_or_source = 'tip' if revision_or_source =~ /^head$/i
|
20
|
+
if revision_or_source =~ /^(\d+|tip)$/i then
|
21
|
+
"hg archive -r #{revision_or_source} -R #{code_repo} #{destination}"
|
22
|
+
else
|
23
|
+
"hg archive -R #{revision_or_source} #{destination}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Returns a command that maps human-friendly revision identifier +revision+
|
29
|
+
# into a subversion revision specification.
|
30
|
+
|
31
|
+
def revision(revision)
|
32
|
+
"`hg identify -R #{code_repo} | cut -f1 -d\\ `"
|
33
|
+
end
|
34
|
+
end
|
data/lib/vlad/mongrel.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'vlad'
|
2
|
+
|
3
|
+
namespace :vlad do
|
4
|
+
##
|
5
|
+
# Mongrel app server
|
6
|
+
|
7
|
+
set :mongrel_address, "127.0.0.1"
|
8
|
+
set :mongrel_clean, false
|
9
|
+
set :mongrel_command, 'mongrel_rails'
|
10
|
+
set(:mongrel_conf) { "#{shared_path}/mongrel_cluster.conf" }
|
11
|
+
set :mongrel_config_script, nil
|
12
|
+
set :mongrel_environment, "production"
|
13
|
+
set :mongrel_group, nil
|
14
|
+
set :mongrel_log_file, nil
|
15
|
+
set :mongrel_pid_file, nil
|
16
|
+
set :mongrel_port, 8000
|
17
|
+
set :mongrel_prefix, nil
|
18
|
+
set :mongrel_servers, 2
|
19
|
+
set :mongrel_user, nil
|
20
|
+
|
21
|
+
desc "Prepares application servers for deployment. Mongrel
|
22
|
+
configuration is set via the mongrel_* variables.".cleanup
|
23
|
+
|
24
|
+
remote_task :setup_app, :roles => :app do
|
25
|
+
cmd = [
|
26
|
+
"#{mongrel_command} cluster::configure",
|
27
|
+
"-N #{mongrel_servers}",
|
28
|
+
"-p #{mongrel_port}",
|
29
|
+
"-e #{mongrel_environment}",
|
30
|
+
"-a #{mongrel_address}",
|
31
|
+
"-c #{current_path}",
|
32
|
+
"-C #{mongrel_conf}",
|
33
|
+
("-P #{mongrel_pid_file}" if mongrel_pid_file),
|
34
|
+
("-l #{mongrel_log_file}" if mongrel_log_file),
|
35
|
+
("--user #{mongrel_user}" if mongrel_user),
|
36
|
+
("--group #{mongrel_group}" if mongrel_group),
|
37
|
+
("--prefix #{mongrel_prefix}" if mongrel_prefix),
|
38
|
+
("-S #{mongrel_config_script}" if mongrel_config_script),
|
39
|
+
].compact.join ' '
|
40
|
+
|
41
|
+
run cmd
|
42
|
+
end
|
43
|
+
|
44
|
+
def mongrel(cmd) # :nodoc:
|
45
|
+
cmd = "#{mongrel_command} #{cmd} -C #{mongrel_conf}"
|
46
|
+
cmd << ' --clean' if mongrel_clean
|
47
|
+
cmd
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "Restart the app servers"
|
51
|
+
|
52
|
+
remote_task :start_app, :roles => :app do
|
53
|
+
run mongrel("cluster::restart")
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "Stop the app servers"
|
57
|
+
|
58
|
+
remote_task :stop_app, :roles => :app do
|
59
|
+
run mongrel("cluster::stop")
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
class Vlad::Perforce
|
2
|
+
|
3
|
+
set :p4_cmd, "p4"
|
4
|
+
set :source, Vlad::Perforce.new
|
5
|
+
|
6
|
+
##
|
7
|
+
# Returns the p4 command that will checkout +revision+ into the directory
|
8
|
+
# +destination+.
|
9
|
+
|
10
|
+
def checkout(revision, destination)
|
11
|
+
"#{p4_cmd} sync ...#{rev_no(revision)}"
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns the p4 command that will export +revision+ into the directory
|
16
|
+
# +directory+.
|
17
|
+
|
18
|
+
def export(revision_or_source, destination)
|
19
|
+
if revision_or_source =~ /^(\d+|head)$/i then
|
20
|
+
"(cd #{destination} && #{p4_cmd} sync ...#{rev_no(revision_or_source)})"
|
21
|
+
else
|
22
|
+
"cp -r #{revision_or_source} #{destination}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns a command that maps human-friendly revision identifier +revision+
|
28
|
+
# into a Perforce revision specification.
|
29
|
+
|
30
|
+
def revision(revision)
|
31
|
+
"`#{p4_cmd} changes -s submitted -m 1 ...#{rev_no(revision)} | cut -f 2 -d\\ `"
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Maps revision +revision+ into a Perforce revision.
|
36
|
+
|
37
|
+
def rev_no(revision)
|
38
|
+
case revision.to_s
|
39
|
+
when /head/i then
|
40
|
+
"#head"
|
41
|
+
when /^\d+$/ then
|
42
|
+
"@#{revision}"
|
43
|
+
else
|
44
|
+
revision
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
namespace :vlad do
|
50
|
+
remote_task :setup_app, :roles => :app do
|
51
|
+
p4data = p4port = p4user = p4passwd = nil
|
52
|
+
|
53
|
+
if ENV['P4CONFIG'] then
|
54
|
+
p4config_name = ENV['P4CONFIG']
|
55
|
+
p4config = nil
|
56
|
+
orig_dir = Dir.pwd.split File::SEPARATOR
|
57
|
+
|
58
|
+
until orig_dir.length == 1 do
|
59
|
+
p4config = orig_dir + [p4config_name]
|
60
|
+
p4config = File.join p4config
|
61
|
+
break if File.exist? p4config
|
62
|
+
orig_dir.pop
|
63
|
+
end
|
64
|
+
|
65
|
+
raise "couldn't find .p4config" unless File.exist? p4config
|
66
|
+
|
67
|
+
p4data = File.readlines(p4config).map { |line| line.strip.split '=', 2 }
|
68
|
+
p4data = Hash[*p4data.flatten]
|
69
|
+
else
|
70
|
+
p4data = ENV
|
71
|
+
end
|
72
|
+
|
73
|
+
p4port = p4data['P4PORT']
|
74
|
+
p4user = p4data['P4USER']
|
75
|
+
p4passwd = p4data['P4PASSWD']
|
76
|
+
|
77
|
+
raise "couldn't get P4PORT" if p4port.nil?
|
78
|
+
raise "couldn't get P4USER" if p4user.nil?
|
79
|
+
raise "couldn't get P4PASSWD" if p4passwd.nil?
|
80
|
+
|
81
|
+
p4client = [p4user, target_host, application].join '-'
|
82
|
+
|
83
|
+
require 'tmpdir'
|
84
|
+
require 'tempfile'
|
85
|
+
|
86
|
+
put File.join(scm_path, '.p4config'), 'vlad.p4config' do
|
87
|
+
[ "P4PORT=#{p4port}",
|
88
|
+
"P4USER=#{p4user}",
|
89
|
+
"P4PASSWD=#{p4passwd}",
|
90
|
+
"P4CLIENT=#{p4client}" ].join("\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
p4client_path = File.join deploy_to, 'p4client.tmp'
|
94
|
+
|
95
|
+
put p4client_path, 'vlad.p4client' do
|
96
|
+
conf = <<-"CLIENT"
|
97
|
+
Client: #{p4client}
|
98
|
+
|
99
|
+
Owner: #{p4user}
|
100
|
+
|
101
|
+
Root: #{scm_path}
|
102
|
+
|
103
|
+
View:
|
104
|
+
#{code_repo}/... //#{p4client}/...
|
105
|
+
CLIENT
|
106
|
+
end
|
107
|
+
|
108
|
+
cmds = [
|
109
|
+
"cd #{scm_path}",
|
110
|
+
"p4 client -i < #{p4client_path}",
|
111
|
+
"rm #{p4client_path}",
|
112
|
+
]
|
113
|
+
|
114
|
+
run cmds.join(' && ')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Vlad::Subversion
|
2
|
+
|
3
|
+
set :source, Vlad::Subversion.new
|
4
|
+
set :svn_cmd, "svn"
|
5
|
+
|
6
|
+
##
|
7
|
+
# Returns the command that will check out +revision+ from the code repo
|
8
|
+
# into directory +destination+
|
9
|
+
|
10
|
+
def checkout(revision, destination)
|
11
|
+
"#{svn_cmd} co -r #{revision} #{code_repo} #{destination}"
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns the command that will export +revision+ from the code repo into
|
16
|
+
# the directory +destination+.
|
17
|
+
|
18
|
+
def export(revision_or_source, destination)
|
19
|
+
if revision_or_source =~ /^(\d+|head)$/i then
|
20
|
+
"#{svn_cmd} export -r #{revision_or_source} #{code_repo} #{destination}"
|
21
|
+
else
|
22
|
+
"#{svn_cmd} export #{revision_or_source} #{destination}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns a command that maps human-friendly revision identifier +revision+
|
28
|
+
# into a subversion revision specification.
|
29
|
+
|
30
|
+
def revision(revision)
|
31
|
+
"`#{svn_cmd} info #{code_repo} | grep 'Revision:' | cut -f2 -d\\ `"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -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
|