#!/usr/bin/perl
#
# cz-puppet -- Execute puppet apply
#
# Written by Bill MacAllister <bill@ca-zephyr.org>
# Copyright (c) 2023-2026 Bill MacAllister <bill@ca-zephyr.org>

use AppConfig qw(:argcount :expand);
use Carp;
use Getopt::Long;
use IPC::Run qw( run timeout );
use Pod::Usage;
use strict;

my $opt_conf = '/etc/cz-puppet.conf';
my $opt_debug;
my $opt_help;
my $opt_list;
my $opt_manual;

my $CONF;
my $DEBUG_TIME;

##############################################################################
# Subroutines
##############################################################################

# ----------------------------------------------------------------------
# output information

sub msg {
    (my $tmp) = @_;
    print {*STDOUT} $tmp
      or croak("Problem writing to STDOUT\n");
    return;
}

# ----------------------------------------------------------------------
# output debugging information

sub dbg {
    (my $tmp) = @_;
    my $now     = time;
    my $elapsed = $now - $DEBUG_TIME;
    msg("$now ($elapsed) $tmp \n");
    $DEBUG_TIME = $now;
    return;
}

# ----------------------------------------------------------------------
# Run a shell command line

sub run_cmd {
    my ($timeout, @cmd) = @_;

    my $in;
    my $out;
    my $err;
    my $cmd_line = 'Executing: ' . join(' ', @cmd);
    msg("$cmd_line\n");
    eval { run(\@cmd, \$in, \$out, \$err, timeout($timeout)); };
    if ($@) {
        if ($err) {
            $err .= "\n";
        }
        $err .= "ERROR executing:$cmd_line\n";
        $err .= $@;
        croak "$err\n";
    }
    if ($opt_debug) {
        if ($out) {
            msg("$out\n");
        }
        if ($err) {
            msg("INFO: $err\n");
        }
    }
    return $out;
}

# ----------------------------------------------------------------------
# Read the setup configuration file if it exists

sub read_conf {
    if ($opt_debug) {
        dbg("reading setup file $opt_conf");
    }
    $CONF = AppConfig->new({});
    $CONF->define(
        'AFS_AKLOG',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => 'not used'
        }
    );
    $CONF->define(
        'BASE_DIR',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => '/etc/cz-puppet.conf'
        }
    );
    $CONF->define(
        'KRB',
        {
            ARGCOUNT => ARGCOUNT_NONE,
            DEFAULT  => 1
        }
    );
    $CONF->define(
        'KRB5CCNAME',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => '/tmp/puppet.tgt'
        }
    );
    $CONF->define(
        'KRB_KEYTAB',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => '/etc/krb5.keytab'
        }
    );
    $CONF->define(
        'MANIFEST_ID',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => 'minimal'
        }
    );
    $CONF->define(
        'TGT_LIFETIME',
        {
            ARGCOUNT => ARGCOUNT_ONE,
            DEFAULT  => 'not used'
        }
    );

    # Read the configuration file if there is one
    if (-e $opt_conf) {
        $CONF->file($opt_conf);
    } else {
        die("ERROR: missing configuration file $opt_conf");
    }

    return;
}

# ----------------------------------------------------------------------
# Set kerberos prefix for commands if we are using kerberos

sub get_krb {
    my @cmd = ();
    if ($CONF->KRB()) {
        if (-e $CONF->KRB5CCNAME()) {
            unlink($CONF->KRB5CCNAME());
        }
        if (!-e $CONF->KRB_KEYTAB) {
            msg('ERROR: missing keytab ' . $CONF->KRB_KEYTAB . "\n");
            exit 1;
        }
        push(@cmd,
            '/usr/bin/k5start', '-f', $CONF->KRB_KEYTAB, '-U', '-t', '--');
    }
    return @cmd;
}

# ----------------------------------------------------------------------
# List available manifests

sub do_list {
    my @cmd = get_krb();
    push(@cmd, 'ls', '-1', $CONF->BASE_DIR, '/exe_*.pp');
    msg(run_cmd(60, @cmd));
    return;
}

# ----------------------------------------------------------------------
# Run puppet apply

sub do_run {
    my $this_pp = $CONF->BASE_DIR . '/exe_' . $CONF->MANIFEST_ID . '.pp';

    my @cmd = get_krb();

    push(@cmd, '/usr/bin/puppet', 'apply', $this_pp);
    msg(run_cmd(300, @cmd));

    return;
}

##############################################################################
# Main routine
##############################################################################

GetOptions(
    'conf=s' => \$opt_conf,
    'debug'  => \$opt_debug,
    'help'   => \$opt_help,
    'list'   => \$opt_list,
    'manual' => \$opt_manual
) or die("ERROR: invalid command line arguments\n");

# Flush output immediately
$| = 1;

# help the poor souls out
pod2usage(-verbose => 0) if $opt_help;
pod2usage(-verbose => 2) if $opt_manual;
my $action = 'run';
if ($ARGV[0]) {
    if ($ARGV[0] eq 'help') {
        pod2usage(-verbose => 0);
    } elsif ($ARGV[0] eq 'manual') {
        pod2usage(-verbose => 2);
    } elsif ($ARGV[0] ne 'run') {
        msg("ERROR: invalid argument $ARGV[0]\n");
        pod2usage(-verbose => 0);
    }
}

# Read the configuration file
read_conf();

if ($opt_list) {
    do_list();
} elsif ($action eq 'run') {
    do_run();
} else {
    msg("ERROR: unknown action\n");
    pod2usage(-verbose => 0);
}

exit;

__END__

=head1 NAME

cz-puppet - Execute puppet apply

=head1 SYNOPSIS

cz-puppet [list|help|manual|<manifest id>]

=head1 DESCRIPTION

This script implements a policy for executing puppet apply using
a specific directory structure for puppet manifests.  The
structure is:

      BASE_DIR
          code
              modules
                  <module 1>
                  <module 2>
                  ...
          <manifest id 1>
          <manifest id 2>
          ...

This policy is established by setting the Puppet configuration
property basemodulepath.  See the script cz-puppet-basepath for
details.

=head1 ARGUMENTS

=over 4

=item list

Lists the available files that can be feed to puppet apply to execute
puppet.  The files are of the format "exe_<manifest id>.pp".

=item <manifest id>

The <manifest id> is used to form the following file path:

     ${BASE_DIR}/exe_${MANIFEST_ID}.pp

The defaults for BASE_DIR and MANIFEST_ID are "/afs/@cell/service/puppet"
and "minimal" respectively.

=item help

Display usage information.

=item manual

Display the man page for this script.

=back

=head1 CONFIGURATION

The optional configuration file /etc/cz-puppet.conf is a simple bash
fragment file that is source'd if it exists.

=head2 Configuration Properties

=over 4

=item BASE_DIR

The BASE_DIR is the path to the top of the Puppet manifests.

=item MANIFEST_ID

The MANIFEST_ID is the default manifest identifier for the host
that is running puppet apply.

=item KRB

If set then k5start will be used to create Kerberos ticket cache and
an AFS token before executing "puppet apply".  The default is to not
create a ticket cache or obtain an AFS token.

=item KRB_KEYTAB

The keytab to use to create a Kerberos ticket cache.  The default is
/etc/krb5.keytab.

=item AFS_AKLOG

The program to use to obtain an AFS token.  If not set then the
k5start default is used.

=back

=head1 CONFIGURATION EXAMPLE

The following configuration file uses kafs to access the puppet
code directory.  The directory is protected by AFS ACLs which
requires that an AFS token be set.

    # file: /etc/cz-puppet.conf
    BASE_DIR="/afs/mycell/service/puppet"
    MANIFEST_ID="minimal"
    KRB="YES"
    KRB_KEYTAB="/etc/krb5.keytab"
    AFS_AKLOG="/usr/bin/aklog-kafs"

=head1 SEE ALSO

cz-puppet-basepath

=head1 COPYRIGHT

Copyright (c) 2023-2026 Bill MacAllister <bill@ca-zephyr.org>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

=head1 AUTHORS

Bill MacAllister <bill@ca-zephyr.org>

=cut
