# This file is part of EC2 on Rails. # http://rubyforge.org/projects/ec2onrails/ # # Copyright 2007 Paul Dowman, http://pauldowman.com/ # # EC2 on Rails is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # EC2 on Rails is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . require 'fileutils' include FileUtils require 'tmpdir' require 'pp' require 'zlib' require 'archive/tar/minitar' include Archive::Tar require 'ec2onrails/version' require 'ec2onrails/capistrano_utils' include Ec2onrails::CapistranoUtils Capistrano::Configuration.instance.load do unless ec2onrails_config raise "ec2onrails_config variable not set. (It should be a hash.)" end cfg = ec2onrails_config #:apache or :nginx cfg[:web_proxy_server] ||= :apache set :ec2onrails_version, Ec2onrails::VERSION::STRING set :image_id_32_bit, Ec2onrails::VERSION::AMI_ID_32_BIT set :image_id_64_bit, Ec2onrails::VERSION::AMI_ID_64_BIT set :deploy_to, "/mnt/app" set :use_sudo, false set :user, "app" #in case any changes were made to the configs, like changing the number of mongrels before "deploy:cold", "ec2onrails:server:grant_sudo_access" after "deploy:symlink", "ec2onrails:server:set_roles", "ec2onrails:server:init_services" after "deploy:cold", "ec2onrails:db:init_backup", "ec2onrails:db:optimize", "ec2onrails:server:restrict_sudo_access" after "ec2onrails:server:install_gems", "ec2onrails:server:add_gem_sources" # override default start/stop/restart tasks namespace :deploy do desc <<-DESC Overrides the default Capistrano deploy:start, uses \ 'god start app' DESC task :start, :roles => :app do sudo "god start app" # sudo "god monitor app" end desc <<-DESC Overrides the default Capistrano deploy:stop, uses \ 'god stop app' DESC task :stop, :roles => :app do # sudo "god unmonitor app" sudo "god stop app" end desc <<-DESC Overrides the default Capistrano deploy:restart, uses \ 'god restart app' DESC task :restart, :roles => :app do sudo "god restart app" end end namespace :ec2onrails do desc <<-DESC Show the AMI id's of the current images for this version of \ EC2 on Rails. DESC task :ami_ids do puts "32-bit server image for EC2 on Rails #{ec2onrails_version}: #{image_id_32_bit}" puts "64-bit server image for EC2 on Rails #{ec2onrails_version}: #{image_id_64_bit}" end desc <<-DESC Copies the public key from the server using the external "ssh" command because Net::SSH, which is used by Capistrano, needs it. This will only work if you have an ssh command in the path. If Capistrano can successfully connect to your EC2 instance you don't need to do this. It will copy from the first server in the :app role, this can be overridden by specifying the HOST environment variable DESC task :get_public_key_from_server do host = find_servers_for_task(current_task).first.host privkey = ssh_options[:keys][0] pubkey = "#{privkey}.pub" msg = <<-MSG Your first key in ssh_options[:keys] is #{privkey}, presumably that's your EC2 private key. The public key will be copied from the server named '#{host}' and saved locally as #{pubkey}. Continue? [y/n] MSG choice = nil while choice != "y" && choice != "n" choice = Capistrano::CLI.ui.ask(msg).downcase msg = "Please enter 'y' or 'n'." end if choice == "y" run_local "scp -i '#{privkey}' app@#{host}:.ssh/authorized_keys #{pubkey}" end end desc <<-DESC Prepare a newly-started instance for a cold deploy. DESC task :setup do server.set_mail_forward_address server.set_timezone server.install_packages server.install_gems server.deploy_files server.setup_web_proxy server.set_roles server.enable_ssl if cfg[:enable_ssl] server.set_rails_env server.restart_services deploy.setup db.create server.harden_server db.enable_ebs end desc <<-DESC Deploy and restore database from S3 DESC task :restore_db_and_deploy do db.recreate deploy.update_code deploy.symlink db.restore deploy.migrations end namespace :ec2 do desc <<-DESC DESC task :configure_firewall do # TODO end end namespace :db do desc <<-DESC [internal] Load configuration info for the database from config/database.yml, and start mysql (it must be running in order to interact with it). DESC task :load_config do unless hostnames_for_role(:db, :primary => true).empty? db_config = YAML::load(ERB.new(File.read("config/database.yml")).result)[rails_env.to_s] || {} cfg[:db_name] ||= db_config['database'] cfg[:db_user] ||= db_config['username'] || db_config['user'] cfg[:db_password] ||= db_config['password'] cfg[:db_host] ||= db_config['host'] cfg[:db_port] ||= db_config['port'] cfg[:db_socket] ||= db_config['socket'] if (cfg[:db_host].nil? || cfg[:db_host].empty?) && (cfg[:db_socket].nil? || cfg[:db_socket].empty?) raise "ERROR: missing database config. Make sure database.yml contains a '#{rails_env}' section with either 'host: hostname' or 'socket: /var/run/mysqld/mysqld.sock'." end [cfg[:db_name], cfg[:db_user], cfg[:db_password]].each do |s| if s.nil? || s.empty? raise "ERROR: missing database config. Make sure database.yml contains a '#{rails_env}' section with a database name, user, and password." elsif s.match(/['"]/) raise "ERROR: database config string '#{s}' contains quotes." end end end end desc <<-DESC Create the MySQL database. Assumes there is no MySQL root \ password. To create a MySQL root password create a task that's run \ after this task using an after hook. DESC task :create, :roles => :db do on_rollback { drop } load_config start # remove the default test database, though sometimes it doesn't exist (perhaps it isn't there anymore?) run %{mysql -u root -e "drop database if exists test; flush privileges;"} # removing anonymous mysql accounts run %{mysql -u root -D mysql -e "delete from db where User = ''; flush privileges;"} run %{mysql -u root -D mysql -e "delete from user where User = ''; flush privileges;"} run %{mysql -u root -e "create database if not exists #{cfg[:db_name]};"} run %{mysql -u root -e "grant all on #{cfg[:db_name]}.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"} run %{mysql -u root -e "grant reload on *.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"} run %{mysql -u root -e "grant super on *.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"} end desc <<-DESC Move the MySQL database to Amazon's Elastic Block Store (EBS), \ which is a persistant data store for the cloud. OPTIONAL PARAMETERS: * SIZE: Pass in num in gigs, like 10, to set the size, otherwise it will \ default to 10 gigs. * VOLUME_ID: The volume_id to use for the mysql database NOTE: keep track of the volume ID, as you'll want to keep this for your \ records and probably add it to the :db role in your deploy.rb file \ (see the ec2onrails sample deploy.rb file for additional information) DESC task :enable_ebs, :roles => :db, :only => { :primary => true } do # based off of Eric's work: # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1663&categoryID=100 # # EXPLAINATION: # There is a lot going on here! At the end, the setup should be: # * create EBS volume if run outside of the ec2onrails:setup and # VOLUME_ID is not passed in when the cap task is called # * EBS volume attached to /dev/sdh # * format to xfs if new or do a xfs_check if previously existed # * mounted on /var/local and update /etc/fstab # * move /mnt/mysql_data -> /var/local/mysql_data # * move /mnt/log/mysql -> /var/local/log/mysql # * change mysql configs by writing /etc/mysql/conf.d/mysql-ec2-ebs.cnf # * keep a copy of the mysql configs with the EBS volume, and if that volume is hooked into # another instance, make sure the mysql configs that go with that volume are symlinked to /etc/mysql # * update the file locations of the mysql binary logs in /mnt/log/mysql/mysql-bin.index # * symlink the moved folders to their old position... makes the move to EBS transparent # * Amazon doesn't contain EBS information in the meta-data API (yet). So write # /etc/ec2onrails/ebs_info.yml # to contain the meta-data information that we need # # DESIGN CONSIDERATIONS # * only moving mysql data to EBS. seems the most obvious, and if we move over other components # we will have to share that bandwidth (1 Gbps pipe to SAN). So limiting to what we really need # * not moving all mysql logic over (tmp scratch space stays local). Again, this is to limit # unnecessary bandwidth usage, PLUS, we are charged per million IO to EBS # # TODO: # * make sure if we have a predefined ebs_vol_id, that we error out with a nice msg IF the zones do not match # * can we move more of the mysql cache files back to the local disk and off of EBS, like the innodb table caches? # * right now we force this task to only be run on one server; that works for db :primary => true # But what is the best way to make this work if it needs to setup multiple servers (like db slaves)? # I need to figure out how to do a direct mapping from a server definition to a ebs_vol_id # * when we enable slaves and we setup ebs volumes on them, make it transparent to the user. # have the slave create a snapshot of the db.master volume, and then use that to mount from # * need to do a rollback that if the volume is created but something fails, lets uncreate it? # carefull though! If it fails towards the end when information is copied over, it could cause information # to be lost! # mysql_dir_root = '/var/local' block_mnt = '/dev/sdh' servers = find_servers_for_task(current_task) if servers.empty? raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched" elsif servers.size > 1 raise Capistrano::Error, "`#{task.fully_qualified_name}' is can only be run on one server, not #{server.size}" end vol_id = ENV['VOLUME_ID'] || servers.first.options[:ebs_vol_id] #HACK! capistrano doesn't allow arguments to be passed in if we call this task as a method, like 'db.enable_ebs' # the places where we do call it like that, we don't want to force a move to ebs, so.... # if the call frame is > 1 (ie, another task called it), do NOT force the ebs move no_force = task_call_frames.size > 1 prev_created = !(vol_id.nil? || vol_id.empty?) #no vol_id was passed in, but perhaps it is already mounted...? prev_created = true if !quiet_capture("mount | grep -inr '#{mysql_dir_root}' || echo ''").empty? unless no_force && (vol_id.nil? || vol_id.empty?) zone = quiet_capture("/usr/local/ec2onrails/bin/ec2_meta_data.rb -key 'placement/availability-zone'") instance_id = quiet_capture("/usr/local/ec2onrails/bin/ec2_meta_data.rb -key 'instance-id'") unless prev_created puts "creating new ebs volume...." size = ENV["SIZE"] || "10" cmd = "ec2-create-volume -s #{size} -z #{zone} 2>&1" puts "running: #{cmd}" output = `#{cmd}` puts output vol_id = (output =~ /^VOLUME\t(.+?)\t/ && $1) puts "NOTE: remember that vol_id" sleep(2) end vol_id.strip! if vol_id if quiet_capture("mount | grep -inr '#{block_mnt}' || echo ''").empty? cmd = "ec2-attach-volume -d #{block_mnt} -i #{instance_id} #{vol_id} 2>&1" puts "running: #{cmd}" output = `#{cmd}` puts output if output =~ /Client.InvalidVolume.ZoneMismatch/i raise Exception, "The volume you are trying to attach does not reside in the zone of your instance. Stopping!" end sleep(10) end ec2onrails.server.allow_sudo do # try to format the volume... if it is already formatted, lets run a check on # it to make sure it is ok, and then continue on # if errors, the device is busy...something else is going on here and it is already mounted... skip! if prev_created quiet_capture("sudo umount #{mysql_dir_root}") #unmount if need to sudo "xfs_check #{block_mnt}" else sudo "mkfs.xfs #{block_mnt}" end # if not added to /etc/fstab, lets do so sudo "sh -c \"grep -iqn '#{mysql_dir_root}' /etc/fstab || echo '#{block_mnt} #{mysql_dir_root} xfs noatime 0 0' >> /etc/fstab\"" sudo "mkdir -p #{mysql_dir_root}" #if not already mounted, lets mount it sudo "sh -c \"mount | grep -iqn '#{mysql_dir_root}' || mount '#{mysql_dir_root}'\"" #ok, now lets move the mysql stuff off of /mnt -> mysql_dir_root stop rescue nil #already stopped sudo "mkdir -p #{mysql_dir_root}/log" #move the data over, but keep a symlink to the new location for backwards compatability #and do not do it if /mnt/mysql_data has already been moved quiet_capture("sudo sh -c 'test ! -d #{mysql_dir_root}/mysql_data && mv /mnt/mysql_data #{mysql_dir_root}/'") sudo "mv /mnt/mysql_data /mnt/mysql_data_old 2>/dev/null || echo" sudo "ln -fs #{mysql_dir_root}/mysql_data /mnt/mysql_data" #but keep the tmpdir on mnt sudo "sh -c 'mkdir -p /mnt/tmp/mysql && chown mysql:mysql /mnt/tmp/mysql'" #move the logs over, but keep a symlink to the new location for backwards compatability #and do not do it if the logs have already been moved sudo("sudo sh -c 'test ! -d #{mysql_dir_root}/log/mysql_data && mv /mnt/log/mysql #{mysql_dir_root}/log/'") sudo "ln -fs #{mysql_dir_root}/log/mysql /mnt/log/mysql" quiet_capture("sudo sh -c \"test -f #{mysql_dir_root}/log/mysql/mysql-bin.index && \ perl -pi -e 's%/mnt/log/%#{mysql_dir_root}/log/%' #{mysql_dir_root}/log/mysql/mysql-bin.index\"") rescue false if quiet_capture("test -d /var/local/etc/mysql && echo 'yes'").empty? txt = <<-FILE [mysqld] datadir = #{mysql_dir_root}/mysql_data tmpdir = /mnt/tmp/mysql log_bin = #{mysql_dir_root}/log/mysql/mysql-bin.log log_slow_queries = #{mysql_dir_root}/log/mysql/mysql-slow.log FILE put txt, '/tmp/mysql-ec2-ebs.cnf' sudo 'mv /tmp/mysql-ec2-ebs.cnf /etc/mysql/conf.d/mysql-ec2-ebs.cnf' #keep a copy sudo "rsync -aR /etc/mysql #{mysql_dir_root}/" end # lets use the mysql configs on the EBS volume sudo "mv /etc/mysql /etc/mysql.orig 2>/dev/null" sudo "ln -sf #{mysql_dir_root}/etc/mysql /etc/mysql" #just put a README on the drive so we know what this volume is for! txt = <<-FILE This volume is setup to be used by Ec2onRails for primary MySql database persistence. RAILS_ENV: #{fetch(:rails_env, 'undefined')} DOMAIN: #{fetch(:domain, 'undefined')} Modify this volume at your own risk FILE put txt, "/tmp/VOLUME-README" sudo "mv /tmp/VOLUME-README #{mysql_dir_root}/VOLUME-README" #update the list of ebs volumes #TODO: abstract this away into a helper method!! ebs_info = quiet_capture("cat /etc/ec2onrails/ebs_info.yml") ebs_info = ebs_info.empty? ? {} : YAML::load(ebs_info) ebs_info[mysql_dir_root] = {'block_loc' => block_mnt, 'volume_id' => vol_id} put(ebs_info.to_yaml, "/tmp/ebs_info.yml") sudo "mv /tmp/ebs_info.yml /etc/ec2onrails/ebs_info.yml" #lets start it back up start end #end of sudo end end desc <<-DESC [internal] Make sure the MySQL server has been started, just in case the db role hasn't been set, e.g. when called from ec2onrails:setup. (But don't enable monitoring on it.) DESC task :start, :roles => :db do sudo "god start db" # sudo "god monitor db" end task :stop, :roles => :db do # sudo "god unmonitor db" sudo "god stop db" end desc <<-DESC Drop the MySQL database. Assumes there is no MySQL root \ password. If there is a MySQL root password, create a task that removes \ it and run that task before this one using a before hook. DESC task :drop, :roles => :db do load_config run %{mysql -u root -e "drop database if exists #{cfg[:db_name]};"} end desc <<-DESC db:drop and db:create. DESC task :recreate, :roles => :db do drop create end desc <<-DESC Set a root password for MySQL, using the variable mysql_root_password \ if it is set. If this is done db:drop won't work. DESC task :set_root_password, :roles => :db do if cfg[:mysql_root_password] run %{mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('#{cfg[:mysql_root_password]}') WHERE User='root'; FLUSH PRIVILEGES;"} end end desc <<-DESC Dump the MySQL database to the S3 bucket specified by \ ec2onrails_config[:archive_to_bucket]. The filename will be \ "database-archive//dump.sql.gz". DESC task :archive, :roles => :db do run "/usr/local/ec2onrails/bin/backup_app_db.rb --bucket #{cfg[:archive_to_bucket]} --dir #{cfg[:archive_to_bucket_subdir]}" end desc <<-DESC Restore the MySQL database from the S3 bucket specified by \ ec2onrails_config[:restore_from_bucket]. The archive filename is \ expected to be the default, "mysqldump.sql.gz". DESC task :restore, :roles => :db do run "/usr/local/ec2onrails/bin/restore_app_db.rb --bucket #{cfg[:restore_from_bucket]} --dir #{cfg[:restore_from_bucket_subdir]}" end desc <<-DESC [internal] Initialize the default backup folder on S3 (i.e. do a full backup of the newly-created db so the automatic incremental backups make sense). DESC task :init_backup, :roles => :db do server.allow_sudo do sudo "/usr/local/ec2onrails/bin/backup_app_db.rb --reset" end end # do NOT run if the flag does not exist. This is placed by a startup script # and it is only run on the first-startup. This means after the db has been # optimized, this task will not work again. # # Of course you can overload it or call the file directly task :optimize, :roles => :db do if !quiet_capture("test -e /tmp/optimize_db_flag && echo 'file exists'").empty? begin sudo "/usr/local/ec2onrails/bin/optimize_mysql.rb" ensure sudo "rm -rf /tmp/optimize_db_flag" #remove so we cannot run again end else puts "skipping as it looks like this task has already been run" end end end namespace :server do desc <<-DESC Tell the servers what roles they are in. This configures them with \ the appropriate settings for each role, and starts and/or stops the \ relevant services. DESC task :set_roles do # TODO generate this based on the roles that actually exist so arbitrary new ones can be added roles = { :web => hostnames_for_role(:web), :app => hostnames_for_role(:app), :db_primary => hostnames_for_role(:db, :primary => true), # doing th ebelow can cause errors elsewhere unless :db is populated. # :db => hostnames_for_role(:db), :memcache => hostnames_for_role(:memcache) } roles_yml = YAML::dump(roles) put roles_yml, "/tmp/roles.yml" server.allow_sudo do sudo "cp /tmp/roles.yml /etc/ec2onrails" #we want everyone to be able to read to it sudo "chmod a+r /etc/ec2onrails/roles.yml" sudo "/usr/local/ec2onrails/bin/set_roles.rb" end end task :init_services do server.allow_sudo do sudo "/usr/local/ec2onrails/bin/init_services.rb" end end task :setup_web_proxy, :roles => :web do sudo "/usr/local/ec2onrails/bin/setup_web_proxy.rb --mode #{cfg[:web_proxy_server].to_s}" end desc <<-DESC Change the default value of RAILS_ENV on the server. Technically this changes the server's mongrel config to use a different value for "environment". The value is specified in :rails_env. Be sure to do deploy:restart after this. DESC task :set_rails_env do rails_env = fetch(:rails_env, "production") sudo "/usr/local/ec2onrails/bin/set_rails_env #{rails_env}" end desc <<-DESC Upgrade to the newest versions of all Ubuntu packages. DESC task :upgrade_packages do sudo "aptitude -q update" sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y safe-upgrade'" end desc <<-DESC Upgrade to the newest versions of all rubygems. DESC task :upgrade_gems do sudo "gem update --system --no-rdoc --no-ri" sudo "gem update --no-rdoc --no-ri" do |ch, str, data| ch[:data] ||= "" ch[:data] << data if data =~ />\s*$/ puts data choice = Capistrano::CLI.ui.ask("The gem command is asking for a number:") ch.send_data("#{choice}\n") else puts data end end end desc <<-DESC Install extra Ubuntu packages. Set ec2onrails_config[:packages], it \ should be an array of strings. NOTE: the package installation will be non-interactive, if the packages \ require configuration either log in as 'root' and run \ 'dpkg-reconfigure packagename' or replace the package's config files \ using the 'ec2onrails:server:deploy_files' task. DESC task :install_packages do sudo "aptitude -q update" if cfg[:packages] && cfg[:packages].any? sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y install #{cfg[:packages].join(' ')}'" end end desc <<-DESC Provide extra security measures. Set ec2onrails_config[:harden_server] = true \ to allow the hardening of the server. These security measures are those which can make initial setup and playing around with Ec2onRails tricky. For example, you can be logged out of your server forever DESC task :harden_server do #NOTES: for those security features that will get in the way of ease-of-use # hook them in here if cfg[:harden_server] #lets install some extra packages: # denyhosts: sshd security tool. config file is already installed... # security_pkgs = %w{denyhosts} old_pkgs = cfg[:packages] begin cfg[:packages] = security_pkgs install_packages ensure cfg[:packages] = old_pkgs end end end desc <<-DESC Install extra rubygems. Set ec2onrails_config[:rubygems], it should \ be with an array of strings. DESC task :install_gems do if cfg[:rubygems] cfg[:rubygems].each do |gem| sudo "gem install #{gem} --no-rdoc --no-ri" do |ch, str, data| ch[:data] ||= "" ch[:data] << data if data =~ />\s*$/ puts data choice = Capistrano::CLI.ui.ask("The gem command is asking for a number:") ch.send_data("#{choice}\n") else puts data end end end end end desc <<-DESC Add extra gem sources to rubygems (to able to fetch gems from for example gems.github.com). Set ec2onrails_config[:rubygems_sources], it should be with an array of strings. DESC task :add_gem_sources do if cfg[:rubygems_sources] cfg[:rubygems_sources].each do |gem_source| sudo "gem sources -a #{gem_source}" end end end desc <<-DESC A convenience task to upgrade existing packages and gems and install \ specified new ones. DESC task :upgrade_and_install_all do upgrade_packages upgrade_gems install_packages install_gems end desc <<-DESC Set the timezone using the value of the variable named timezone. \ Valid options for timezone can be determined by the contents of \ /usr/share/zoneinfo, which can be seen here: \ http://packages.ubuntu.com/cgi-bin/search_contents.pl?searchmode=filelist&word=tzdata&version=gutsy&arch=all&page=1&number=all \ Remove 'usr/share/zoneinfo/' from the filename, and use the last \ directory and file as the value. For example 'Africa/Abidjan' or \ 'posix/GMT' or 'Canada/Eastern'. DESC task :set_timezone do if cfg[:timezone] sudo "bash -c 'echo #{cfg[:timezone]} > /etc/timezone'" sudo "cp /usr/share/zoneinfo/#{cfg[:timezone]} /etc/localtime" end end desc <<-DESC Deploy a set of config files to the server, the files will be owned by \ root. This doesn't delete any files from the server. This is intended mainly for customized config files for new packages installed via the \ ec2onrails:server:install_packages task. Subdirectories and files \ inside here will be placed within the same directory structure \ relative to the root of the server's filesystem. DESC task :deploy_files do if cfg[:server_config_files_root] begin filename = "config_files.tar" local_file = "#{Dir.tmpdir}/#{filename}" remote_file = "/tmp/#{filename}" FileUtils.cd(cfg[:server_config_files_root]) do File.open(local_file, 'wb') { |tar| Minitar.pack(".", tar) } end put File.read(local_file), remote_file sudo "tar xvf #{remote_file} -o -C /" ensure rm_rf local_file sudo "rm -f #{remote_file}" end end end desc <<-DESC Restart a set of services. Set ec2onrails_config[:services_to_restart] \ to an array of strings. It's assumed that each service has a script \ in /etc/init.d DESC task :restart_services do if cfg[:services_to_restart] && cfg[:services_to_restart].any? cfg[:services_to_restart].each do |service| run_init_script(service, "restart") end end end desc <<-DESC Set the email address that mail to the app user forwards to. DESC task :set_mail_forward_address do run "echo '#{cfg[:mail_forward_address]}' >> /home/app/.forward" if cfg[:mail_forward_address] # put cfg[:admin_mail_forward_address], "/home/admin/.forward" if cfg[:admin_mail_forward_address] end desc <<-DESC Enable ssl for the web server. The SSL cert file should be in /etc/ssl/certs/default.pem and the SSL key file should be in /etc/ssl/private/default.key (use the deploy_files task). DESC task :enable_ssl, :roles => :web do #TODO: enable for nginx sudo "a2enmod ssl" sudo "a2ensite default-ssl" run_init_script("web_proxy", "restart") end desc <<-DESC Restrict the main user's sudo access. Defaults the user to only be able to \ sudo to god DESC task :restrict_sudo_access do old_user = fetch(:user) begin set :user, 'root' sessions.clear #clear out sessions cache..... this way the ssh connections are reinitialized sudo "cp -f /etc/sudoers.restricted_access /etc/sudoers" # run "ln -sf /etc/sudoers.restricted_access /etc/sudoers" ensure set :user, old_user sessions.clear end end desc <<-DESC Grant *FULL* sudo access to the main user. DESC task :grant_sudo_access do allow_sudo end @within_sudo = 0 def allow_sudo begin @within_sudo += 1 old_user = fetch(:user) if @within_sudo > 1 yield if block_given? true elsif capture("ls -l /etc/sudoers /etc/sudoers.full_access | awk '{print $5}'").split.uniq.size == 1 yield if block_given? false else begin # need to cheet and temporarily set the user to ROOT so we # can (re)grant full sudo access. # we can do this because the root and app user have the same # ssh login preferences.... # # TODO: # do not escalate priv. to root...use another user like 'admin' that has full sudo access set :user, 'root' sessions.clear #clear out sessions cache..... this way the ssh connections are reinitialized run "cp -f /etc/sudoers.full_access /etc/sudoers" set :user, old_user sessions.clear yield if block_given? ensure server.restrict_sudo_access if block_given? set :user, old_user sessions.clear true end end ensure @within_sudo -= 1 end end end end end