yad 0.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.
- data/.gitignore +9 -0
- data/LICENSE +20 -0
- data/README.rdoc +110 -0
- data/Rakefile +87 -0
- data/VERSION.yml +4 -0
- data/doc/faq.rdoc +9 -0
- data/features/step_definitions/yad_steps.rb +0 -0
- data/features/support/env.rb +9 -0
- data/features/yad.feature +9 -0
- data/lib/ext/rake.rb +66 -0
- data/lib/ext/rake/remote_task.rb +521 -0
- data/lib/ext/string.rb +6 -0
- data/lib/yad.rb +46 -0
- data/lib/yad/app/passenger.rb +28 -0
- data/lib/yad/core.rb +199 -0
- data/lib/yad/db/rails.rb +62 -0
- data/lib/yad/framework/rails.rb +64 -0
- data/lib/yad/maintenance/shared_system.rb +39 -0
- data/lib/yad/scm/git.rb +62 -0
- data/lib/yad/web/apache.rb +62 -0
- data/spec/ext/rake/remote_task_spec.rb +394 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/yad/app/passenger_spec.rb +10 -0
- data/spec/yad/core_spec.rb +69 -0
- data/spec/yad/db/rails_spec.rb +15 -0
- data/spec/yad/framework/rails_spec.rb +25 -0
- data/spec/yad/maintenance/shared_system_spec.rb +15 -0
- data/spec/yad/scm/git_spec.rb +30 -0
- data/spec/yad/web/apache_spec.rb +25 -0
- data/yad.gemspec +87 -0
- metadata +132 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Don Barlow
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
= Yad: Yet Another Deployer
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Vlad the Deployer by Ryan Davis, Eric Hodel and Wilson Bilkovich is
|
6
|
+
great, but it is not exactly what I wanted. The underlying
|
7
|
+
Rake::RemoteTask is awesome, so I wanted to keep that. However, I
|
8
|
+
wanted to change how the recipes work. Therefore, I created yet
|
9
|
+
another deployer, Yad.
|
10
|
+
|
11
|
+
In general, Yad is a deployer for database-backed applications in the
|
12
|
+
style of Capistrano by Jamis Buck. However, in addition to deploying
|
13
|
+
Rails web applications, it can be used to deploy applications that do
|
14
|
+
not have a web tier and work from a cron task or something
|
15
|
+
of that nature.
|
16
|
+
|
17
|
+
Yad works by setting up a a core set of tasks that do nothing by
|
18
|
+
default. It is up to you to define delegates that will take the
|
19
|
+
actions specific for your application.
|
20
|
+
|
21
|
+
http://sites.google.com/a/perrysburghacker.com/yad/_/rsrc/1247934950955/Home/yad.png
|
22
|
+
|
23
|
+
At this time, the following delegates are supported:
|
24
|
+
|
25
|
+
==== Source Code Managers
|
26
|
+
* Git
|
27
|
+
==== Database Managers
|
28
|
+
* None
|
29
|
+
* Rails
|
30
|
+
==== Frameworks
|
31
|
+
* None
|
32
|
+
* Rails
|
33
|
+
==== Application Servers
|
34
|
+
* Passenger
|
35
|
+
==== Maintenance Page
|
36
|
+
* Shared System
|
37
|
+
(i.e. maintenance.html in the shared/config directory copied to the shared/system
|
38
|
+
directory to be seen by your web server)
|
39
|
+
==== Web Servers
|
40
|
+
* Apache
|
41
|
+
|
42
|
+
|
43
|
+
== Installation
|
44
|
+
|
45
|
+
sudo gem install yad
|
46
|
+
or
|
47
|
+
sudo gem install ottobar-yad
|
48
|
+
|
49
|
+
|
50
|
+
== Examples
|
51
|
+
|
52
|
+
=== Single stage with git, rails and passenger
|
53
|
+
|
54
|
+
* Create a deploy.rake file:
|
55
|
+
|
56
|
+
# #{Rails.root}/lib/tasks/deploy.rake
|
57
|
+
begin
|
58
|
+
require 'yad'
|
59
|
+
|
60
|
+
set :application, "myapp"
|
61
|
+
set :repository, "path/to/my/repository/#{application}.git"
|
62
|
+
set :domain, "mydomain.com"
|
63
|
+
set :deploy_to "/home/deploy/projects/#{application}"
|
64
|
+
|
65
|
+
set :scm, :git
|
66
|
+
set :framework, :rails
|
67
|
+
# assumes that you have a config/database_production.yml file in your repository
|
68
|
+
set :framework_update_db_config_via, :copy
|
69
|
+
set :db, :rails
|
70
|
+
set :app, :passenger
|
71
|
+
|
72
|
+
desc "Deploy a new version of the application"
|
73
|
+
task "yad:deploy" => %w(yad:update yad:start_app yad:cleanup)
|
74
|
+
|
75
|
+
desc "Deploy a new version of the application and run database migrations"
|
76
|
+
task "yad:deploy_with_migrations" => %w(yad:update yad:migrate_db yad:start_app yad:cleanup)
|
77
|
+
|
78
|
+
rescue LoadError
|
79
|
+
puts "Yad not available. Install it with: sudo gem install yad"
|
80
|
+
end
|
81
|
+
|
82
|
+
* For initial deployment, run the following tasks:
|
83
|
+
|
84
|
+
rake yad:setup
|
85
|
+
rake yad:update
|
86
|
+
rake yad:create_db
|
87
|
+
rake yad:migrate_db
|
88
|
+
|
89
|
+
* Then, configure or otherwise set up your web server
|
90
|
+
|
91
|
+
* For subsequent releases, run:
|
92
|
+
|
93
|
+
rake yad:deploy
|
94
|
+
or
|
95
|
+
rake yad:deploy_with_migrations
|
96
|
+
|
97
|
+
--
|
98
|
+
see the FAQ[link:files/doc/faq_rdoc.html] for more details
|
99
|
+
++
|
100
|
+
|
101
|
+
== Links
|
102
|
+
|
103
|
+
* http://github.com/ottobar/yad/tree/master
|
104
|
+
* http://yad.rubyforge.org
|
105
|
+
* http://rubyforge.org/projects/yad
|
106
|
+
|
107
|
+
|
108
|
+
== Copyright
|
109
|
+
|
110
|
+
Copyright (c) 2009 Don Barlow. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "yad"
|
8
|
+
gem.description = "Yad: Yet Another Deployer"
|
9
|
+
gem.summary = %Q{Yet another deployer, pretty much stolen from Vlad}
|
10
|
+
gem.email = "ottobar@perryburghacker.com"
|
11
|
+
gem.homepage = "http://yad.rubyforge.org"
|
12
|
+
gem.authors = ["Don Barlow"]
|
13
|
+
gem.rubyforge_project = "yad"
|
14
|
+
gem.add_dependency('rake', '~> 0.8.7')
|
15
|
+
gem.add_dependency('open4', '~> 0.9.6')
|
16
|
+
gem.add_development_dependency('voloko-sdoc', '~> 0.2.12.1')
|
17
|
+
gem.add_development_dependency('technicalpickles-jeweler', '~> 1.0.1')
|
18
|
+
|
19
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'spec/rake/spectask'
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
32
|
+
spec.libs << 'lib' << 'spec'
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
begin
|
41
|
+
require 'sdoc'
|
42
|
+
Rake::RDocTask.new('doc') do |rdoc|
|
43
|
+
if File.exist?('VERSION.yml')
|
44
|
+
config = YAML.load(File.read('VERSION.yml'))
|
45
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
46
|
+
else
|
47
|
+
version = ""
|
48
|
+
end
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'doc/rdoc'
|
51
|
+
rdoc.title = "Yad: Yet Another Deployer v#{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
rdoc.rdoc_files.include('LICENSE')
|
55
|
+
rdoc.rdoc_files.include('doc/faq.rdoc')
|
56
|
+
rdoc.main = 'README.rdoc'
|
57
|
+
rdoc.template = 'direct'
|
58
|
+
end
|
59
|
+
rescue LoadError
|
60
|
+
puts "Sdoc not available. Install it with: sudo gem install voloko-sdoc -s http://gems.github.com"
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
require 'rake/contrib/sshpublisher'
|
65
|
+
namespace :rubyforge do
|
66
|
+
|
67
|
+
desc "Release gem and RDoc documentation to RubyForge"
|
68
|
+
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
69
|
+
|
70
|
+
namespace :release do
|
71
|
+
desc "Publish RDoc to RubyForge."
|
72
|
+
task :docs => [:doc] do
|
73
|
+
config = YAML.load(
|
74
|
+
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
75
|
+
)
|
76
|
+
|
77
|
+
host = "#{config['username']}@rubyforge.org"
|
78
|
+
remote_dir = "/var/www/gforge-projects/yad/"
|
79
|
+
local_dir = 'doc/rdoc'
|
80
|
+
|
81
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
rescue LoadError
|
86
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
87
|
+
end
|
data/VERSION.yml
ADDED
data/doc/faq.rdoc
ADDED
File without changes
|
data/lib/ext/rake.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Rake
|
2
|
+
module TaskManager
|
3
|
+
# This gives us access to the tasks already defined in rake.
|
4
|
+
def all_tasks
|
5
|
+
@tasks
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Simple shortcut for Rake.application.all_tasks
|
10
|
+
def self.all_tasks
|
11
|
+
Rake.application.all_tasks
|
12
|
+
end
|
13
|
+
|
14
|
+
# Hooks into rake and allows us to clear out a task by name or
|
15
|
+
# regexp. Use this if you want to completely override a task instead
|
16
|
+
# of extend it.
|
17
|
+
def self.clear_tasks(*tasks)
|
18
|
+
tasks.flatten.each do |name|
|
19
|
+
case name
|
20
|
+
when Regexp then
|
21
|
+
all_tasks.delete_if { |k,_| k =~ name }
|
22
|
+
else
|
23
|
+
all_tasks.delete(name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Removes the last action added to a task. Use this when two
|
29
|
+
# libraries define the same task and you only want one of the
|
30
|
+
# actions.
|
31
|
+
#
|
32
|
+
# require 'hoe'
|
33
|
+
# require 'tasks/rails'
|
34
|
+
# Rake.undo("test") # rolls out rails' test task
|
35
|
+
def self.undo(*names)
|
36
|
+
names.each do |name|
|
37
|
+
all_tasks[name].actions.delete_at(-1)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Task
|
42
|
+
# Invokes the task named +task_name+ if the +module_name+ has been
|
43
|
+
# defined. It is the mechanism for delegating commands out of the
|
44
|
+
# core to modules specific for your application
|
45
|
+
def self.invoke_if_defined(task_name, module_name, error_message = nil)
|
46
|
+
module_value = Rake::RemoteTask.fetch(module_name, false)
|
47
|
+
if module_value
|
48
|
+
# define delegated tasks
|
49
|
+
eval("Yad::#{module_name.to_s.classify}::#{module_value.to_s.classify}.define_tasks")
|
50
|
+
Rake::Task[task_name].invoke
|
51
|
+
elsif error_message
|
52
|
+
raise ArgumentError, error_message
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if Gem::Version.new(RAKEVERSION) < Gem::Version.new('0.8') then
|
59
|
+
class Rake::Task
|
60
|
+
alias task_original_execute execute
|
61
|
+
|
62
|
+
def execute(args = nil)
|
63
|
+
task_original_execute
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,521 @@
|
|
1
|
+
$TESTING ||= false
|
2
|
+
$TRACE = Rake.application.options.trace
|
3
|
+
$-w = true if $TRACE
|
4
|
+
|
5
|
+
def export receiver, *methods
|
6
|
+
methods.each do |method|
|
7
|
+
eval "def #{method} *args, █ #{receiver}.#{method}(*args, &block);end"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
export "Thread.current[:task]", :get, :put, :rsync, :run, :sudo, :target_host
|
12
|
+
export "Rake::RemoteTask", :host, :remote_task, :role, :set
|
13
|
+
|
14
|
+
# Rake::RemoteTask is a subclass of Rake::Task that adds
|
15
|
+
# remote_actions that execute in parallel on multiple hosts via ssh.
|
16
|
+
class Rake::RemoteTask < Rake::Task
|
17
|
+
# Base error class for all errors.
|
18
|
+
class Error < RuntimeError; end
|
19
|
+
|
20
|
+
# Raised when you have incorrect configuration.
|
21
|
+
class ConfigurationError < Error; end
|
22
|
+
|
23
|
+
# Raised when a remote command fails.
|
24
|
+
class CommandFailedError < Error; end
|
25
|
+
|
26
|
+
# Raised when an environment variable hasn't been set.
|
27
|
+
class FetchError < Error; end
|
28
|
+
|
29
|
+
@@current_roles = []
|
30
|
+
|
31
|
+
include Open4
|
32
|
+
|
33
|
+
# Options for execution of this task.
|
34
|
+
attr_accessor :options
|
35
|
+
|
36
|
+
# The host this task is running on during execution.
|
37
|
+
attr_accessor :target_host
|
38
|
+
|
39
|
+
# An Array of Actions this host will perform during execution. Use
|
40
|
+
# enhance to add new actions to a task.
|
41
|
+
attr_reader :remote_actions
|
42
|
+
|
43
|
+
def self.current_roles
|
44
|
+
@@current_roles
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a new task named +task_name+ attached to Rake::Application +app+.
|
48
|
+
def initialize(task_name, app)
|
49
|
+
super
|
50
|
+
@remote_actions = []
|
51
|
+
end
|
52
|
+
|
53
|
+
# Add a local action to this task. This calls Rake::Task#enhance.
|
54
|
+
alias_method :original_enhance, :enhance
|
55
|
+
|
56
|
+
# Add remote action +block+ to this task with dependencies +deps+. See
|
57
|
+
# Rake::Task#enhance.
|
58
|
+
def enhance(deps=nil, &block)
|
59
|
+
original_enhance(deps) # can't use super because block passed regardless.
|
60
|
+
@remote_actions << Action.new(self, block) if block_given?
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Execute this action. Local actions will be performed first, then remote
|
65
|
+
# actions will be performed in parallel on each host configured for this
|
66
|
+
# RemoteTask.
|
67
|
+
def execute(args = nil)
|
68
|
+
raise(Rake::RemoteTask::ConfigurationError,
|
69
|
+
"No target hosts specified on task #{self.name} for roles #{options[:roles].inspect}") if
|
70
|
+
! defined_target_hosts?
|
71
|
+
|
72
|
+
super args
|
73
|
+
|
74
|
+
@remote_actions.each { |act| act.execute(target_hosts, self, args) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Pulls +files+ from the +target_host+ using rsync to +local_directory+.
|
78
|
+
def get(local_directory, *files)
|
79
|
+
rsync(files.map { |f| "#{target_host}:#{f}" }, local_directory)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Copys a (usually generated) file to +remote_path+. Contents of block
|
83
|
+
# are copied to +remote_path+ and you may specify an optional
|
84
|
+
# base_name for the tempfile (aids in debugging).
|
85
|
+
def put(remote_path, base_name = File.basename(remote_path))
|
86
|
+
require 'tempfile'
|
87
|
+
Tempfile.open(base_name) do |fp|
|
88
|
+
fp.puts yield
|
89
|
+
fp.flush
|
90
|
+
rsync(fp.path, "#{target_host}:#{remote_path}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Use rsync to send +local+ to +remote+.
|
95
|
+
def rsync(local, remote)
|
96
|
+
cmd = [rsync_cmd, rsync_flags, local, remote].flatten.compact
|
97
|
+
cmdstr = cmd.join(' ')
|
98
|
+
|
99
|
+
warn cmdstr if $TRACE
|
100
|
+
|
101
|
+
success = system(*cmd)
|
102
|
+
|
103
|
+
unless success then
|
104
|
+
raise Rake::RemoteTask::CommandFailedError, "execution failed: #{cmdstr}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
|
109
|
+
# sudo password will be prompted for then saved for subsequent sudo commands.
|
110
|
+
def run(command)
|
111
|
+
cmd = [ssh_cmd, ssh_flags, target_host, command].flatten
|
112
|
+
result = []
|
113
|
+
|
114
|
+
trace = [ssh_cmd, ssh_flags, target_host, "'#{command}'"].flatten.join(' ')
|
115
|
+
warn trace if $TRACE
|
116
|
+
|
117
|
+
pid, inn, out, err = popen4(*cmd)
|
118
|
+
|
119
|
+
inn.sync = true
|
120
|
+
streams = [out, err]
|
121
|
+
out_stream = {
|
122
|
+
out => $stdout,
|
123
|
+
err => $stderr,
|
124
|
+
}
|
125
|
+
|
126
|
+
# Handle process termination ourselves
|
127
|
+
status = nil
|
128
|
+
Thread.start do
|
129
|
+
status = Process.waitpid2(pid).last
|
130
|
+
end
|
131
|
+
|
132
|
+
until streams.empty? do
|
133
|
+
# don't busy loop
|
134
|
+
selected, = select streams, nil, nil, 0.1
|
135
|
+
|
136
|
+
next if selected.nil? or selected.empty?
|
137
|
+
|
138
|
+
selected.each do |stream|
|
139
|
+
if stream.eof? then
|
140
|
+
streams.delete stream if status # we've quit, so no more writing
|
141
|
+
next
|
142
|
+
end
|
143
|
+
|
144
|
+
data = stream.readpartial(1024)
|
145
|
+
out_stream[stream].write data
|
146
|
+
|
147
|
+
if stream == err and data =~ sudo_prompt then
|
148
|
+
inn.puts sudo_password
|
149
|
+
data << "\n"
|
150
|
+
$stderr.write "\n"
|
151
|
+
end
|
152
|
+
|
153
|
+
result << data
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
unless status.success? then
|
158
|
+
raise(Rake::RemoteTask::CommandFailedError,
|
159
|
+
"execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
|
160
|
+
end
|
161
|
+
|
162
|
+
result.join
|
163
|
+
ensure
|
164
|
+
inn.close rescue nil
|
165
|
+
out.close rescue nil
|
166
|
+
err.close rescue nil
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns an Array with every host configured.
|
170
|
+
def self.all_hosts
|
171
|
+
hosts_for(roles.keys)
|
172
|
+
end
|
173
|
+
|
174
|
+
# The default environment values. Used for resetting (mostly for
|
175
|
+
# tests).
|
176
|
+
def self.default_env
|
177
|
+
@@default_env
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.per_thread
|
181
|
+
@@per_thread
|
182
|
+
end
|
183
|
+
|
184
|
+
# The remote task environment.
|
185
|
+
def self.env
|
186
|
+
@@env
|
187
|
+
end
|
188
|
+
|
189
|
+
# Fetches environment variable +name+ from the environment using
|
190
|
+
# default +default+.
|
191
|
+
def self.fetch(name, default = nil)
|
192
|
+
name = name.to_s if Symbol === name
|
193
|
+
if @@env.has_key? name then
|
194
|
+
protect_env(name) do
|
195
|
+
v = @@env[name]
|
196
|
+
v = @@env[name] = v.call if Proc === v unless per_thread[name]
|
197
|
+
v = v.call if Proc === v
|
198
|
+
v
|
199
|
+
end
|
200
|
+
elsif default || default == false
|
201
|
+
v = @@env[name] = default
|
202
|
+
else
|
203
|
+
raise Rake::RemoteTask::FetchError
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Fetches a set of options and turns it into a hash to be passed to
|
208
|
+
# the application specific delegate tasks.
|
209
|
+
def self.get_options_hash(*options)
|
210
|
+
options_hash = {}
|
211
|
+
options.each do |option|
|
212
|
+
option_value = Rake::RemoteTask.fetch(option, false)
|
213
|
+
options_hash[option] = option_value if option_value
|
214
|
+
end
|
215
|
+
options_hash
|
216
|
+
end
|
217
|
+
|
218
|
+
# Add host +host_name+ that belongs to +roles+. Extra arguments may
|
219
|
+
# be specified for the host as a hash as the last argument.
|
220
|
+
#
|
221
|
+
# host is the inversion of role:
|
222
|
+
#
|
223
|
+
# host 'db1.example.com', :db, :master_db
|
224
|
+
#
|
225
|
+
# Is equivalent to:
|
226
|
+
#
|
227
|
+
# role :db, 'db1.example.com'
|
228
|
+
# role :master_db, 'db1.example.com'
|
229
|
+
def self.host(host_name, *roles)
|
230
|
+
opts = Hash === roles.last ? roles.pop : {}
|
231
|
+
|
232
|
+
roles.each do |role_name|
|
233
|
+
role role_name, host_name, opts.dup
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns an Array of all hosts in +roles+.
|
238
|
+
def self.hosts_for(*roles)
|
239
|
+
roles.flatten.map { |r|
|
240
|
+
self.roles[r].keys
|
241
|
+
}.flatten.uniq.sort
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.mandatory(name, desc) # :nodoc:
|
245
|
+
self.set(name) do
|
246
|
+
raise(Rake::RemoteTask::ConfigurationError,
|
247
|
+
"Please specify the #{desc} via the #{name.inspect} variable")
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Ensures exclusive access to +name+.
|
252
|
+
def self.protect_env(name) # :nodoc:
|
253
|
+
@@env_locks[name].synchronize do
|
254
|
+
yield
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Adds a remote task named +name+ with options +options+ that will
|
259
|
+
# execute +block+.
|
260
|
+
def self.remote_task(name, *args, &block)
|
261
|
+
options = (Hash === args.last) ? args.pop : {}
|
262
|
+
t = Rake::RemoteTask.define_task(name, *args, &block)
|
263
|
+
options[:roles] = Array options[:roles]
|
264
|
+
options[:roles] |= @@current_roles
|
265
|
+
t.options = options
|
266
|
+
t
|
267
|
+
end
|
268
|
+
|
269
|
+
# Ensures +name+ does not conflict with an existing method.
|
270
|
+
def self.reserved_name?(name) # :nodoc:
|
271
|
+
!@@env.has_key?(name.to_s) && self.respond_to?(name)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Resets vlad, restoring all roles, tasks and environment variables
|
275
|
+
# to the defaults.
|
276
|
+
def self.reset
|
277
|
+
@@def_role_hash = {} # official default role value
|
278
|
+
@@env = {}
|
279
|
+
@@tasks = {}
|
280
|
+
@@roles = Hash.new { |h,k| h[k] = @@def_role_hash }
|
281
|
+
@@env_locks = Hash.new { |h,k| h[k] = Mutex.new }
|
282
|
+
|
283
|
+
@@default_env.each do |k,v|
|
284
|
+
case v
|
285
|
+
when Symbol, Fixnum, nil, true, false, 42 then # ummmm... yeah.
|
286
|
+
@@env[k] = v
|
287
|
+
else
|
288
|
+
@@env[k] = v.dup
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Adds role +role_name+ with +host+ and +args+ for that host.
|
294
|
+
def self.role(role_name, host = nil, args = {})
|
295
|
+
if block_given? then
|
296
|
+
raise ArgumentError, 'host not allowed with block' unless host.nil?
|
297
|
+
|
298
|
+
begin
|
299
|
+
current_roles << role_name
|
300
|
+
yield
|
301
|
+
ensure
|
302
|
+
current_roles.delete role_name
|
303
|
+
end
|
304
|
+
else
|
305
|
+
raise ArgumentError, 'host required' if host.nil?
|
306
|
+
|
307
|
+
[*host].each do |hst|
|
308
|
+
raise ArgumentError, "invalid host: #{hst}" if hst.nil? or hst.empty?
|
309
|
+
end
|
310
|
+
@@roles[role_name] = {} if @@def_role_hash.eql? @@roles[role_name]
|
311
|
+
@@roles[role_name][host] = args
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# The configured roles.
|
316
|
+
def self.roles
|
317
|
+
host domain, :app, :web, :db if @@roles.empty?
|
318
|
+
|
319
|
+
@@roles
|
320
|
+
end
|
321
|
+
|
322
|
+
# Set environment variable +name+ to +value+ or +default_block+.
|
323
|
+
#
|
324
|
+
# If +default_block+ is defined, the block will be executed the
|
325
|
+
# first time the variable is fetched, and the value will be used for
|
326
|
+
# every subsequent fetch.
|
327
|
+
def self.set(name, value = nil, &default_block)
|
328
|
+
raise ArgumentError, "cannot provide both a value and a block" if
|
329
|
+
value and default_block unless
|
330
|
+
value == :per_thread
|
331
|
+
raise ArgumentError, "cannot set reserved name: '#{name}'" if
|
332
|
+
Rake::RemoteTask.reserved_name?(name) unless $TESTING
|
333
|
+
|
334
|
+
name = name.to_s
|
335
|
+
|
336
|
+
Rake::RemoteTask.per_thread[name] = true if
|
337
|
+
default_block && value == :per_thread
|
338
|
+
|
339
|
+
Rake::RemoteTask.default_env[name] = Rake::RemoteTask.env[name] =
|
340
|
+
default_block || value
|
341
|
+
|
342
|
+
Object.send :define_method, name do
|
343
|
+
Rake::RemoteTask.fetch name
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Sets all the default values. Should only be called once. Use reset
|
348
|
+
# if you need to restore values.
|
349
|
+
def self.set_defaults
|
350
|
+
@@default_env ||= {}
|
351
|
+
@@per_thread ||= {}
|
352
|
+
self.reset
|
353
|
+
|
354
|
+
mandatory :repository, "repository path"
|
355
|
+
mandatory :deploy_to, "deploy path"
|
356
|
+
mandatory :domain, "server domain"
|
357
|
+
|
358
|
+
simple_set(:app_env, "production",
|
359
|
+
:deploy_timestamped, true,
|
360
|
+
:keep_releases, 5,
|
361
|
+
:rake_cmd, "rake",
|
362
|
+
:rsync_cmd, "rsync",
|
363
|
+
:rsync_flags, ['-azP', '--delete'],
|
364
|
+
:ssh_cmd, "ssh",
|
365
|
+
:ssh_flags, [],
|
366
|
+
:sudo_cmd, "sudo",
|
367
|
+
:sudo_flags, ['-p Password:'],
|
368
|
+
:sudo_prompt, /^Password:/)
|
369
|
+
|
370
|
+
set(:latest_release) { deploy_timestamped ? File.join(releases_path, releases[-1]) : releases_path }
|
371
|
+
set(:previous_release) do
|
372
|
+
if deploy_timestamped
|
373
|
+
if releases[-2]
|
374
|
+
File.join(releases_path, releases[-2])
|
375
|
+
else
|
376
|
+
nil
|
377
|
+
end
|
378
|
+
else
|
379
|
+
releases_path
|
380
|
+
end
|
381
|
+
end
|
382
|
+
set(:release_name) { deploy_timestamped ? Time.now.utc.strftime("%Y%m%d%H%M%S") : nil }
|
383
|
+
set(:release_path) { release_name ? File.join(releases_path, release_name) : releases_path }
|
384
|
+
set(:releases) { task.run("ls -x #{releases_path}").split.sort }
|
385
|
+
|
386
|
+
set_path :current_path, "current"
|
387
|
+
set_path :releases_path, "releases"
|
388
|
+
set_path :scm_path, "scm"
|
389
|
+
set_path :shared_path, "shared"
|
390
|
+
set_path :shared_config_path, File.join("shared", "config")
|
391
|
+
|
392
|
+
set(:sudo_password) do
|
393
|
+
state = `stty -g`
|
394
|
+
|
395
|
+
raise Rake::RemoteTask::Error, "stty(1) not found" unless $?.success?
|
396
|
+
|
397
|
+
begin
|
398
|
+
system "stty -echo"
|
399
|
+
$stdout.print "sudo password: "
|
400
|
+
$stdout.flush
|
401
|
+
sudo_password = $stdin.gets
|
402
|
+
$stdout.puts
|
403
|
+
ensure
|
404
|
+
system "stty #{state}"
|
405
|
+
end
|
406
|
+
sudo_password
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.set_path(name, subdir) # :nodoc:
|
411
|
+
set(name) { File.join(deploy_to, subdir) }
|
412
|
+
end
|
413
|
+
|
414
|
+
def self.simple_set(*args) # :nodoc:
|
415
|
+
args = Hash[*args]
|
416
|
+
args.each do |k, v|
|
417
|
+
set k, v
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# The Rake::RemoteTask executing in this Thread.
|
422
|
+
def self.task
|
423
|
+
Thread.current[:task]
|
424
|
+
end
|
425
|
+
|
426
|
+
# The configured Rake::RemoteTasks.
|
427
|
+
def self.tasks
|
428
|
+
@@tasks
|
429
|
+
end
|
430
|
+
|
431
|
+
# Execute +command+ under sudo using run.
|
432
|
+
def sudo(command)
|
433
|
+
run [sudo_cmd, sudo_flags, command].flatten.compact.join(" ")
|
434
|
+
end
|
435
|
+
|
436
|
+
# The hosts this task will execute on. The hosts are determined from
|
437
|
+
# the role this task belongs to.
|
438
|
+
#
|
439
|
+
# The target hosts may be overridden by providing a comma-separated
|
440
|
+
# list of commands to the HOSTS environment variable:
|
441
|
+
#
|
442
|
+
# rake my_task HOSTS=app1.example.com,app2.example.com
|
443
|
+
def target_hosts
|
444
|
+
if hosts = ENV["HOSTS"] then
|
445
|
+
hosts.strip.gsub(/\s+/, '').split(",")
|
446
|
+
else
|
447
|
+
roles = Array options[:roles]
|
448
|
+
|
449
|
+
if roles.empty? then
|
450
|
+
Rake::RemoteTask.all_hosts
|
451
|
+
else
|
452
|
+
Rake::RemoteTask.hosts_for roles
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# Similar to target_hosts, but returns true if user defined any hosts, even
|
458
|
+
# an empty list.
|
459
|
+
def defined_target_hosts?
|
460
|
+
return true if ENV["HOSTS"]
|
461
|
+
roles = Array options[:roles]
|
462
|
+
return true if roles.empty?
|
463
|
+
# borrowed from hosts_for:
|
464
|
+
roles.flatten.each { |r|
|
465
|
+
return true unless @@def_role_hash.eql? Rake::RemoteTask.roles[r]
|
466
|
+
}
|
467
|
+
return false
|
468
|
+
end
|
469
|
+
|
470
|
+
# Action is used to run a task's remote_actions in parallel on each
|
471
|
+
# of its hosts. Actions are created automatically in
|
472
|
+
# Rake::RemoteTask#enhance.
|
473
|
+
class Action
|
474
|
+
|
475
|
+
# The task this action is attached to.
|
476
|
+
attr_reader :task
|
477
|
+
|
478
|
+
# The block this action will execute.
|
479
|
+
attr_reader :block
|
480
|
+
|
481
|
+
# An Array of threads, one for each host this action executes on.
|
482
|
+
attr_reader :workers
|
483
|
+
|
484
|
+
# Creates a new Action that will run +block+ for +task+.
|
485
|
+
def initialize(task, block)
|
486
|
+
@task = task
|
487
|
+
@block = block
|
488
|
+
@workers = ThreadGroup.new
|
489
|
+
end
|
490
|
+
|
491
|
+
def ==(other) # :nodoc:
|
492
|
+
return false unless Action === other
|
493
|
+
block == other.block && task == other.task
|
494
|
+
end
|
495
|
+
|
496
|
+
# Execute this action on +hosts+ in parallel. Returns when block
|
497
|
+
# has completed for each host.
|
498
|
+
def execute(hosts, task, args)
|
499
|
+
hosts.each do |host|
|
500
|
+
t = task.clone
|
501
|
+
t.target_host = host
|
502
|
+
thread = Thread.new(t) do |task|
|
503
|
+
Thread.current.abort_on_exception = true
|
504
|
+
Thread.current[:task] = task
|
505
|
+
case block.arity
|
506
|
+
when 1
|
507
|
+
block.call task
|
508
|
+
else
|
509
|
+
block.call task, args
|
510
|
+
end
|
511
|
+
Thread.current[:task] = nil
|
512
|
+
end
|
513
|
+
@workers.add thread
|
514
|
+
end
|
515
|
+
@workers.list.each { |thr| thr.join }
|
516
|
+
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
Rake::RemoteTask.set_defaults
|