279 lines
8.7 KiB
Perl
Executable File
279 lines
8.7 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
# $Id: slack-stage 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 local cache
|
|
# directory to the local stage, building a unified single tree onstage
|
|
# from the multiple trees that are the role + subroles in the cache
|
|
|
|
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 constant LIB_DIR => '/usr/lib/slack';
|
|
use lib LIB_DIR;
|
|
use Slack;
|
|
|
|
my @rsync = ('rsync',
|
|
'--recursive',
|
|
'--times',
|
|
'--ignore-times',
|
|
'--perms',
|
|
'--sparse',
|
|
);
|
|
|
|
(my $PROG = $0) =~ s#.*/##;
|
|
|
|
sub check_stage ();
|
|
sub sync_role ($$@);
|
|
sub apply_default_perms_to_role ($$);
|
|
|
|
########################################
|
|
# 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> [<role>...]");
|
|
$usage .= <<EOF;
|
|
|
|
--subdir DIR
|
|
Sync this subdir only. Possible values for DIR are 'files' and
|
|
'scripts'.
|
|
EOF
|
|
# Option defaults
|
|
my %opt = ();
|
|
Slack::get_options(
|
|
opthash => \%opt,
|
|
command_line_options => [
|
|
'subdir=s',
|
|
],
|
|
usage => $usage,
|
|
required_options => [ qw(cache stage) ],
|
|
);
|
|
|
|
# Arguments are required
|
|
die "No roles given!\n\n$usage" unless @ARGV;
|
|
|
|
# We only allow certain values for this option
|
|
if ($opt{subdir}) {
|
|
unless ($opt{subdir} eq 'files' or $opt{subdir} eq 'scripts') {
|
|
die "--subdir option must be 'files' or 'scripts'\n\n$usage";
|
|
}
|
|
} else {
|
|
$opt{subdir} = '';
|
|
}
|
|
|
|
# Prepare for backups
|
|
if ($opt{backup} and $opt{'backup-dir'}) {
|
|
# Make sure backup directory exists
|
|
unless (-d $opt{'backup-dir'}) {
|
|
($opt{verbose} > 0) and print STDERR "Creating backup directory '$opt{'backup-dir'}'\n";
|
|
if (not $opt{'dry-run'}) {
|
|
eval { mkpath($opt{'backup-dir'}); };
|
|
die "Could not mkpath backup dir '$opt{'backup-dir'}': $@\n" if $@;
|
|
}
|
|
}
|
|
push(@rsync, "--backup", "--backup-dir=$opt{'backup-dir'}");
|
|
}
|
|
|
|
# Pass options along to rsync
|
|
if ($opt{'dry-run'}) {
|
|
push @rsync, '--dry-run';
|
|
}
|
|
# Pass options along to rsync
|
|
if ($opt{'verbose'} > 1) {
|
|
push @rsync, '--verbose';
|
|
}
|
|
# }}}
|
|
|
|
# copy over the new files
|
|
for my $full_role (@ARGV) {
|
|
# Split the full role (e.g. google.foogle.woogle) into components
|
|
my @role_parts = split(/\./, $full_role);
|
|
die "Internal error: Expect at least one role part" if not @role_parts;
|
|
# Reassemble parts one at a time onto @role and sync as we go,
|
|
# so we do "google", then "google.foogle", then "google.foogle.woogle"
|
|
my @role = ();
|
|
# Make sure we've got the right perms before we copy stuff down
|
|
check_stage();
|
|
|
|
# For the base role, do both files and scripts.
|
|
push @role, shift @role_parts;
|
|
for my $subdir(qw(files scripts)) {
|
|
if (not $opt{subdir} or $opt{subdir} eq $subdir) {
|
|
($opt{verbose} > 1)
|
|
and print STDERR "$PROG: Calling sync_role for $full_role, @role\n";
|
|
# @role here will have one element, so sync_role will use --delete
|
|
sync_role($full_role, $subdir, @role)
|
|
}
|
|
}
|
|
|
|
# For all subroles, just do the files.
|
|
# (If we wanted script subroles to work like files, we'd get rid of this
|
|
# distinction and simplify the code.)
|
|
if (not $opt{subdir} or $opt{subdir} eq 'files') {
|
|
while (@role_parts) {
|
|
push @role, shift @role_parts;
|
|
($opt{verbose} > 1)
|
|
and print STDERR "$PROG: Calling sync_role for $full_role, @role\n";
|
|
sync_role($full_role, 'files', @role);
|
|
}
|
|
}
|
|
|
|
for my $subdir (qw(files scripts)) {
|
|
apply_default_perms_to_role($full_role, $subdir)
|
|
if (not $opt{subdir} or $opt{subdir} eq $subdir);
|
|
}
|
|
}
|
|
exit 0;
|
|
|
|
# Make sure the stage directory exists and is mode 0700, to protect files
|
|
# underneath in transit
|
|
sub check_stage () {
|
|
my $stage = $opt{stage} . "/roles";
|
|
if (not $opt{'dry-run'}) {
|
|
if (not -d $stage) {
|
|
($opt{verbose} > 0) and print STDERR "$PROG: Creating '$stage'\n";
|
|
eval { mkpath($stage); };
|
|
die "Could not mkpath cache dir '$stage': $@\n" if $@;
|
|
}
|
|
($opt{verbose} > 0) and print STDERR "$PROG: Checking perms on '$stage'\n";
|
|
if ($> != 0) {
|
|
warn "WARNING[$PROG]: Not superuser; unable to chown files\n";
|
|
} else {
|
|
chown(0, 0, $stage)
|
|
or die "Could not chown 0:0 '$stage': $!\n";
|
|
}
|
|
chmod(0700, $stage)
|
|
or die "Could not chmod 0700 '$stage': $!\n";
|
|
}
|
|
}
|
|
|
|
# Copy the files for a role from CACHE to STAGE
|
|
sub sync_role ($$@) {
|
|
my ($full_role, $subdir, @role) = @_;
|
|
my @this_rsync = @rsync;
|
|
|
|
# If we were only given one role part, we're in the base role
|
|
my $in_base_role = (scalar @role == 1);
|
|
|
|
# For the base role, delete any files that don't exist in the cache.
|
|
# Not for the subrole (otherwise we'll delete all files not in
|
|
# the subrole, which may be most of them!)
|
|
if ($in_base_role) {
|
|
push @this_rsync, "--delete";
|
|
}
|
|
|
|
# (a) => a/files
|
|
# (a,b,c) => a/files.b.c
|
|
my $src_path = $role[0].'/'.join(".", $subdir, @role[1 .. $#role]);
|
|
# This one's a little simpler:
|
|
my $dst_path = $full_role.'/'.$subdir;
|
|
|
|
# final / is important for rsync
|
|
my $source = $opt{cache} . "/roles/" . $src_path . "/";
|
|
my $destination = $opt{stage} . "/roles/" . $dst_path . "/";
|
|
if (not -d $destination and -d $source) {
|
|
($opt{verbose} > 0) and print STDERR "$PROG: Creating '$destination'\n";
|
|
if (not $opt{'dry-run'}) {
|
|
eval { mkpath($destination); };
|
|
die "Could not mkpath stage dir '$destination': $@\n" if $@;
|
|
}
|
|
}
|
|
|
|
# We no longer require the source to exist
|
|
if (not -d $source) {
|
|
# but we need to remove the destination if the source
|
|
# doesn't exist and we're in the base role
|
|
if ($in_base_role) {
|
|
rmtree($destination);
|
|
# rmtree() doesn't throw exceptions or give a return value useful
|
|
# for detecting failure, so we just check after the fact.
|
|
die "Could not rmtree '$destination' when '$source' missing\n"
|
|
if -e $destination;
|
|
}
|
|
# if we continue, rsync will fail because source is missing,
|
|
# so we don't.
|
|
return;
|
|
}
|
|
|
|
# All this to run an rsync command
|
|
my @command = (@this_rsync, $source, $destination);
|
|
($opt{verbose} > 0) and print STDERR "$PROG: Syncing $src_path with '@command'\n";
|
|
Slack::wrap_rsync(@command);
|
|
}
|
|
|
|
# This just takes the base role, and chowns/chmods everything under it to
|
|
# give it some sensible permissions. Basically, the only thing we preserve
|
|
# about the original permissions is the executable bit, since that's the
|
|
# only thing source code controls systems like CVS, RCS, Perforce seem to
|
|
# preserve.
|
|
sub apply_default_perms_to_role ($$) {
|
|
my ($role, $subdir) = @_;
|
|
my $destination = $opt{stage} . "/roles/" . $role;
|
|
|
|
if ($subdir) {
|
|
$destination .= '/' . $subdir;
|
|
}
|
|
|
|
# If the destination doesn't exist, it's probably because the source didn't
|
|
return if not -d $destination;
|
|
|
|
($opt{verbose} > 0) and print STDERR "$PROG: Setting default perms on $destination\n";
|
|
if ($> != 0) {
|
|
warn "WARNING[$PROG]: Not superuser; won't be able to chown files\n";
|
|
}
|
|
# Use File::Find to recurse the directory
|
|
find({
|
|
# The "wanted" subroutine is called for every directory entry
|
|
wanted => sub {
|
|
return if $opt{'dry-run'};
|
|
($opt{verbose} > 2) and print STDERR "$File::Find::name\n";
|
|
if (-l) {
|
|
# symlinks shouldn't be in here,
|
|
# since we dereference when copying
|
|
warn "WARNING[$PROG]: Skipping symlink at $File::Find::name: $!\n";
|
|
return;
|
|
} elsif (-f _) { # results of last stat saved in the "_"
|
|
if (-x _) {
|
|
chmod 0555, $_
|
|
or die "Could not chmod 0555 $File::Find::name: $!";
|
|
} else {
|
|
chmod 0444, $_
|
|
or die "Could not chmod 0444 $File::Find::name: $!";
|
|
}
|
|
} elsif (-d _) {
|
|
chmod 0755, $_
|
|
or die "Could not chmod 0755 $File::Find::name: $!";
|
|
} else {
|
|
warn "WARNING[$PROG]: Unknown file type at $File::Find::name: $!\n";
|
|
}
|
|
return if $> != 0; # skip chowning if not superuser
|
|
chown 0, 0, $_
|
|
or die "Could not chown 0:0 $File::Find::name: $!";
|
|
},
|
|
# end of wanted function
|
|
},
|
|
# way down here, we have the directory to traverse with File::Find
|
|
$destination,
|
|
);
|
|
}
|