# Capistrano2 differentiator load 'deploy' if respond_to?(:namespace) Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } # Required gems/libraries require 'rubygems' require 'ash/common' require 'capistrano/ext/multistage' # Bootstrap Capistrano instance configuration = Capistrano::Configuration.respond_to?(:instance) ? Capistrano::Configuration.instance(:must_exist) : Capistrano.configuration(:must_exist) configuration.load do # Set default stages set :stages, %w(staging production) set :default_stage, "staging" # -------------------------------------------- # Task chains # -------------------------------------------- after "deploy:setup", "deploy:setup_shared" after "deploy:setup_shared", "deploy:setup_backup" after "deploy:finalize_update", "ash:htaccess" after "deploy", "deploy:cleanup" # -------------------------------------------- # Default variables # -------------------------------------------- # SSH set :user, proc{text_prompt("SSH username: ")} set :password, proc{Capistrano::CLI.password_prompt("SSH password for '#{user}':")} # Database set :dbuser, proc{text_prompt("Database username: ")} set :dbpass, proc{Capistrano::CLI.password_prompt("Database password for '#{dbuser}':")} set :dbname, proc{text_prompt("Database name: ")} _cset :mysqldump, "mysqldump" _cset :dump_options, "--single-transaction --create-options --quick" # Source Control set :group_writable, false set :use_sudo, false set :scm, :subversion set :scm_verbose, true set :scm_username, proc{text_prompt("Subversion username: ")} set :scm_password, proc{Capistrano::CLI.password_prompt("Subversion password for '#{scm_username}': ")} set :keep_releases, 3 set :deploy_via, :remote_cache set :copy_strategy, :checkout set :copy_compression, :bz2 set :copy_exclude, [".svn", ".git*", ".DS_Store", "*.sample", "LICENSE*", "Capfile", "RELEASE*", "*.rb", "*.sql", "nbproject", "_template"] # phpMyAdmin version set :pma_version, "3.4.5" # Backups Path _cset(:backups_path) { File.join(deploy_to, "backups") } _cset(:tmp_backups_path) { File.join("#{backups_path}", "tmp") } _cset(:backups) { capture("ls -x #{backups_path}", :except => { :no_release => true }).split.sort } # Define which files or directories you want to exclude from being backed up _cset(:backup_exclude) { [] } set :exclude_string, '' # show password requests on windows # (http://weblog.jamisbuck.org/2007/10/14/capistrano-2-1) default_run_options[:pty] = true # Database migration settings set :db_local_host, "192.168.16.116" set :db_local_user, "developer" set :db_local_name, proc{text_prompt("Local database name: #{db_local_name}: ")} set :db_local_pass, proc{text_prompt("Local database password for: #{db_local_user}: ")} set :db_remote_user, proc{text_prompt("Remote database user: #{db_remote_user}: ")} set :db_remote_pass, proc{text_prompt("Remote database password for: #{db_remote_user}: ")} set :db_remote_name, proc{text_prompt("Remote database name: #{db_remote_name}: ")} set :db_remote_host, "localhost" # Database replacement values # Format: local => remote set :db_regex_hash, { } # -------------------------------------------- # Overloaded tasks # -------------------------------------------- namespace :deploy do desc <<-DESC Prepares one or more servers for deployment. Before you can use any \ of the Capistrano deployment tasks with your project, you will need to \ make sure all of your servers have been prepared with `cap deploy:setup'. When \ you add a new server to your cluster, you can easily run the setup task \ on just that server by specifying the HOSTS environment variable: $ cap HOSTS=new.server.com deploy:setup It is safe to run this task on servers that have already been set up; it \ will not destroy any deployed revisions or data. DESC task :setup, :except => { :no_release => true } do dirs = [deploy_to, releases_path, shared_path] dirs += shared_children.map { |d| File.join(shared_path, d.split('/').last) } run "#{try_sudo} mkdir -p #{dirs.join(' ')}" run "#{try_sudo} chmod 755 #{dirs.join(' ')}" if fetch(:group_writable, true) end desc "Setup shared application directories and permissions after initial setup" task :setup_shared do puts "STUB: Setup" end desc "Setup backup directory for database and web files" task :setup_backup, :except => { :no_release => true } do run "#{try_sudo} mkdir -p #{backups_path} #{tmp_backups_path} && #{try_sudo} chmod 755 #{backups_path}" end desc <<-DESC Deprecated API. This has become deploy:create_symlink, please update your recipes DESC task :symlink, :except => { :no_release => true } do logger.important "[Deprecation Warning] This API has changed, please hook `deploy:create_symlink` instead of `deploy:symlink`." create_symlink end desc <<-DESC Clean up old releases. By default, the last 5 releases are kept on each \ server (though you can change this with the keep_releases variable). All \ other deployed revisions are removed from the servers. By default, this \ will use sudo to clean up the old releases, but if sudo is not available \ for your environment, set the :use_sudo variable to false instead. \ Overridden to set/reset file and directory permissions DESC task :cleanup, :except => { :no_release => true } do count = fetch(:keep_releases, 5).to_i local_releases = capture("ls -xt #{releases_path}").split.reverse if count >= local_releases.length logger.important "no old releases to clean up" else logger.info "keeping #{count} of #{local_releases.length} deployed releases" directories = (local_releases - local_releases.last(count)).map { |release| File.join(releases_path, release) }.join(" ") directories.split(" ").each do |dir| # adding a chown -R method to fix permissions on the directory # this should help with issues related to permission denied # as in issues #28 and #30 try_sudo "chown -R #{user}:#{user} #{dir}" set_perms_dirs(dir) set_perms_files(dir) end try_sudo "rm -rf #{directories}" end end end # -------------------------------------------- # Ash tasks # -------------------------------------------- namespace :ash do desc "Set standard permissions for Ash servers" task :fixperms, :roles => :web, :except => { :no_release => true } do # chmod the files and directories. set_perms_dirs("#{latest_release}") set_perms_files("#{latest_release}") end desc "Test: Task used to verify Capistrano is working. Prints operating system name." task :uname do run "uname -a" end desc "Test: Task used to verify Capistrano is working. Prints environment of Capistrano user." task :getpath do run "echo $PATH" end desc 'Copy distribution htaccess file' task :htaccess, :roles => :web do case true when remote_file_exists?("#{latest_release}/htaccess.#{stage}.dist") run "mv #{latest_release}/htaccess.#{stage}.dist #{latest_release}/.htaccess" when remote_file_exists?("#{latest_release}/htaccess.#{stage}") run "mv #{latest_release}/htaccess.#{stage} #{latest_release}/.htaccess" when remote_file_exists?("#{latest_release}/htaccess.dist") run "mv #{latest_release}/htaccess.dist #{latest_release}/.htaccess" else logger.important "Failed to move the .htaccess file in #{latest_release} because an unknown pattern was used" end end end # -------------------------------------------- # PHP tasks # -------------------------------------------- namespace :php do namespace :apc do desc "Disable the APC administrative panel" task :disable, :roles => :web, :except => { :no_release => true } do run "rm #{current_path}/apc.php" end desc "Enable the APC administrative panel" task :enable, :roles => :web, :except => { :no_release => true } do run "ln -s /usr/local/lib/php/apc.php #{current_path}/apc.php" end end end # -------------------------------------------- # Remote/Local database migration tasks # -------------------------------------------- namespace :db do desc "Migrate remote application database to local server" task :to_local, :roles => :db, :except => { :no_release => true } do remote_export remote_download local_import end desc "Migrate local application database to remote server" task :to_remote, :roles => :db, :except => { :no_release => true } do local_export local_upload remote_import end desc "Handles importing a MySQL database dump file. Uncompresses the file, does regex replacements, and imports." task :local_import, :roles => :db do # check for compressed file and decompress if local_file_exists?("#{db_remote_name}.sql.gz") system "gunzip -f #{db_remote_name}.sql.gz" end if local_file_exists?("#{db_remote_name}.sql") # run through replacements on SQL file db_regex_hash.each_pair do |local, remote| system "perl -pi -e 's/#{remote}/#{local}/g' #{db_remote_name}.sql" end # import into database system "mysql -h#{db_local_host} -u#{db_local_user} -p#{db_local_pass} #{db_local_name} < #{db_remote_name}.sql" # remove used file run "rm -f #{deploy_to}/#{db_remote_name}.sql.gz" system "rm -f #{db_remote_name}.sql" end end task :local_export do mysqldump = fetch(:mysqldump, "mysqldump") dump_options = fetch(:dump_options, "--single-transaction --create-options --quick") system "#{mysqldump} #{dump_options} --opt -h#{db_local_host} -u#{db_local_user} -p#{db_local_pass} #{db_local_name} | gzip -c --best > #{db_local_name}.sql.gz" end desc "Upload locally created MySQL dumpfile to remote server via SCP" task :local_upload, :roles => :db do upload "#{db_local_name}.sql.gz", "#{deploy_to}/#{db_local_name}.sql.gz", :via => :scp end desc "Handles importing a MySQL database dump file. Uncompresses the file, does regex replacements, and imports." task :remote_import, :roles => :db do # check for compressed file and decompress if remote_file_exists?("#{deploy_to}/#{db_local_name}.sql.gz") run "gunzip -f #{deploy_to}/#{db_local_name}.sql.gz" end if remote_file_exists?("#{deploy_to}/#{db_local_name}.sql") # run through replacements on SQL file db_regex_hash.each_pair do |local, remote| run "perl -pi -e 's/#{local}/#{remote}/g' #{deploy_to}/#{db_local_name}.sql" end # import into database run "mysql -h#{db_remote_host} -u#{db_remote_user} -p#{db_remote_pass} #{db_remote_name} < #{deploy_to}/#{db_local_name}.sql" # remove used file run "rm -f #{deploy_to}/#{db_local_name}.sql" system "rm -rf #{db_local_name}.sql.gz" end end desc "Create a compressed MySQL dumpfile of the remote database" task :remote_export, :roles => :db do mysqldump = fetch(:mysqldump, "mysqldump") dump_options = fetch(:dump_options, "--single-transaction --create-options --quick") run "#{mysqldump} #{dump_options} --opt -h#{db_remote_host} -u#{db_remote_user} -p#{db_remote_pass} #{db_remote_name} | gzip -c --best > #{deploy_to}/#{db_remote_name}.sql.gz" end desc "Download remotely created MySQL dumpfile to local machine via SCP" task :remote_download, :roles => :db do download "#{deploy_to}/#{db_remote_name}.sql.gz", "#{db_remote_name}.sql.gz", :via => :scp end end # -------------------------------------------- # phpMyAdmin tasks # -------------------------------------------- namespace :pma do desc "Disable the phpMyAdmin utility" task :disable, :roles => :web, :except => { :no_release => true } do run "rm -f #{current_path}/pma" end desc "Enable the phpMyAdmin utility" task :enable, :roles => :web, :except => { :no_release => true } do run "ln -s #{shared_path}/pma #{current_path}/pma" end desc "Install phpMyAdmin utility" task :install, :roles => :web do # fetch PMA run "wget -O #{shared_path}/pma.tar.gz http://downloads.sourceforge.net/project/phpmyadmin/phpMyAdmin/#{pma_version}/phpMyAdmin-#{pma_version}-english.tar.gz" # decompress and install run "tar -zxf #{shared_path}/pma.tar.gz -C #{shared_path}" run "mv #{shared_path}/phpMyAdmin-#{pma_version}-english/ #{shared_path}/pma/" run "rm -f #{shared_path}/pma.tar.gz" end end # -------------------------------------------- # Backup tasks # -------------------------------------------- namespace :backup do desc "Perform a backup of web and database files" task :default do deploy.setup_backup db web cleanup end desc <<-DESC Requires the rsync package to be installed. Performs a file-level backup of the application and any assets \ from the shared directory that have been symlinked into the \ applications root or sub-directories. You can specify which files or directories to exclude from being \ backed up (i.e., log files, sessions, cache) by setting the \ :backup_exclude variable set(:backup_exclude) { [ "var/", "tmp/", logs/debug.log ] } DESC task :web, :roles => :web do if previous_release puts "Backing up web files (user uploaded content and previous release)" if !backup_exclude.nil? && !backup_exclude.empty? logger.debug "processing backup exclusions..." backup_exclude.each do |pattern| exclude_string << "--exclude '#{pattern}' " end logger.debug "Exclude string = #{exclude_string}" end # Copy the previous release to the /tmp directory logger.debug "Copying previous release to the #{tmp_backups_path}/#{release_name} directory" run "rsync -avzrtpL #{exclude_string} #{current_path}/ #{tmp_backups_path}/#{release_name}/" # -------------------------- # SET/RESET PERMISSIONS # -------------------------- set_perms_dirs("#{tmp_backups_path}/#{release_name}", 755) set_perms_files("#{tmp_backups_path}/#{release_name}", 644) # create the tarball of the previous release set :archive_name, "release_B4_#{release_name}.tar.gz" logger.debug "Creating a Tarball of the previous release in #{backups_path}/#{archive_name}" run "cd #{tmp_backups_path} && tar -cvpf - ./#{release_name}/ | gzip -c --best > #{backups_path}/#{archive_name}" # remove the the temporary copy logger.debug "Removing the tempory copy" run "rm -rf #{tmp_backups_path}/#{release_name}" else logger.important "no previous release to backup; backup of files skipped" end end desc "Perform a backup of database files" task :db, :roles => :db do if previous_release mysqldump = fetch(:mysqldump, "mysqldump") dump_options = fetch(:dump_options, "--single-transaction --create-options --quick") puts "Backing up the database now and putting dump file in the previous release directory" # define the filename (include the current_path so the dump file will be within the directory) filename = "#{current_path}/#{dbname}_dump-#{Time.now.to_s.gsub(/ /, "_")}.sql.gz" # dump the database for the proper environment run "#{mysqldump} #{dump_options} -u #{dbuser} -p #{dbname} | gzip -c --best > #{filename}" do |ch, stream, out| ch.send_data "#{dbpass}\n" if out =~ /^Enter password:/ end else logger.important "no previous release to backup to; backup of database skipped" end end desc <<-DESC Clean up old backups. By default, the last 10 backups are kept on each \ server (though you can change this with the keep_backups variable). All \ other backups are removed from the servers. By default, this \ will use sudo to clean up the old backups, but if sudo is not available \ for your environment, set the :use_sudo variable to false instead. DESC task :cleanup, :except => { :no_release => true } do count = fetch(:keep_backups, 10).to_i if count >= backups.length logger.important "no old backups to clean up" else logger.info "keeping #{count} of #{backups.length} backups" archives = (backups - backups.last(count)).map { |backup| File.join(backups_path, backup) }.join(" ") # fix permissions on the the files and directories before removing them archives.split(" ").each do |backup| set_perms_dirs("#{backup}", 755) set_perms_files("#{backup}", 644) end try_sudo "rm -rf #{archives}" end end end # -------------------------------------------- # Remote File/Directory test tasks # -------------------------------------------- namespace :remote do namespace :file do desc "Test: Task to test existence of missing file" task :missing do if remote_file_exists?('/dev/mull') logger.info "FAIL - Why does the '/dev/mull' path exist???" else logger.info "GOOD - Verified the '/dev/mull' path does not exist!" end end desc "Test: Task used to test existence of a present file" task :exists do if remote_file_exists?('/dev/null') logger.info "GOOD - Verified the '/dev/null' path exists!" else logger.info "FAIL - WHAT happened to the '/dev/null' path???" end end end namespace :dir do desc "Test: Task to test existence of missing dir" task :missing do if remote_dir_exists?('/etc/fake_dir') logger.info "FAIL - Why does the '/etc/fake_dir' dir exist???" else logger.info "GOOD - Verified the '/etc/fake_dir' dir does not exist!" end end desc "Test: Task used to test existence of an existing directory" task :exists do if remote_dir_exists?('/etc') logger.info "GOOD - Verified the '/etc' dir exists!" else logger.info "FAIL - WHAT happened to the '/etc' dir???" end end end end end