LegacyTechops/slack/dist/slack

330 lines
9.1 KiB
Perl
Executable File

#!/usr/bin/perl -w
# $Id: slack 180 2008-01-19 08:26:19Z alan $
# vim:sw=2
# vim600:fdm=marker
# Copyright (C) 2004-2008 Alan Sundell <alan@sundell.net>
# All Rights Reserved. This program comes with ABSOLUTELY NO WARRANTY.
# See the file COPYING for details.
# This script is in charge of copying files from the (possibly remote)
# master directory to a local cache, using rsync
require 5.006;
use warnings FATAL => qw(all);
use strict;
use sigtrap qw(die untrapped normal-signals
stack-trace any error-signals);
use File::Path;
use File::Find;
use POSIX; # for strftime
use constant LIBEXEC_DIR => '/usr/lib/slack';
use constant LIB_DIR => '/usr/lib/slack';
use lib LIB_DIR;
use Slack;
sub run_backend(@);
sub run_conditional_backend($@);
(my $PROG = $0) =~ s#.*/##;
# Arguments to pass to each backends (initialized to a hash of empty arrays)
my %backend_flags = ( map { $_ => [] }
qw(getroles sync stage preview preinstall fixfiles installfiles postinstall)
);
my @roles;
########################################
# Environment
# Helpful prefix to die messages
$SIG{__DIE__} = sub { die "FATAL[$PROG]: @_"; };
# Set a reasonable umask
umask 077;
# Get out of wherever (possibly NFS-mounted) we were
chdir("/")
or die "Could not chdir /: $!";
# Autoflush on STDERR
select((select(STDERR), $|=1)[0]);
########################################
# Config and option parsing {{{
my $usage = Slack::default_usage("$PROG [options] [<role>...]");
$usage .= <<EOF;
--preview MODE
Do a diff of scripts and files before running them.
MODE can be one of 'simple' or 'prompt'.
--no-files
Don't install any files in ROOT, but tell rsync to print what
it would do.
--no-scripts
Don't run scripts.
--no-sync
Skip the slack-sync step. (useful if you're pushing stuff into
the CACHE outside of slack)
--role-list
Role list for slack-getroles
--libexec-dir DIR
Look for backend scripts in this directory.
--diff PROG
Use this diff program for previews
--sleep TIME
Randomly sleep between 1 and TIME seconds before starting
operations
EOF
# Options
my %opt = ();
# So we can distinguish stuff on the command line from config file stuff
my %command_line_opt = ();
Slack::get_options(
opthash => \%opt,
command_line_options => [
'preview=s',
'role-list=s',
'no-scripts|noscripts',
'no-files|nofiles',
'no-sync|nosync',
'libexec-dir=s',
'diff=s',
'sleep=i',
],
required_options => [ qw(source cache stage root) ],
command_line_hash => \%command_line_opt,
usage => $usage,
);
# Special options
if ($opt{'dry-run'}) {
$opt{'no-scripts'} = 1;
$opt{'no-files'} = 1;
}
if ($opt{'no-scripts'}) {
for my $action (qw(fixfiles preinstall postinstall)) {
push @{$backend_flags{$action}},
'--dry-run';
}
}
if ($opt{'no-files'}) {
push @{$backend_flags{installfiles}},
'--dry-run';
}
# propagate verbosity - 1 to all backends
if (defined $command_line_opt{'verbose'} and
$command_line_opt{'verbose'} > 1) {
for my $action (keys %backend_flags) {
push @{$backend_flags{$action}},
('--verbose') x ($command_line_opt{'verbose'} - 1);
}
}
# propagate these flags to all the backends
for my $option (qw(config root cache stage source hostname rsh)) {
if ($command_line_opt{$option}) {
for my $action (keys %backend_flags) {
push @{$backend_flags{$action}},
"--$option=$command_line_opt{$option}";
}
}
}
# getroles also can take 'role-list'
if ($command_line_opt{'role-list'}) {
push @{$backend_flags{'getroles'}},
"--role-list=$command_line_opt{'role-list'}";
}
# The libexec dir defaults to this if it wasn't specified
# on the command line or in a config file.
if (not defined $opt{'libexec-dir'}) {
$opt{'libexec-dir'} = LIBEXEC_DIR;
}
# Pass diff option along to slack-rolediff
if ($opt{'diff'}) {
push @{$backend_flags{preview}},
"--diff=$opt{'diff'}";
}
# Preview takes an optional argument. If no argument is given,
# it gets "" from getopt.
if (defined $opt{'preview'}) {
if (not grep /^$opt{'preview'}$/, qw(simple prompt)) {
die "Unknown preview mode '$opt{'preview'}'!";
}
}
# The backup option defaults to on if it wasn't specified
# on the command line or in a config file
if (not defined $opt{backup}) {
$opt{backup} = 1;
}
# Figure out a place to put backups
if ($opt{backup} and $opt{'backup-dir'}) {
push @{$backend_flags{installfiles}},
'--backup',
'--backup-dir='.
$opt{'backup-dir'}.
"/".
strftime('%F-%T', localtime(time))
;
}
# }}}
# Random sleep, helpful when called from cron.
if ($opt{sleep}) {
my $secs = int(rand($opt{sleep})) + 1;
$opt{verbose} and print STDERR "$PROG: sleep $secs\n";
sleep($secs);
}
# Get a list of roles to install from slack-getroles {{{
if (not @ARGV) {
my @command = ($opt{'libexec-dir'}.'/slack-getroles',
@{$backend_flags{'getroles'}});
$opt{verbose} and print STDERR "$PROG: getroles\n";
($opt{verbose} > 2) and print STDERR "$PROG: Calling '@command' to get a list of roles for this host.\n";
my ($roles_pid, $roles_fh);
if ($roles_pid = open($roles_fh, "-|")) {
# Parent
} elsif (defined $roles_pid) {
# Child
exec(@command);
die "Could not exec '@command': $!\n";
} else {
die "Could not fork to run '@command': $!\n";
}
@roles = split(/\s+/, join(" ", <$roles_fh>));
unless (close($roles_fh)) {
Slack::check_system_exit(@command);
}
} else {
@roles = @ARGV;
}
# }}}
# Check role name syntax {{{
for my $role (@roles) {
# Roles MUST begin with a letter. All else is reserved.
if ($role !~ m/^[a-zA-Z]/) {
die "Role '$role' does not begin with a letter!";
}
}
# }}}
$opt{verbose} and print STDERR "$PROG: installing roles: @roles\n";
unless ($opt{'no-sync'}) {
# sync all the roles down at once
$opt{verbose} and print STDERR "$PROG: sync @roles\n";
run_backend('slack-sync',
@{$backend_flags{sync}}, @roles);
}
ROLE: for my $role (@roles) {
# stage
$opt{verbose} and print STDERR "$PROG: stage files $role\n";
run_backend('slack-stage',
@{$backend_flags{stage}}, '--subdir=files', $role);
if ($opt{preview}) {
if ($opt{preview} eq 'simple') {
$opt{verbose} and print STDERR "$PROG: preview $role\n";
# Here, we run the backend in no-prompt mode.
run_conditional_backend(0, 'slack-rolediff',
@{$backend_flags{preview}}, $role);
# ...and we skip further action in the ROLE after showing the diff.
next ROLE;
} elsif ($opt{preview} eq 'prompt') {
$opt{verbose} and print STDERR "$PROG: preview scripts $role\n";
# Here, we want to prompt and just do the scripts, since
# we need to run preinstall and fixfiles before doing the files.
run_conditional_backend(1, 'slack-rolediff',
@{$backend_flags{preview}}, '--subdir=scripts', $role);
} else {
# Should get caught in option processing, above
die "Unknown preview mode!\n";
}
}
$opt{verbose} and print STDERR "$PROG: stage scripts $role\n";
run_backend('slack-stage',
@{$backend_flags{stage}}, '--subdir=scripts', $role);
# preinstall
$opt{verbose} and print STDERR "$PROG: preinstall $role\n";
run_backend('slack-runscript',
@{$backend_flags{preinstall}}, 'preinstall', $role);
# fixfiles
$opt{verbose} and print STDERR "$PROG: fixfiles $role\n";
run_backend('slack-runscript',
@{$backend_flags{fixfiles}}, 'fixfiles', $role);
# preview files
if ($opt{preview} and $opt{preview} eq 'prompt') {
$opt{verbose} and print STDERR "$PROG: preview files $role\n";
run_conditional_backend(1, 'slack-rolediff',
@{$backend_flags{preview}}, '--subdir=files', $role);
}
# installfiles
$opt{verbose} and print STDERR "$PROG: install $role\n";
run_backend('slack-installfiles',
@{$backend_flags{installfiles}}, $role);
# postinstall
$opt{verbose} and print STDERR "$PROG: postinstall $role\n";
run_backend('slack-runscript',
@{$backend_flags{postinstall}}, 'postinstall', $role);
}
exit 0;
sub run_backend (@) {
my ($backend, @args) = @_;
# If we weren't given an explicit path, prepend the libexec dir
unless ($backend =~ m#^/#) {
$backend = $opt{'libexec-dir'} . '/' . $backend;
}
# Assemble our command line
my (@command) = ($backend, @args);
($opt{verbose} > 2) and print STDERR "$PROG: Calling '@command'\n";
unless (system(@command) == 0) {
Slack::check_system_exit(@command);
}
}
sub run_conditional_backend ($@) {
my ($prompt, $backend, @args) = @_;
# If we weren't given an explicit path, prepend the libexec dir
unless ($backend =~ m#^/#) {
$backend = $opt{'libexec-dir'} . '/' . $backend;
}
# Assemble our command line
my (@command) = ($backend, @args);
($opt{verbose} > 2) and print STDERR "$PROG: Calling '@command'\n";
unless (system(@command) == 0) {
my $exit = Slack::get_system_exit(@command);
if ($exit == 1) {
# exit 1 means a difference found or something normal that requires
# a prompt before continuing.
if ($prompt) {
exit 1 unless Slack::prompt("Continue? [yN] ") eq 'y';
}
} else {
# any other non-successful exit is a serious error.
die "'@command' exited $exit";
}
}
}