mysociety/alaveteli

View on GitHub
lib/tasks/config_files.rake

Summary

Maintainability
Test Coverage
require File.join(File.dirname(__FILE__), 'usage')
namespace :config_files do

  include Usage

  class ExampleERBRenderer
    def initialize(file, **variables)
      @template = ERB.new(File.read(file), trim_mode: '-')
      @variables = variables
    end

    def lines
      @template.result(binding).split(/\r?\n/)
    end

    def method_missing(variable, *_args)
      @variables.fetch(variable) do
        raise "Unhandled variable in example file: #{variable}"
      end
    end
  end

  def convert_erb(file, **replacements)
    puts ExampleERBRenderer.new(file, **replacements).lines
  end

  def default_replacements
    opts = {
      cpus: ENV.fetch('CPUS') { '1' },
      mailto: ENV.fetch('MAILTO') { "#{ ENV['DEPLOY_USER'] }@localhost" },
      rails_env: ENV.fetch('RAILS_ENV') { 'development' },
      ruby_version: ENV.fetch('RUBY_VERSION') { '3.0.4' },
      site: ENV.fetch('SITE') { 'foi' },
      user: ENV.fetch('DEPLOY_USER') { 'alaveteli' },
      vcspath: ENV.fetch('VCSPATH') { 'alaveteli' },
      vhost_dir: ENV.fetch('VHOST_DIR') { '/var/www/alaveteli' },
      use_rbenv?: ENV.fetch('USE_RBENV', 'false') == 'true',
      rails_env_defined?: ENV['RAILS_ENV_DEFINED'] == 'true'
    }

    if opts[:use_rbenv?]
      rbenv_root = "/home/#{opts[:user]}/.rbenv"
      opts[:ruby_path] = "#{rbenv_root}/bin:#{rbenv_root}/shims"
    else
      opts[:ruby_path] = "/home/#{opts[:user]}/.gem/ruby/#{opts[:ruby_version]}/bin"
    end

    opts
  end

  def daemons
    [
      {
        path: '/etc/init.d',
        name: 'thin',
        template: 'config/sysvinit-thin.example',
        condition: -> { ENV['RAILS_ENV'] == 'production' }
      },
      {
        path: '/etc/systemd/system',
        name: 'sidekiq.service',
        template: 'config/sidekiq.service.example'
      },
      {
        path: '/etc/systemd/system',
        name: 'alert-tracks.service',
        template: 'config/alert-tracks.service.example'
      },
      {
        path: '/etc/systemd/system',
        name: 'send-notifications.service',
        template: 'config/send-notifications.service.example'
      },
      {
        path: '/etc/systemd/system',
        name: 'poll-for-incoming',
        template: 'config/poll-for-incoming.service.example',
        condition: -> do
          AlaveteliConfiguration.production_mailer_retriever_method == 'pop'
        end
      }
    ]
  end

  desc 'Return list of daemons to install based on the settings defined
        in general.yml for a given path'
  task active_daemons: :environment do
    example = 'rake config_files:active_daemons PATH=/etc/init.d'
    check_for_env_vars(['PATH'], example)

    puts daemons.
      select { |d| d[:path] == ENV['PATH'] }.
      select { |d| d.fetch(:condition, -> { true }).call }.
      map { |d| d[:name] }
  end

  desc 'Return list of all daemons the application defines for a given path'
  task all_daemons: :environment do
    example = 'rake config_files:all_daemons PATH=/etc/init.d SITE=alaveteli'
    check_for_env_vars(['PATH', 'SITE'], example)

    seporator = '-' if ENV['PATH'] == '/etc/init.d'
    seporator = '.' if ENV['PATH'] == '/etc/systemd/system'

    base = "#{ENV['SITE']}#{seporator}"
    glob = File.join("#{base}*")
    puts Dir.glob(glob, base: ENV['PATH']).map { _1.sub(base, '') }
  end

  desc 'Convert wrapper example in config to a form suitable for running mail handling scripts with rbenv'
  task convert_wrapper: :environment do
    example = 'rake config_files:convert_wrapper DEPLOY_USER=deploy SCRIPT_FILE=config/run-with-rbenv-path.example'
    check_for_env_vars(%w[DEPLOY_USER SCRIPT_FILE], example)

    convert_erb(ENV['SCRIPT_FILE'], **default_replacements)
  end

  desc 'Convert Debian example init script in config to a form suitable for installing in /etc/init.d'
  task convert_init_script: :environment do
    example = 'rake config_files:convert_init_script ' \
              'DEPLOY_USER=deploy ' \
              'VHOST_DIR=/dir/above/alaveteli ' \
              'VCSPATH=alaveteli ' \
              'SITE=alaveteli ' \
              'SCRIPT_FILE=config/sysvinit-thin.example ' \
              'RUBY_VERSION=3.0.4 ' \
              'USE_RBENV=false '
    check_for_env_vars(%w[DEPLOY_USER VHOST_DIR SCRIPT_FILE], example)

    daemon_name = ENV.fetch('DAEMON_NAME') do
      File.basename(ENV['SCRIPT_FILE'], '-debian.example')
    end

    replacements = default_replacements.merge(
      daemon_name: "#{default_replacements[:site]}-#{daemon_name}"
    )

    convert_erb(ENV['SCRIPT_FILE'], **replacements)
  end

  desc 'Convert example daemon in config to a form suitable for installing ' \
       'on a server'
  task convert_daemon: :environment do
    example = 'rake config_files:convert_daemon ' \
              'DEPLOY_USER=deploy ' \
              'VHOST_DIR=/dir/above/alaveteli ' \
              'VCSPATH=alaveteli ' \
              'SITE=alaveteli ' \
              'DAEMON=alert-tracks.service ' \
              'RUBY_VERSION=3.0.4 ' \
              'USE_RBENV=false '
    check_for_env_vars(%w[DEPLOY_USER VHOST_DIR DAEMON], example)

    daemon = daemons.find { |d| d[:name] == ENV['DAEMON'] }
    raise 'Unknown daemon' unless daemon

    ENV['SCRIPT_FILE'] = daemon[:template]
    ENV['DAEMON_NAME'] = daemon[:name].sub(/\.service$/, '')

    Rake::Task['config_files:convert_init_script'].invoke
  end

  desc 'Convert Debian example crontab file in config to a form suitable for installing in /etc/cron.d'
  task convert_crontab: :environment do
    example = 'rake config_files:convert_crontab ' \
              'DEPLOY_USER=deploy ' \
              'VHOST_DIR=/dir/above/alaveteli VCSPATH=alaveteli ' \
              'SITE=alaveteli CRONTAB=config/crontab-example ' \
              'MAILTO=cron-alaveteli@example.org ' \
              'RUBY_VERSION=3.0.4 ' \
              'USE_RBENV=false '
    check_for_env_vars(%w[DEPLOY_USER VHOST_DIR VCSPATH SITE CRONTAB], example)
    convert_erb(ENV['CRONTAB'], **default_replacements)
  end

  desc 'Convert miscellaneous example scripts. This does not check for required environment variables for the script, so please check the script file itself.'
  task convert_script: :environment do
    example = 'rake config_files:convert_script SCRIPT_FILE=config/run-with-rbenv-path.example'
    check_for_env_vars(['SCRIPT_FILE'], example)
    convert_erb(ENV['SCRIPT_FILE'], **default_replacements)
  end

  desc 'Set reject_incoming_at_mta on old requests that are rejecting incoming mail'
  task set_reject_incoming_at_mta: :environment do
    example = 'rake config_files:set_reject_incoming_at_mta REJECTED_THRESHOLD=5 AGE_IN_MONTHS=12'
    check_for_env_vars(%w[REJECTED_THRESHOLD AGE_IN_MONTHS], example)
    dryrun = ENV['DRYRUN'] != '0'
    STDERR.puts "Only a dry run; info_requests will not be updated" if dryrun
    options = { rejection_threshold: ENV['REJECTED_THRESHOLD'],
               age_in_months: ENV['AGE_IN_MONTHS'],
               dryrun: dryrun }

    updated_count = InfoRequest.reject_incoming_at_mta(options) do |ids|
      puts "Info Request\tRejected incoming count\tLast updated"
      ids.each do |id|
        info_request = InfoRequest.find(id)
        puts "#{info_request.id}\t#{info_request.rejected_incoming_count}\t#{info_request.updated_at}"
      end
    end
    puts "Updated #{updated_count} info requests"
  end

  desc 'Set reject_incoming_at_mta on a list of requests identified by ' \
       'request address'
  task set_reject_incoming_at_mta_from_list: :environment do
    example = 'rake config_files:set_reject_incoming_at_mta_from_list ' \
              'FILE=/tmp/rejection_list.txt'

    check_for_env_vars(['FILE'], example)

    File.read(ENV['FILE']).each_line do |line|
      info_request = InfoRequest.find_by_incoming_email(line.strip)
      info_request.reject_incoming_at_mta = true
      info_request.save!
    end
  end

  desc 'Unset reject_incoming_at_mta on a request'
  task unset_reject_incoming_at_mta: :environment do
    example = 'rake config_files:unset_reject_incoming_at_mta REQUEST_ID=4'
    check_for_env_vars(['REQUEST_ID'], example)
    info_request = InfoRequest.find(ENV['REQUEST_ID'])
    if info_request.reject_incoming_at_mta
      info_request.reject_incoming_at_mta = false
      info_request.allow_new_responses_from = 'authority_only'
      info_request.save!
      puts "reject_incoming_at_mta set to false for InfoRequest #{ENV['REQUEST_ID']}"
    else
      puts "Warning: reject_incoming_at_mta already false for " \
           "InfoRequest #{ENV['REQUEST_ID']}"
    end
  end

  desc 'Produce a list of email addresses for which the MTA should reject messages at RCPT time'
  task generate_mta_rejection_list: :environment do
    example = 'rake config_files:generate_mta_rejection_list MTA=(exim|postfix)'
    check_for_env_vars(['MTA'], example)
    mta = ENV['MTA'].downcase
    unless %w[postfix exim].include? mta
      puts "Error: Unrecognised MTA"
      exit 1
    end
    InfoRequest.where(reject_incoming_at_mta: true).each do |info_request|
      if mta == 'postfix'
        puts "#{info_request.incoming_email} REJECT"
      else
        puts info_request.incoming_email
      end
    end
  end
end