# encoding: utf-8
module Sprinkle
# = Packages
#
# A package defines one or more things to provision onto the server.
# There is a lot of flexibility in a way a package is defined but
# let me give you a basic example:
#
# package :ruby do
# description 'Ruby MRI'
# version '1.8.6'
# apt 'ruby'
#
# verify { has_executable 'ruby' }
# end
#
# The above would define a package named 'ruby' and give it a description
# and explicitly say its version. It is installed via apt and to verify
# the installation was successful sprinkle will check for the executable
# 'ruby' being availble. Pretty simple, right?
#
# Note: Defining a package does not INSTALL it. To install a
# package, you must require it in a Sprinkle::Policy block.
#
# == Pre-Requirements
#
# Most packages have some sort of pre-requisites in order to be installed.
# Sprinkle allows you to define the requirements of the package, which
# will be installed before the package itself. An example below:
#
# package :rubygems do
# source 'http://rubyforge.org/rubygems.tgz'
# requires :ruby
# end
#
# In this case, when rubygems is being installed, Sprinkle will first
# provision the server with Ruby to make sure the requirements are met.
# In turn, if ruby has requirements, it installs those first, and so on.
#
# == Defaults
#
# Packages can be given defaults.
# These default options are available as a hash in opts.
#
# package :deploy_user do
# defaults :username => 'deploy'
# add_user opts[:username]
# end
#
# Options given when requiring a package are merged over the defaults
#
# requires :deploy_user, :username => 'deployer'
#
# == Verifications
#
# Most of the time its important to know whether the software you're
# attempting to install was installed successfully or not. For this,
# Sprinkle provides verifications. Verifications are one or more blocks
# which define rules with which Sprinkle can check if it installed
# the package successfully. If these verification blocks fail, then
# Sprinkle will gracefully stop the entire process. An example below:
#
# package :rubygems do
# source 'http://rubyforge.org/rubygems.tgz'
# requires :ruby
#
# verify { has_executable 'gem' }
# end
#
# In addition to verifying an installation was successfully, by default
# Sprinkle runs these verifications before the installation to
# check if the package is already installed. If the verifications pass
# before installing the package, it skips the package. To override this
# behavior, set the -f flag on the sprinkle script or set the
# :force option to true in Sprinkle::OPTIONS
#
# For more information on verifications and to see all the available
# verifications, see Sprinkle::Verify
#
# == Virtual Packages
#
# Sometimes, there are multiple packages available for a single task. An
# example is a database package. It can contain mySQL, postgres, or sqlite!
# This is where virtual packages come in handy. They are defined as follows:
#
# package :sqlite3, :provides => :database do
# apt 'sqlite3'
# end
#
# The :provides option allows you to reference this package either by :sqlite3
# or by :database. But whereas the package name is unique, multiple packages may
# share the same provision. If this is the case, when running Sprinkle, the
# script will ask you which provision you want to install. At this time, you
# can only install one.
#
# == Meta-Packages
#
# A package doesn't require an installer. If you want to define a package which
# merely encompasses other packages, that is fine too. Example:
#
# package :meta do
# requires :magic_beans
# requires :magic_sauce
# end
#
#--
# FIXME: Should probably document recommendations.
#++
module Package
PACKAGES = PackageRepository.new
def package(name, metadata = {}, &block)
package = Package.new(name, metadata, &block)
PACKAGES << package
package
end
class Package #:nodoc:
attr_accessor :name, :provides, :installers, :verifications
attr_accessor :args, :opts
cattr_reader :installer_methods
@@installer_methods = []
include Rendering
def self.add_api(&block)
before = self.instance_methods
self.class_eval &block
added = self.instance_methods - before
@@installer_methods += added.map(&:to_sym)
end
def initialize(name, metadata = {}, &block)
raise 'No package name supplied' unless name
@name = name
@metadata = metadata
@provides = metadata[:provides]
@dependencies = []
@recommends = []
@optional = []
@verifications = []
@installers = []
@block = block
# this should probably not be done twice
self.instance_eval &block
end
def description(s=nil)
s ? @description = s : @description
end
def version(s=nil)
s ? @version = s : @version
end
def instance(*args)
p=Package.new(name, @metadata) {}
p.opts = defaults.merge(args.extract_options!)
p.args = args
p.instance_variable_set("@block", @block)
p.instance_eval &@block
p
end
def sudo?
@use_sudo
end
def use_sudo(flag=true)
@use_sudo = flag
end
def defaults(s=nil)
s ? @defaults = s : @defaults ||= Hash.new
end
def args
@args ||= []
end
def opts
@opts ||= defaults.clone
end
class ContextError < StandardError #:nodoc:
end
def get(x)
raise ContextError, "Cannot call get inside a package, must be inside an Installer block"
end
# TODO - remove
def push_file(file, options ={}, &block)
ActiveSupport::Deprecation.warn("push_file is depreciated and will be removed in v0.9. Use the new `file` installer instead.")
raise "need content" unless options[:content]
runner "#{"sudo " if sudo?}rm -f #{file}"
push_text options[:content], file, options, &block
end
def verify(description = '', &block)
@verifications << Sprinkle::Verify.new(self, description, &block)
end
def process(deployment, roles)
logger.info " * #{name}"
return if meta_package?
opts.each_with_index do |(k, v), index|
branch = (index == opts.size - 1) ? "└" : "├"
logger.debug " #{branch}─ #{k}: #{v}"
end
# Run a pre-test to see if the software is already installed. If so,
# we can skip it, unless we have the force option turned on!
unless @verifications.empty? || Sprinkle::OPTIONS[:force]
begin
process_verifications(deployment, roles, true)
logger.info " --> already installed for roles: #{roles}"
return
rescue Sprinkle::VerificationFailed => e
# Continue
end
end
@installers.each do |installer|
installer.defaults(deployment)
installer.process(roles)
end
process_verifications(deployment, roles)
logger.info " --> INSTALLED for roles: #{roles}"
end
def process_verifications(deployment, roles, pre = false)
return if @verifications.blank?
if pre
logger.debug "--> Checking if #{self.name} is already installed for roles: #{roles}"
else
logger.debug "--> Verifying #{self.name} was properly installed for roles: #{roles}"
end
@verifications.each do |v|
v.defaults(deployment)
v.process(roles)
end
end
def requires(*packages)
add_dependencies packages, :dependencies
end
def recommends(*packages)
add_dependencies packages, :recommends
end
def optional(*packages)
add_dependencies packages, :optional
end
def dependencies
@dependencies.map {|a,b| a }
end
def tree(depth = 1, &block)
packages = []
packages << tree_for_packages(@recommends, :depth => depth, &block)
packages << tree_for_packages(@dependencies, :depth => depth, :required => true, &block)
packages << self
packages << tree_for_packages(@optional, :depth => depth, &block)
packages
end
def to_s
"#{@name} #{@version}".strip
end
# allow an installer to request a private install queue from the package
# for example to allow pre and post hooks to have their own installers that
# do not mess with the packages installer list
#
# returns: the private queue
def with_private_install_queue()
b = @installers
@installers = private_queue =[]
yield
@installers = b
private_queue
end
protected
def install(i)
@installers << i
i
end
private
def add_dependencies(packages, kind)
opts = packages.extract_options!
depends = instance_variable_get("@#{kind}")
packages.each do |pack|
depends << [pack, opts]
end
depends.map {|a,b| a }
end
def tree_for_packages(packages, opts={}, &block)
depth = opts[:depth]
tree = []
packages.each do |dep, config|
package = PACKAGES.find_all(dep, config)
raise "Package definition not found for key: #{dep}" if package.empty? and opts[:required]
next if package.empty? # skip missing recommended packages as they're allowed to not exist
package = Chooser.select_package(dep, package) #if package.size>1
package = package.instance(config)
block.call(self, package, depth) if block
tree << package.tree(depth + 1, &block)
end
tree
end
def cloud_info(message)
logger.info(message) if Sprinkle::OPTIONS[:cloud] or logger.debug?
end
def meta_package?
@installers.blank?
end
end
end
end