Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1003 lines (815 sloc) 25.8 KB
#!/usr/bin/perl
# -*- mode: cperl; cperl-continued-brace-offset: -4; indent-tabs-mode: nil; -*-
# vim:shiftwidth=2:tabstop=8:expandtab:textwidth=78:softtabstop=4:ai:
# $Id$
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# (C) Copyright Ticketmaster, Inc. 2007
#
our $VERSION = sprintf("%d", q$Revision$ =~ /(\d+)/);
use strict;
use lib q(/usr/lib/spine-mgmt);
my $perl_ver = sprintf "%vd", $^V;
my $error_count = 0;
use Fcntl qw(:DEFAULT :flock);
use Getopt::Long;
use IO::File;
use Net::Domain qw(hostfqdn);
use Storable;
use Spine::ConfigFile;
use Spine::Constants qw(:basic :plugin);
use Spine::Data;
use Spine::Registry;
use Spine::State;
use Spine::Util;
use POSIX qw(ctime);
use File::Spec::Functions;
use Sys::Syslog;
use sigtrap 'handler' => \&ignore_signal, 'normal-signals';
use constant NODEFILE => '/etc/nodename';
use constant LOCKFILE => '/var/run/spine-mgmt.lock';
use constant DEFAULT_CONFIGFILE => '/etc/spine-mgmt/spine-mgmt.conf';
use constant DEFAULT_CONFIGFILE_FALLBACK => ('/etc/spine-config.conf',\
'/etc/spine.conf');
use constant SPINE_PHASES => qw(PREPARE EMIT APPLY CLEAN);
use constant DEFAULT_CONFIG => {
spine => {
StateDir => '/var/spine-mgmt',
ConfigSource => 'ISO9660',
Profile => 'StandardPlugins',
Parser => 'pureTT',
SyslogIdent => 'spine-mgmt',
SyslogFacility => 'local3',
SyslogOptions => 'ndelay,pid',
},
DefaultPlugins => {
'DISCOVERY/populate' => 'DryRun SystemInfo',
'DISCOVERY/policy-selection' => 'DescendOrder',
'PARSE/complete' => 'Interpolate',
},
FileSystem => {
Path => '/software/spine/config'
},
ISO9660 => {
URL => 'http://repository/cgi-bin/rcrb.pl',
Destination => '/var/spine-mgmt/configballs',
Timeout => 5
},
StandardPlugins => {
PREPARE => 'PrintData Templates Overlay',
EMIT => 'Templates',
APPLY => 'Overlay RestartServices Finalize',
CLEAN => 'Overlay'
},
FirstRun => {
PREPARE => 'PrintData Templates Overlay',
EMIT => 'FirstRun Templates',
APPLY => 'Overlay Finalize',
CLEAN => 'Overlay',
}
};
#
# These are package level variables so that the various supporting modules and
# plugins can access them directly via $::CONFIG. Should this stuff get
# registered with Spine::Registry instead?
#
our ($SAVE_STATE, $DEBUG, $CONFIG, %OPTIONS);
#
# $SAVE_STATE is used by Spine::Plugin::Templates::quick_template and
# Spine::Plugin::Prindata::printdata at the moment to tell the driver script
# to not bother storing the state for this run. We really need a better
# mechanism for it but this'll do for now.
#
# rtilder Thu Jan 4 11:44:24 PST 2007
#
$SAVE_STATE = 1;
$DEBUG = $ENV{SPINE_DEBUG} || 0;
# Turn off buffering
if ($DEBUG)
{
select STDERR;
$| = 1;
select STDOUT;
$| = 1;
}
#
# Grab only our config file so we can load our plugins
#
Getopt::Long::Configure('pass_through');
my $profile = undef;
my $conf_file = undef;
my $quiet = undef;
my @actions = ();
GetOptions('config-file=s' => \$conf_file,
'action|plugin=s@' => \@actions,
'quiet|q' => \$quiet,
'actiongroup|profile=s' => \$profile);
Getopt::Long::Configure('no_pass_through');
my @cfiles = (DEFAULT_CONFIGFILE, DEFAULT_CONFIGFILE_FALLBACK);
if (defined($conf_file))
{
@cfiles = ($conf_file);
}
foreach my $file (@cfiles)
{
if (-f $file)
{
print "spine-mgmt: Using config file at $file\n"
unless(defined($quiet));
$CONFIG = new Spine::ConfigFile(Filename => $file, Type => 'Ini');
if (not defined($CONFIG))
{
print STDERR $Spine::ConfigFile::ERROR;
goto failure;
}
last;
}
}
if (not defined($CONFIG))
{
print STDERR "spine-mgmt: No config file found. Using hardcoded defaults.\n";
$CONFIG = DEFAULT_CONFIG;
}
$CONFIG->{spine}->{Profile} = $profile if defined($profile);
# Take care of our PluginPath if provided
if (defined($CONFIG->{spine}->{PluginPath}))
{
unshift @INC, split(/:/, $CONFIG->{spine}->{PluginPath});
}
# Run openlog() once so any plugin can use it with the user-configured
# facility, options, and program name (or the defaults).
openlog($CONFIG->{spine}->{SyslogIdent} ?
$CONFIG->{spine}->{SyslogIdent} :
DEFAULT_CONFIG->{spine}->{SyslogIdent},
$CONFIG->{spine}->{SyslogFacility} ?
$CONFIG->{spine}->{SyslogFacility} :
DEFAULT_CONFIG->{spine}->{SyslogFacility},
$CONFIG->{spine}->{SyslogOptions} ?
$CONFIG->{spine}->{SyslogOptions} :
DEFAULT_CONFIG->{spine}->{SyslogOptions});
#
# If specific actions were requested, we don't use our profile,
# we build a new profile of just the actions the user wants.
#
#
# First, we need to build a case insensitive lookup table
# Back in the pre-GPL days, the old codebase was case insensitive
# about the then-equivalent of profiles. So we try to provide some
# backwards compatibility and not make the user know the case
# of profiles and actions.
#
my %lc_config_keys = map { lc($_) => $_ } keys(%$CONFIG);
if (scalar(@actions) > 0)
{
$CONFIG->{spine}->{Profile} = '__action';
# Use a hash for uniqueness
my $action_profile = {};
foreach my $action (@actions) {
my $lcaction = lc($action);
unless (exists $lc_config_keys{'action_' . $lcaction}) {
print STDERR "Action $action is not a valid action!\n";
goto failure;
}
my $real_action_name = $lc_config_keys{'action_' . $lcaction};
my $action_map = $CONFIG->{$real_action_name};
foreach my $phase (keys(%{$action_map}))
{
unless (exists($action_profile->{$phase}))
{
$action_profile->{$phase} = {};
}
unless ($action_map->{$phase} eq '')
{
foreach my $method (split(/\s+/, $action_map->{$phase}))
{
$action_profile->{$phase}->{$method} = undef;
}
}
}
}
# Now we actually make our profile
foreach my $phase (keys(%{$action_profile}))
{
$CONFIG->{$CONFIG->{spine}->{Profile}}->{$phase} =
join(' ', keys(%{$action_profile->{$phase}}));
}
}
#
# Prepare our profile - if any phase was left out, we fill it in
# with defaults, if we have them.
#
# First grab the proper casing
my $real_profile_name = $lc_config_keys{lc($CONFIG->{spine}->{Profile})};
$CONFIG->{spine}->{Profile} = $real_profile_name;
foreach my $phase (keys(%{$CONFIG->{DefaultPlugins}}))
{
unless (exists($CONFIG->{$CONFIG->{spine}->{Profile}}->{$phase})) {
$CONFIG->{$CONFIG->{spine}->{Profile}}->{$phase} =
$CONFIG->{DefaultPlugins}->{$phase};
}
}
# Set up our plugin registry singleton object for this process
my $registry = new Spine::Registry($CONFIG);
# Create our hookable points for plugins
$registry->create_hook_point(SPINE_PHASES);
# Now attempt to load all our plugins
while (my ($phase, $plugins) = each(%{$CONFIG->{$CONFIG->{spine}->{Profile}}})) {
my @plugins = split(/(?:\s*,?\s+)/, $plugins);
unless ($registry->load_plugin(@plugins) == SPINE_SUCCESS) {
print STDERR "Failed to load at least one plugin!\n";
goto failure;
}
}
# No plugins loaded? Not ok!
unless (scalar(keys(%{$registry->{PLUGINS}})))
{
print STDERR "Didn't load any plugins. Library path or config problem!\n";
goto failure;
}
#
# Time to process command line options
#
# First, we handle just this script's options
Getopt::Long::Configure('pass_through');
unless (GetOptions(\%OPTIONS,
'croot=s',
'help|h',
'autofqdn',
'verbosity=s',
'config-source=s',
'release=s',
'freeze',
'hostname|as=s',
'version',
'last|which|what|huh|wtf')) {
usage();
goto failure;
}
Getopt::Long::Configure('no_pass_through');
# Help option.
if ($OPTIONS{help})
{
usage();
goto finished;
}
# Now we get our plugin options
unless (GetOptions(%{$registry->get_options()}))
{
print STDERR "Invalid option.\n";
goto failure;
}
# Now grab a copy of our previous state information
my $prev_state = new Spine::State($CONFIG);
$prev_state->load();
if (defined($OPTIONS{version}))
{
print 'Spine version: ', $VERSION, "\n";
goto finished;
}
# If we're asked to report what we're currently configured with, do so and exit
if (defined($OPTIONS{last}))
{
if (defined($prev_state->release()))
{
print "Last configured:\n";
print "\tVersion: ", $prev_state->version(), "\n";
print "\tRelease: ", $prev_state->release();
if (get_frozen_release($CONFIG->{spine}->{StateDir}) ==
$prev_state->release())
{
print ' (frozen)';
}
print "\n";
print "\tDate: ", ctime($prev_state->run_time());
}
else
{
print "No previous config found.\n";
}
goto finished;
}
# Get our configuration tree location
my $source = get_source(\%OPTIONS, $CONFIG);
unless (defined($source))
{
# Error reporting is handled by get_source()
goto failure;
}
#
# Figure out what release we're at.
#
my $release = get_release($source, \%OPTIONS, $CONFIG);
if (not defined($release)) # There was an error
{
print STDERR "Couldn't find a config for that release. Bailing.\n";
goto failure;
}
unless(defined($quiet))
{
print 'spine-mgmt core: Using configuration: ',
$source->source_info(), "\n";
}
# Determine the hostname.
my ($hostname, $file_hostname);
if (defined($OPTIONS{hostname}))
{
$hostname = $OPTIONS{hostname};
}
elsif ( $file_hostname = read_nodefile(NODEFILE) )
{
$hostname = $file_hostname;
}
else
{
$hostname = hostfqdn() if ($OPTIONS{autofqdn});
}
# Set default verbosity
my $verbosity;
if ( exists $OPTIONS{verbosity} )
{
$verbosity = $OPTIONS{verbosity};
}
else
{
$verbosity = 2;
}
# Announce what we're doing
unless(defined($quiet))
{
print 'spine-mgmt: starting Spine v' . $VERSION .
' -- configuration release ', $source->release(), "\n";
print "spine-mgmt: initializing data for $hostname\n";
}
if ( ${%{$registry->get_options()}->{dryrun}} ) {
unless(defined($quiet))
{
print "spine-mgmt: running in dryrun mode\n";
}
syslog("info", "running in dryrun mode");
}
# Create the configuration object.
my $c = Spine::Data->new(hostname => $hostname,
verbosity => $verbosity,
quiet => $quiet,
source => $source,
release => $release,
config => $CONFIG);
# This should be a redundant check now
#
# rtilder Tue Jun 27 12:23:39 PDT 2006
if (not defined($c)) {
print STDERR 'spine-mgmt initialization: Errors encountered parsing '
. "data tree.\n";
goto failure;
}
# Our data object will contain the c_failure key if we
# have had a critical parsing error.
if ($c->getval('c_failure'))
{
print STDERR 'spine-mgmt initialization: Errors encountered parsing '
. "data tree.\n";
goto failure;
}
# A quick sanity check, just to be sure
if ($c->{c_release} != $source->release())
{
print STDERR "Requested release \"$c->{c_release}\" doesn't match "
. "release parsed\n";
print STDERR 'from config source "', $source->release(), "\"\n";
goto failure;
}
# Populate our current runtime state information
my $state = new Spine::State($CONFIG);
# Make sure our state can save itself.
$state->data($c);
# Establish a lock before we run any actions.
my $lock_fh = get_lock();
unless ($lock_fh)
{
$c->error('could not get exclusive lock', 'crit');
goto failure;
}
# Now that we have a lock and are potentially going to
# make changes, check to be sure we aren't in dryrun mode
# and unfreeze the system.
if ( ! ${%{$registry->get_options()}->{dryrun}} ) {
unlink catfile($CONFIG->{spine}->{StateDir}, 'FrozenAtRelease');
}
PHASE: foreach my $pork ( ( ['PREPARE', 'Failed to prepare for emission!'],
['EMIT', 'Failed to emit configuration!'],
['APPLY', 'Failed to apply configuration!'],
['CLEAN', 'Failed to clean up!'] ) ) {
my ($phase, $msg) = @{$pork};
my $point = $registry->get_hook_point($phase);
unless ($point->register_hooks() == SPINE_SUCCESS) {
print STDERR "Error registering hooks!\n";
last;
}
my (undef, $rc, $errors) = $point->run_hooks($c, $prev_state);
$error_count += $errors;
if (($rc & PLUGIN_FATAL) == PLUGIN_FATAL) {
print STDERR "\tExiting as gracelessly as possible.\n";
goto failure;
} elsif ($rc & PLUGIN_EXIT) {
#
# Need a better messaging option. This message is causing some
# confusion. :\
#
# rtilder Wed May 9 10:33:20 PDT 2007
#
print STDOUT "\tAt least one plugin requested a clean exit.\n"
unless(defined($quiet));
last PHASE;
}
}
# Clean up our config tree now that we're all done with it
$source->clean();
if ($SAVE_STATE)
{
# Store our config object to disk.
unless(defined($quiet))
{
print STDERR "Saving state...\n";
}
unless ($state->store())
{
$c->error('failed to write session object [' . $state->error . ']',
'err');
}
# Store the hostname for future use only if it was explicitly
# specified as an argument.
if ($OPTIONS{hostname} or $OPTIONS{autofqdn})
{
write_nodefile(NODEFILE, $hostname);
}
#
# Doesn't freeze if it doesn't need to but only return non-true on error
# while trying to freeze.
#
unless (freeze_release($release, \%OPTIONS, $CONFIG,
catfile($state->{StateDir}, 'FrozenAtRelease'))) {
print STDERR "Failed to freeze state!\n";
goto failure;
}
}
# Figure out if we had lame keys and report the number.
if ( defined $c->{c_lame_keys} )
{
my $size = keys %{$c->{c_lame_keys}};
print "spine-mgmt: Encountered $size keys with lame TT syntax!\n"
unless(defined($quiet));
}
finished:
# Close syslog
closelog();
# Release our exclusive lock and exit.
release_lock();
if ($error_count)
{
exit(1);
}
exit(0);
failure:
# Close syslog
closelog();
release_lock();
exit(1);
END {
release_lock();
}
sub read_nodefile
{
my $file = shift;
my $hostname = undef;
unless (-f $file) {
print STDERR "Node file \"$file\" doesn't exist!\n";
return undef;
}
my $fh = new IO::File("< $file");
unless (defined($fh)) {
print STDERR "Couldn't open node file \"$file\": $!\n";
return undef;
}
$hostname = <$fh>;
$fh->close();
chomp $hostname;
$hostname =~ s/\s*//g;
return $hostname;
}
sub write_nodefile
{
my $file = shift;
my $hostname = shift;
my $fh = new IO::File("> $file");
unless (defined($fh)) {
print STDERR "Couldn't open node file \"$file\": $!\n";
return 0;
}
print $fh "$hostname\n";
$fh->close();
return 1;
}
sub get_lock
{
sysopen(LOCK_FH, LOCKFILE, O_RDWR|O_CREAT)
|| return undef;
flock(LOCK_FH, LOCK_EX|LOCK_NB)
|| return undef;
return *LOCK_FH;
}
sub release_lock
{
flock(LOCKFILE, LOCK_UN);
close(LOCKFILE);
}
sub ignore_signal
{
my $signal = shift;
$SIG{$signal} = 'IGNORE';
print STDERR "Caught signal $signal.\n";
if (-t STDIN and ($signal eq 'INT' or $signal eq 'TERM')) {
release_lock();
exit(1);
}
return 1;
}
sub get_frozen_release
{
my $dir = shift;
my $frozen = "$dir/FrozenAtRelease";
if (not -f $frozen)
{
return '';
}
if (-z $frozen)
{
print STDERR "Frozen release file \"$frozen\" exists but is 0 bytes.\n";
return undef;
}
my $fh = new IO::File("< $frozen");
if (not defined($fh))
{
print STDERR "Failed to open frozen release file \"$frozen\": $!\n";
return undef;
}
my $release = $fh->getline();
$fh->close();
chomp($release);
$release =~ s/\s*//g;
return $release;
}
#
# Handles determining and instantiating our configuration source.
#
# Command line options override the config file, obviously
#
sub get_source
{
my $options = shift;
my $config = shift;
my $source_type = 'Spine::ConfigSource::' . $config->{spine}->{ConfigSource};
my $source = undef;
# Config root option. We handle this separately so that we can can pass
# in the Path parameter explicitly
#
if (defined($options->{croot})) {
require Spine::ConfigSource::FileSystem;
$source = new Spine::ConfigSource::FileSystem(Config => $config,
Path => $options->{croot});
if (not defined($source)) {
print STDERR 'Failed to initialize config root from command line:'
. " $Spine::ConfigSource::FileSystem::ERROR\n";
#XXX: I don't think we want to fall back to the config if the
# the caller requested us to use something else
goto failure;
#print STDERR "Falling back to configuration file settings.\n";
}
}
# If --config-source was provided, use that.
#
elsif (defined($options->{'config-source'})) {
$source_type = 'Spine::ConfigSource::' . $options->{'config-source'};
}
# If we were passed --croot=/foo then we should already have a valid config
# source and we can skip this section
if (not defined($source)) {
# Try to instantiate the appropriate Spine::ConfigSource object
# specififed in the config file.
# Has to be done in this way. See perldoc -f require and search for
# "::" for details on why
eval "require $source_type";
if ($@) {
print STDERR "Failed to load the $source_type configuration "
. "source: $@\n";
return undef;
}
eval
{
no strict 'refs';
$source = $source_type->new(Config => $config);
use strict 'refs';
};
if ($@) {
print STDERR "Failed to initialize configuration source: $@\n";
return undef;
}
}
#
# If our configuration source still isn't a valid object, we error out.
#
if (not defined($source)) {
my $foo;
no strict 'refs';
eval { $foo = ${$source_type . '::ERROR'} };
print STDERR 'Failure to initialize configuration source '
. "$source_type: $foo\n";
use strict 'refs';
return undef;
}
return $source;
}
sub get_release
{
my ($source, $options, $config) = @_;
#
# Check to see if we're frozen at a release. If we are, use that release
# for all our future endeavours!
#
my $release = get_frozen_release($config->{spine}->{StateDir});
# Was there an error retrieving the frozen release?
#
if (not defined($release))
{
print STDERR "Can't parse frozen release. Bailing. Delete or fix.\n";
goto release_failed;
}
#
# --release on the command line overrides frozen release
#
if (exists($options->{release}))
{
if ($options->{release} eq 'latest') {
$release = '';
} else {
$release = $options->{release};
}
}
# If it's an empty string, we want to snag some updates, yo!
if ($release eq '') {
$release = $source->check_for_update($prev_state->release())
}
if (not defined($release)) {
print STDERR 'Failed while checking for newer configuration '
. "releases: $source->{error}\n";
goto release_failed;
}
if ($release) {
if (not defined($source->retrieve($release))) {
print STDERR 'Failed to retrieve latest configuration release: '
. "$source->{error}\n";
goto release_failed;
}
}
if (not defined($source->config_root()))
{
print STDERR 'Couldn\'t mount or find the configuration source: '
. "$source->{error}\n";
goto release_failed;
}
return $source->release();
release_failed:
return undef;
}
#
# Freeze if specified
#
# Defined by http://bugz.tm.tmcs/show_bug.cgi?id=27820#c0
#
# Basically:
#
# If --release=latest is used we never freeze.
#
# If --freeze was specified on the command line, always freeze.
#
# If AutoFreeze is enabled on the command line and a specific release
# number is specified via --release, then freeze on that.
#
sub freeze_release
{
my $release = shift;
my $options = shift;
my $config = shift;
my $freeze_file = shift;
# Print out a special warning for --release=latest and --freeze on the
# command line
if ($options->{release} eq 'latest' and exists($options->{freeze})) {
print STDERR "WARNING: Won't freeze when --release=\"latest\"\n";
# Don't return an error, though.
return 1;
}
if (exists($options->{freeze})
or (exists($options->{release})
and $options->{release} ne 'latest'
and $config->{Spine}->{AutoFreeze} =~ m/(?:yes|true|on|1)/i) ) {
my $fh = new IO::File("> $freeze_file");
if (not defined($fh)) {
print STDERR "Failed to open $freeze_file: $!\n";
return 0;
}
print $fh "$release\n";
$fh->close();
}
return 1;
}
sub usage
{
print STDERR (<<EOF);
Spine v$VERSION -- Configuration management system.
Usage: spine-mgmt [options]
--action <action>
Run the specified action(s). Actions are defined
much like profiles in the config file, but begin
with 'action_'. The difference is that actions are
small pieces that can be stacked together. When using
an action or actions, your profile is ignored.
However, the DefaultPlugins section of the config is
still used to fill in missing phases where possible.
You may run multiple actions by repeating this
option.
--croot <directory>
Location of the configuration hierarchy.
By default, this path will be relative
to the spine-mgmt executable in config/.
--autofqdn
Attempt to determine the FQDN. Use this
option if /etc/nodename has not been
generated.
--dryrun
Do not actually make changes to the system.
Only report the actions which would be taken.
--printdata
--spinaltap
Only print out the data object and
exit. No actions will be run.
--printauth
--with-auth
Only print out the auth data object and exit.
No actions will be run.
--printall
Shortcut for --printdata and --printauth.
--profile <profile>
--actiongroup <profile>
Runs the set of plugins defined in any profile
in config file. This replaces v1 actiongroups.
The term actiongroup doesn't apply any longer
as actions profiles are not strictly groups of
actions, though a profile can be written to do
the same thing as any group of actions.
--verbosity
Numeric value for setting the verbosity
level. Greater numbers mean more verbose.
The default value is 2.
--config-file <file>
The configuration file to use for this script.
The default is /etc/spine-mgmt/spine-mgmt.conf.
--config-source <configuration source type>
The type of configuration source to use. Defined by
the "ConfigSource" key in the [spine] section of the
configuration file,
--release <release number | "latest">
The release to use for this run.
--freeze
--paralyze
Freeze this machine's configuration release at the
release it uses for this run. Can only be overridden
with --release on the command line.
--last
Print information about the last successful run on this
machine.
--hostname <hostname to configure for>
Use hostname to configure system
--help
This help screen.
Examples:
spine-mgmt --profile apply_auth
spine-mgmt --action apply_overlay --action process_templates
spine-mgmt --croot /tmp/testconfig --hostname some.host.name.tld
Notes:
Spine extracts all the information necessary to configure
the system from the hostname. If the hostname is provided
on the command line and results in a sucessful run, it is
stored in /etc/nodename. The hostname stored in this file
is then used for subsequent runs.
EOF
}