From d8c27b4ddeefaf60f332c7e454de46ed29cd8a10 Mon Sep 17 00:00:00 2001 From: Chris Gerber Date: Thu, 27 Feb 2014 15:59:23 -0500 Subject: [PATCH 1/4] Add a self-extracting sh package type implementation --- lib/fpm/package/sh.rb | 75 +++++++++++ lib/fpm/version.rb | 2 +- templates/sh.erb | 297 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 lib/fpm/package/sh.rb create mode 100644 templates/sh.erb diff --git a/lib/fpm/package/sh.rb b/lib/fpm/package/sh.rb new file mode 100644 index 0000000000..03764f5606 --- /dev/null +++ b/lib/fpm/package/sh.rb @@ -0,0 +1,75 @@ +require "erb" +require "fpm/namespace" +require "fpm/package" +require "fpm/errors" +require "fpm/util" +require "backports" +require "fileutils" +require "digest" + +# Support for self extracting sh files (.sh files) +# +# This class only supports output of packages. +# +# The sh package is a single sh file with a bzipped tar payload concatenated to the end. +# The script can unpack the tarball to install it and call optional post install scripts. +class FPM::Package::Sh < FPM::Package + + def output(output_path) + create_scripts + + # Make one file. The installscript can unpack itself. + `cat #{install_script} #{payload} > #{output_path}` + FileUtils.chmod("+x", output_path) + end + + def create_scripts + if script?(:before_install) + # the scripts are kept in the payload so what would before install be if we've already + # unpacked the payload? + raise "sh package does not support before install scripts." + end + + if script?(:after_install) + File.write(File.join(fpm_meta_path, "after_install"), script(:after_install)) + end + end + + def install_script + path = build_path("installer.sh") + File.open(path, "w") do |file| + file.write template("sh.erb").result(binding) + end + path + end + + # Returns the path to the tar file containing the packed up staging directory + def payload + payload_tar = build_path("payload.tar") + @logger.info("Creating payload tar ", :path => payload_tar) + + args = [ tar_cmd, + "-C", + staging_path, + "-cf", + payload_tar, + "--owner=0", + "--group=0", + "--numeric-owner", + "." ] + + unless safesystem(*args) + raise "Command failed while creating payload tar: #{args}" + end + payload_tar + end + + # Where we keep metadata and post install scripts and such + def fpm_meta_path + @fpm_meta_path ||= begin + path = File.join(staging_path, ".fpm") + FileUtils.mkdir_p(path) + path + end + end +end diff --git a/lib/fpm/version.rb b/lib/fpm/version.rb index 07cd2d3c7b..1bfa939f9c 100644 --- a/lib/fpm/version.rb +++ b/lib/fpm/version.rb @@ -1,3 +1,3 @@ module FPM - VERSION = "1.0.2" + VERSION = "1.1.0" end diff --git a/templates/sh.erb b/templates/sh.erb new file mode 100644 index 0000000000..f9e98d820b --- /dev/null +++ b/templates/sh.erb @@ -0,0 +1,297 @@ +#!/bin/bash + +# bail out if any part of this fails +set -e + +# This is the self-extracting installer script for an FPM shell installer package. +# It contains the logic to unpack a tar archive appended to the end of this script +# and, optionally, to run post install logic. +# Run the package file with -h to see a usage message or look at the print_usage method. +# +# The post install scripts are called with INSTALL_ROOT, INSTALL_DIR and VERBOSE exported +# into the environment for their use. +# +# INSTALL_ROOT = the path passed in with -i or a relative directory of the name of the package +# file with no extension +# INSTALL_DIR = the same as INSTALL_ROOT unless -c (capistrano release directory) argumetn +# is used. Then it is $INSTALL_ROOT/releases/ +# CURRENT_DIR = if -c argument is used, this is set to the $INSTALL_ROOT/current which is +# symlinked to INSTALL_DIR +# VERBOSE = is set if the package was called with -v for verbose output +function main() { + set_install_dir + + create_pid + + wait_for_others + + kill_others + + set_owner + + unpack_payload + + if [[ -n $UNPACK_ONLY ]] ; then + echo "Unpacking complete, not moving symlinks or restarting because unpack only was specified." + else + create_symlinks + + set +e # don't exit on errors to allow us to clean up + if ! run_post_install ; then + revert_symlinks + log "Installation failed." + exit 1 + else + clean_out_old_releases + log "Installation complete." + fi + fi +} + +# deletes the PID file for this installation +function delete_pid(){ + rm -f ${INSTALL_DIR}/$$.pid 2> /dev/null +} + +# creates a PID file for this installation +function create_pid(){ + trap "delete_pid" EXIT + echo $$> ${INSTALL_DIR}/$$.pid +} + + +# checks for other PID files and sleeps for a grace period if found +function wait_for_others(){ + local count=`ls ${INSTALL_DIR}/*.pid | wc -l` + + if [ $count -gt 1 ] ; then + sleep 10 + fi +} + +# kills other running installations +function kill_others(){ + for PID_FILE in $(ls ${INSTALL_DIR}/*.pid) ; do + local p=`cat ${PID_FILE}` + if ! [ $p == $$ ] ; then + kill -9 $p + rm -f $PID_FILE 2> /dev/null + fi + done +} + +# echos metadata file. A function so that we can have it change after we set INSTALL_ROOT +function fpm_metadata_file(){ + echo "${INSTALL_ROOT}/.install-metadata" +} + +# if this package was installed at this location already we will find a metadata file with the details +# about the installation that we left here. Load from that if available but allow command line args to trump +function load_environment(){ + local METADATA=$(fpm_metadata_file) + if [ -r "${METADATA}" ] ; then + log "Found existing metadata file '${METADATA}'. Loading previous install details. Env vars in current environment will take precedence over saved values." + local TMP="/tmp/$(basename $0).$$.tmp" + # save existing environment, load saved environment from previous run from install-metadata and then + # overlay current environment so that anything set currencly will take precedence + # but missing values will be loaded from previous runs. + save_environment "$TMP" + source "${METADATA}" + source $TMP + rm "$TMP" + fi +} + +# write out metadata for future installs +function save_environment(){ + local METADATA=$1 + echo -n "" > ${METADATA} # empty file + + # just piping env to a file doesn't quote the variables. This does + # filter out multiline junk and _. _ is a readonly variable + env | egrep "^[^ ]+=.*" | grep -v "^_=" | while read ENVVAR ; do + local NAME=${ENVVAR%%=*} + # sed is to preserve variable values with dollars (for escaped variables or $() style command replacement), + # and command replacement backticks + # Escaped parens captures backward reference \1 which gets replaced with backslash and \1 to esape them in the saved + # variable value + local VALUE=$(eval echo '$'$NAME | sed 's/\([$`]\)/\\\1/g') + echo "export $NAME=\"$VALUE\"" >> ${METADATA} + done + + if [ -n "${OWNER}" ] ; then + chown ${OWNER} ${METADATA} + fi +} + +function set_install_dir(){ + # if INSTALL_ROOT isn't set by parsed args, use basename of package file with no extension + DEFAULT_DIR=$(echo $(basename $0) | sed -e 's/\.[^\.]*$//') + INSTALL_DIR=${INSTALL_ROOT:-$DEFAULT_DIR} + + DATESTAMP=$(date +%Y%m%d%H%M%S) + if [ -z "$USE_FLAT_RELEASE_DIRECTORY" ] ; then + RELEASE_ID=<%= release_id %> + INSTALL_DIR="${RELEASES_DIR}/${RELEASE_ID:-$DATESTAMP}" + fi + + mkdir -p "$INSTALL_DIR" || die "Unable to create install directory $INSTALL_DIR" + + export INSTALL_DIR + + log "Installing package to '$INSTALL_DIR'" +} + +function set_owner(){ + export OWNER=${OWNER:-$USER} + log "Installing as user $OWNER" +} + +function unpack_payload(){ + if $FORCE || [ ! "$(ls -A $INSTALL_DIR)" ] ; then + log "Unpacking payload . . ." + local archive_line=$(grep -a -n -m1 '__ARCHIVE__$' $0 | sed 's/:.*//') + tail -n +$((archive_line + 1)) $0 | tar -C $INSTALL_DIR -xf - > /dev/null || die "Failed to unpack payload from the end of '$0' into '$INSTALL_DIR'" + else + # Files are already here, just move symlinks + log "Directory already exists and has contents ($INSTALL_DIR). Not unpacking payload." + fi +} + +function run_post_install(){ + local AFTER_INSTALL=$INSTALL_DIR/.fpm/after_install + if [ -r $AFTER_INSTALL ] ; then + chmod +x $AFTER_INSTALL + log "Running post install script" + $AFTER_INSTALL + return $? + fi + return 0 +} + +function create_symlinks(){ + [ -n "$USE_FLAT_RELEASE_DIRECTORY" ] && return + + export CURRENT_DIR="$INSTALL_ROOT/current" + if [ -e "$CURRENT_DIR" ] ; then + OLD_CURRENT_TARGET=$(readlink $CURRENT_DIR) + rm "$CURRENT_DIR" + fi + ln -s "$INSTALL_DIR" "$CURRENT_DIR" + + log "Symlinked '$INSTALL_DIR' to '$CURRENT_DIR'" +} + +# in case post install fails we may have to back out switching the symlink to current +# We can't switch the symlink after because post install may assume that it is in the +# exact state of being installed (services looking to current for their latest code) +function revert_symlinks(){ + if [ -n "$OLD_CURRENT_TARGET" ] ; then + log "Putting current symlink back to '$OLD_CURRENT_TARGET'" + if [ -e "$CURRENT_DIR" ] ; then + rm "$CURRENT_DIR" + fi + ln -s "$OLD_CURRENT_TARGET" "$CURRENT_DIR" + fi +} + +function clean_out_old_releases(){ + [ -n "$USE_FLAT_RELEASE_DIRECTORY" ] && return + + while [ $(ls -tr "${RELEASES_DIR}" | wc -l) -gt 2 ] ; do + OLDEST_RELEASE=$(ls -tr "${RELEASES_DIR}" | head -1) + log "Deleting old release '${OLDEST_RELEASE}'" + rm -rf "${RELEASES_DIR}/${OLDEST_RELEASE}" + done +} + +function print_usage(){ + echo "Usage: `basename $0` [options]" + echo "Install this package" + echo " -i : install_root - an optional directory to install to." + echo " Default is package file name without file extension" + echo " -o : owner - the name of the user that will own the files installed" + echo " by the package. Defaults to current user" + echo " -r: disable capistrano style release directories - Default behavior is to create a releases directory inside" + echo " install_root and unpack contents into a date stamped (or build time id named) directory under the release" + echo " directory. Then create a 'current' symlink under install_root to the unpacked" + echo " directory once installation is complete replacing the symlink if it already " + echo " exists. If this flag is set just install into install_root directly" + echo " -u: Unpack the package, but do not install and symlink the payload" + echo " -f: force - Always overwrite existing installations" + echo " -y: yes - Don't prompt to clobber existing installations" + echo " -v: verbose - More output on installation" + echo " -h: help - Display this message" +} + +function die () { + local message=$* + echo "Error: $message : $!" + exit 1 +} + +function log(){ + local message=$* + if [ -n "$VERBOSE" ] ; then + echo "$*" + fi +} + +function parse_args() { + args=`getopt i:o:rfuyvh $*` + + if [ $? != 0 ] ; then + print_usage + exit 2 + fi + set -- $args + for i + do + case "$i" + in + -r) + USE_FLAT_RELEASE_DIRECTORY=1 + shift;; + -i) + shift; + export INSTALL_ROOT="$1" + export RELEASES_DIR="${INSTALL_ROOT}/releases" + shift;; + -o) + shift; + export OWNER="$1" + shift;; + -v) + export VERBOSE=1 + shift;; + -u) + UNPACK_ONLY=true + shift;; + -f) + FORCE=true + shift;; + -y) + CONFIRM="y" + shift;; + -h) + print_usage + exit 0 + shift;; + --) + shift; break;; + esac + done +} + +# parse args first to get install root +parse_args $* +# load environment from previous installations so we get defaults from that +load_environment +# reparse args so they can override any settings from previous installations if provided on the command line +parse_args $* + +main +save_environment $(fpm_metadata_file) +exit 0 + +__ARCHIVE__ From 76d93fca9b5d5f38f2d2fe26e3c245af114258c0 Mon Sep 17 00:00:00 2001 From: Chris Gerber Date: Mon, 10 Mar 2014 18:39:28 -0400 Subject: [PATCH 2/4] Fix FORCE variable handling; add post install logging --- templates/sh.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/sh.erb b/templates/sh.erb index f9e98d820b..e57992310f 100644 --- a/templates/sh.erb +++ b/templates/sh.erb @@ -31,7 +31,7 @@ function main() { unpack_payload - if [[ -n $UNPACK_ONLY ]] ; then + if [ "$UNPACK_ONLY" == "1" ] ; then echo "Unpacking complete, not moving symlinks or restarting because unpack only was specified." else create_symlinks @@ -148,7 +148,7 @@ function set_owner(){ } function unpack_payload(){ - if $FORCE || [ ! "$(ls -A $INSTALL_DIR)" ] ; then + if [ "$FORCE" == "1" ] || [ ! "$(ls -A $INSTALL_DIR)" ] ; then log "Unpacking payload . . ." local archive_line=$(grep -a -n -m1 '__ARCHIVE__$' $0 | sed 's/:.*//') tail -n +$((archive_line + 1)) $0 | tar -C $INSTALL_DIR -xf - > /dev/null || die "Failed to unpack payload from the end of '$0' into '$INSTALL_DIR'" @@ -163,7 +163,7 @@ function run_post_install(){ if [ -r $AFTER_INSTALL ] ; then chmod +x $AFTER_INSTALL log "Running post install script" - $AFTER_INSTALL + log $($AFTER_INSTALL) return $? fi return 0 @@ -265,10 +265,10 @@ function parse_args() { export VERBOSE=1 shift;; -u) - UNPACK_ONLY=true + UNPACK_ONLY=1 shift;; -f) - FORCE=true + FORCE=1 shift;; -y) CONFIRM="y" From df0e1589c424984775cdc68f990c42a6b85cff3d Mon Sep 17 00:00:00 2001 From: Chris Gerber Date: Tue, 11 Mar 2014 00:35:44 -0400 Subject: [PATCH 3/4] Only set RELEASE_ID if it is passed in as a template variable --- templates/sh.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/sh.erb b/templates/sh.erb index e57992310f..cfc95aaad8 100644 --- a/templates/sh.erb +++ b/templates/sh.erb @@ -131,7 +131,7 @@ function set_install_dir(){ DATESTAMP=$(date +%Y%m%d%H%M%S) if [ -z "$USE_FLAT_RELEASE_DIRECTORY" ] ; then - RELEASE_ID=<%= release_id %> + <%= "RELEASE_ID=#{release_id}" if respond_to?(:release_id) %> INSTALL_DIR="${RELEASES_DIR}/${RELEASE_ID:-$DATESTAMP}" fi From 665bb7c3a79448450731306e1144de9292a8e5bd Mon Sep 17 00:00:00 2001 From: Chris Gerber Date: Wed, 19 Mar 2014 12:43:08 -0400 Subject: [PATCH 4/4] Move pids up to install root to prevent "folder *not* empty" problem --- templates/sh.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/sh.erb b/templates/sh.erb index cfc95aaad8..3252b0a7c4 100644 --- a/templates/sh.erb +++ b/templates/sh.erb @@ -50,19 +50,19 @@ function main() { # deletes the PID file for this installation function delete_pid(){ - rm -f ${INSTALL_DIR}/$$.pid 2> /dev/null + rm -f ${INSTALL_ROOT}/$$.pid 2> /dev/null } # creates a PID file for this installation function create_pid(){ trap "delete_pid" EXIT - echo $$> ${INSTALL_DIR}/$$.pid + echo $$> ${INSTALL_ROOT}/$$.pid } # checks for other PID files and sleeps for a grace period if found function wait_for_others(){ - local count=`ls ${INSTALL_DIR}/*.pid | wc -l` + local count=`ls ${INSTALL_ROOT}/*.pid | wc -l` if [ $count -gt 1 ] ; then sleep 10 @@ -71,7 +71,7 @@ function wait_for_others(){ # kills other running installations function kill_others(){ - for PID_FILE in $(ls ${INSTALL_DIR}/*.pid) ; do + for PID_FILE in $(ls ${INSTALL_ROOT}/*.pid) ; do local p=`cat ${PID_FILE}` if ! [ $p == $$ ] ; then kill -9 $p