LegacyTechops/slack/dist/Slack.pm

372 lines
9.5 KiB
Perl
Executable File

# $Id: Slack.pm 189 2008-04-21 00:52:56Z sundell $
# 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.
package Slack;
require 5.006;
use strict;
use Carp qw(cluck confess croak);
use File::Find;
use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG);
use base qw(Exporter);
use vars qw($VERSION @EXPORT @EXPORT_OK $DEFAULT_CONFIG_FILE);
$VERSION = '0.15.2';
@EXPORT = qw();
@EXPORT_OK = qw();
$DEFAULT_CONFIG_FILE = '/etc/slack.conf';
my $term;
my @default_options = (
'help|h|?',
'version',
'verbose|v+',
'quiet',
'config|C=s',
'source|s=s',
'rsh|e=s',
'cache|c=s',
'stage|t=s',
'root|r=s',
'dry-run|n',
'backup|b',
'backup-dir=s',
'hostname|H=s',
);
sub default_usage ($) {
my ($synopsis) = @_;
return <<EOF;
Usage: $synopsis
Options:
-h, -?, --help
Print this help message and exit.
--version
Print the version number and exit.
-v, --verbose
Be verbose.
--quiet
Don't be verbose (Overrides previous uses of --verbose)
-C, --config FILE
Use this config file instead of '$DEFAULT_CONFIG_FILE'.
-s, --source DIR
Source for slack files
-e, --rsh COMMAND
Remote shell for rsync
-c, --cache DIR
Local cache directory for slack files
-t, --stage DIR
Local staging directory for slack files
-r, --root DIR
Root destination for slack files
-n, --dry-run
Don't write any files to disk -- just report what would have been done.
-b, --backup
Make backups of existing files in ROOT that are overwritten.
--backup-dir DIR
Put backups into this directory.
-H, --hostname HOST
Pretend to be running on HOST, instead of the name given by
gethostname(2).
EOF
}
# Read options from a config file. Arguments:
# file => config file to read
# opthash => hashref in which to store the options
# verbose => whether to be verbose
sub read_config (%) {
my %arg = @_;
my ($config_fh);
local $_;
confess "Slack::read_config: no config file given"
if not defined $arg{file};
$arg{opthash} = {}
if not defined $arg{opthash};
open($config_fh, '<', $arg{file})
or confess "Could not open config file '$arg{file}': $!";
# Make this into a hash so we can quickly see if we're looking
# for a particular option
my %looking_for;
if (ref $arg{options} eq 'ARRAY') {
%looking_for = map { $_ => 1 } @{$arg{options}};
}
while(<$config_fh>) {
chomp;
s/#.*//; # delete comments
s/\s+$//; # delete trailing spaces
next if m/^$/; # skip empty lines
if (m/^[A-Z_]+=\S+/) {
my ($key, $value) = split(/=/, $_, 2);
$key =~ tr/A-Z_/a-z-/;
# Only set options we're looking for
next if (%looking_for and not $looking_for{$key});
# Don't set options that are already set
next if defined $arg{opthash}->{$key};
$arg{verbose} and print STDERR "Slack::read_config: Setting '$key' to '$value'\n";
$arg{opthash}->{$key} = $value;
} else {
cluck "Slack::read_config: Garbage line '$_' in '$arg{file}' line $. ignored";
}
}
close($config_fh)
or confess "Slack::read_config: Could not close config file: $!";
# The verbose option is treated specially in so many places that
# we need to make sure it's defined.
$arg{opthash}->{verbose} ||= 0;
return $arg{opthash};
}
# Just get the exit code from a command that failed.
# croaks if anything weird happened.
sub get_system_exit (@) {
my @command = @_;
if (WIFEXITED($?)) {
my $exit = WEXITSTATUS($?);
return $exit if $exit;
}
if (WIFSIGNALED($?)) {
my $sig = WTERMSIG($?);
croak "'@command' caught sig $sig";
}
if ($!) {
croak "Syserr on system '@command': $!";
}
croak "Unknown error on '@command'";
}
sub check_system_exit (@) {
my @command = @_;
my $exit = get_system_exit(@command);
# Exit is non-zero if get_system_exit() didn't croak.
croak "'@command' exited $exit";
}
# get options from the command line and the config file
# Arguments
# opthash => hashref in which to store options
# usage => usage statement
# required_options => arrayref of options to require -- an exception
# will be thrown if these options are not defined
# command_line_hash => store options specified on the command line here
sub get_options {
my %arg = @_;
use Getopt::Long;
Getopt::Long::Configure('bundling');
if (not defined $arg{opthash}) {
$arg{opthash} = {};
}
if (not defined $arg{usage}) {
$arg{usage} = default_usage($0);
}
my @extra_options = (); # extra arguments to getoptions
if (defined $arg{command_line_options}) {
@extra_options = @{$arg{command_line_options}};
}
# Make a --quiet function that turns off verbosity
$arg{opthash}->{quiet} = sub { $arg{opthash}->{verbose} = 0; };
unless (GetOptions($arg{opthash},
@default_options,
@extra_options,
)) {
print STDERR $arg{usage};
exit 1;
}
if ($arg{opthash}->{help}) {
print $arg{usage};
exit 0;
}
if ($arg{opthash}->{version}) {
print "slack version $VERSION\n";
exit 0;
}
# Get rid of the quiet handler
delete $arg{opthash}->{quiet};
# If we've been given a hashref, save our options there at this
# stage, so the caller can see what was passed on the command line.
# Unfortunately, perl has no .replace function, so we iterate.
if (ref $arg{command_line_hash} eq 'HASH') {
while (my ($k, $v) = each %{$arg{opthash}}) {
$arg{command_line_hash}->{$k} = $v;
}
}
# Use the default config file
if (not defined $arg{opthash}->{config}) {
$arg{opthash}->{config} = $DEFAULT_CONFIG_FILE;
}
# We need to decide whether to be verbose about reading the config file
# Currently we just do it if global verbosity > 2
my $verbose_config = 0;
if (defined $arg{opthash}->{verbose}
and $arg{opthash}->{verbose} > 2) {
$verbose_config = 1;
}
# Read options from the config file, passing along the options we've
# gotten so far
read_config(
file => $arg{opthash}->{config},
opthash => $arg{opthash},
verbose => $verbose_config,
);
# The "verbose" option gets compared a lot and needs to be defined
$arg{opthash}->{verbose} ||= 0;
# The "hostname" option is set specially if it's not defined
if (not defined $arg{opthash}->{hostname}) {
use Sys::Hostname;
$arg{opthash}->{hostname} = hostname;
}
# We can require some options to be set
if (ref $arg{required_options} eq 'ARRAY') {
for my $option (@{$arg{required_options}}) {
if (not defined $arg{opthash}->{$option}) {
croak "Required option '$option' not given on command line or specified in config file!\n";
}
}
}
return $arg{opthash};
}
sub prompt ($) {
my ($prompt) = @_;
if (not defined $term) {
require Term::ReadLine;
$term = new Term::ReadLine 'slack'
}
$term->readline($prompt);
}
# Calls the callback on absolute pathnames of files in the source directory,
# and also on names of directories that don't exist in the destination
# directory (i.e. where $source/foo exists but $destination/foo does not).
sub find_files_to_install ($$$) {
my ($source, $destination, $callback) = @_;
return find ({
wanted => sub {
if (-l or not -d _) {
# Copy all files, links, etc
my $file = $File::Find::name;
&$callback($file);
} elsif (-d _) {
# For directories, we only want to copy it if it doesn't
# exist in the destination yet.
my $dir = $File::Find::name;
# We know the root directory will exist (we make it above),
# so skip the base of the source
(my $short_source = $source) =~ s#/$##;
return if $dir eq $short_source;
# Strip the $source from the path,
# so we can build the destination dir from it.
my $subdir = $dir;
($subdir =~ s#^$source##)
or croak "sub failed: $source|$subdir";
if (not -d "$destination/$subdir") {
&$callback($dir);
}
}
}
},
$source,
);
}
# Runs rsync with the necessary redirection to its filehandles
sub wrap_rsync (@) {
my @command = @_;
my ($pid);
if ($pid = fork) {
# Parent
} elsif (defined $pid) {
# Child
open(STDIN, "<", "/dev/null")
or die "Could not redirect STDIN from /dev/null\n";
# This redirection is necessary because rsync sends
# verbose output to STDOUT
open(STDOUT, ">&STDERR")
or die "Could not redirect STDOUT to STDERR\n";
exec(@command);
die "Could not exec '@command': $!\n";
} else {
die "Could not fork: $!\n";
}
my $kid = waitpid($pid, 0);
if ($kid != $pid) {
die "waitpid returned $kid\n";
} elsif ($?) {
Slack::check_system_exit(@command);
}
}
# Runs rsync with the necessary redirection to its filehandles, but also
# returns an FH to stdin and a PID.
sub wrap_rsync_fh (@) {
my @command = @_;
my ($fh, $pid);
if ($pid = open($fh, "|-")) {
# Parent
} elsif (defined $pid) {
# Child
# This redirection is necessary because rsync sends
# verbose output to STDOUT
open(STDOUT, ">&STDERR")
or die "Could not redirect STDOUT to STDERR\n";
exec(@command);
die "Could not exec '@command': $!\n";
} else {
die "Could not fork: $!\n";
}
return($fh, $pid);
}
1;