sprinkle-tool/sprinkle

View on GitHub
lib/sprinkle/installers/installer.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Sprinkle
  # Installers are where the bulk of the work in Sprinkle happens.  Installers are
  # the building blocks of packages.  Typically each unique type of install
  # command has it's own installer class.
  #
  module Installers
    # The base class which all installers must subclass, this class makes
    # sure all installers share some general features, which are outlined
    # below.
    #
    # = Pre/Post Installation Hooks
    #
    # With all installation methods you have the ability to specify multiple
    # pre/post installation hooks. This gives you the ability to specify
    # commands to run before and after an installation takes place.
    # There are three ways to specify a pre/post hook.
    #
    # Note about sudo:
    # When using the Capistrano actor all commands by default are run using
    # sudo (unless your Capfile includes "set :use_sudo, false").  If you wish
    # to use sudo periodically with "set :user_sudo, false" or with an actor
    # other than Capistrano then you can just append it to your command. Some
    # installers (transfer) also support a :sudo option, so check each
    # installer for details.
    #
    # First, a single command:
    #
    #   pre :install, 'echo "Hello, World!"'
    #   post :install, 'rm -rf /'
    #
    # Second, an array of commands:
    #
    #   commands = ['echo "First"', 'echo "Then Another"']
    #   pre :install, commands
    #   post :install, commands
    #
    # Third, a block which returns either a single or multiple commands:
    #
    #   pre :install do
    #     amount = 7 * 3
    #     "echo 'Before we install, lets plant #{amount} magic beans...'"
    #   end
    #   post :install do
    #     ['echo "Now... let's hope they sprout!", 'echo "Indeed they have!"']
    #   end
    #
    # = Other Pre/Post Hooks
    #
    # Some installation methods actually grant you more fine grained
    # control of when commands are run rather than a blanket pre :install
    # or post :install. If this is the case, it will be documented on
    # the installation method's corresponding documentation page.
    class Installer
      include Sprinkle::Attributes
      include Sprinkle::Sudo

      delegate :version, :to => :package

      attr_accessor :delivery, :package, :options, :pre, :post #:nodoc:

      def initialize(package, options = {}, &block) #:nodoc:
        @package = package
        @options = options || {}
        @pre = {}; @post = {}
        @delivery = nil
        self.instance_eval(&block) if block
      end

      attributes :prefix, :archives, :builds

      class << self
        def subclasses
          @subclasses ||= []
        end

        def api(&block)
          Sprinkle::Package::Package.add_api(&block)
        end

        def verify_api(&block)
          Sprinkle::Verify.class_eval(&block)
        end

        def inherited(base)
          subclasses << base
        end
      end

      def escape_shell_arg(str)
        str.gsub("'", "'\\\\''").gsub("\n", '\n')
      end

      def pre(stage, *commands, &block)
        @pre[stage] ||= []
        @pre[stage] += commands
        @pre[stage] << defer(block) if block_given?
        @pre[stage]
      end

      def post(stage, *commands, &block)
        @post[stage] ||= []
        @post[stage] += commands
        @post[stage] << defer(block) if block_given?
        @post[stage]
      end

      # defer execution of command block until the package is being
      # processed
      def defer(block)
        p = Proc.new { self.commands_from_block(block) }
      end

      def commands_from_block(block)
        return [] unless block
        out = nil
        diff = @package.with_private_install_queue { out = block.call }
        diff.each {|x| x.delivery = self.delivery }
        diff.empty? ? out : diff.map {|x| x.install_sequence }
      end

      def method_missing(method, *args, &block)
        if package.class.installer_methods.include?(method)
          @package.send(method, *args, &block)
        else
          super(method, *args, &block)
        end
      end

      def per_host?
        return false
      end

      # Called right after processing, can be used for local cleanup such
      # as removing any temporary files created on the local system, etc
      def post_process; end

      # Called right before an installer is exected, can be used for logging
      # and announcing what is about to happen
      def announce; end

      def process(roles) #:nodoc:
        if logger.debug?
          sequence = install_sequence; sequence = sequence.join('; ') if sequence.is_a? Array
          logger.debug "#{@package.name} install sequence: #{sequence} for roles: #{roles}\n"
        end

        unless Sprinkle::OPTIONS[:testing]
          logger.debug "    --> Running #{self.class.name} for roles: #{roles}"
          @delivery.install(self, roles, :per_host => per_host?)
        end
        post_process
      end

        # More complicated installers that have different stages, and require pre/post commands
        # within stages can override install_sequence and take complete control of the install
        # command sequence construction (eg. source based installer).
        def install_sequence
          commands = pre_commands(:install) + [ install_commands ] + post_commands(:install)
          flatten commands
        end

      protected

        def log(t, level=:info) #:nodoc:
          logger.send(level, t)
        end

        def flatten(commands) #:nodoc:
          commands.flatten.map {|c| c.is_a?(Proc) ? c.call : c }.flatten
        end

        # A concrete installer (subclass of this virtual class) must override this method
        # and return the commands it needs to run as either a string or an array.
        #
        # <b>Overriding this method is required.</b>
        def install_commands
          raise 'Concrete installers implement this to specify commands to run to install their respective packages'
        end

        def pre_commands(stage) #:nodoc:
          dress @pre[stage] || [], :pre
        end

        def post_commands(stage) #:nodoc:
          dress @post[stage] || [], :post
        end

        # Concrete installers (subclasses of this virtual class) can override this method to
        # specify stage-specific (pre-installation, post-installation, etc.) modifications
        # of commands.
        #
        # An example usage of overriding this would be to prefix all commands for a
        # certain stage to change to a certain directory. An example is given below:
        #
        #   def dress(commands, stage)
        #     commands.collect { |x| "cd #{magic_beans_path} && #{x}" }
        #   end
        #
        # By default, no modifications are made to the commands.
        def dress(commands, stage); commands; end

    end
  end
end