#!/usr/bin/perl -w
# #
# Software subject to following license(s):
#   Apache 2 License (http://www.opensource.org/licenses/apache2.0)
#   Copyright (c) Responsible Organization
#

# #
# Current developer(s):
#   Luis Fernando Muñoz Mejías <Luis.Munoz@UGent.be>
#   Ronald Starink <ronalds@nikhef.nl>
#

# #
# Author(s): Michel Jouvin, Ben Jones, Gabor Gombas, Nick Williams, Stijn De Weirdt
#

# #
# server, 26.2.0, 1, Tue Apr 07 2026
#
################################################################################
#
# aii-installfe      AII Remote Management Shell
#
################################################################################

=pod

=head1 NAME

aii-installfe - AII remote command line management frontend

=head1 SYNOPSIS

 aii-installfe [options] <--boot <hostname|regexp>      |
                          --bootlist <filename>         |
                          --configure <hostname|regexp> |
                          --configurelist <filename>    |
                          --install <hostname|regexp>   |
                          --installlist <filename>      |
                          --remove <hostname|regexp>    |
                          --removelist <filename>       |
                          --removeall                   |
                          --reinstall <hostname|regexp> |
                          --rescue <hostname|regex>     |
                          --rescuelist <filename>       |
                          --status <hostname|regexp>    |
                          --statuslist <filename>       |
			  --firmware <hostname|regexp>  |
			  --firmwarelist <filename>     |
			  --livecd <hostname|regexp>    |
			  --livecdlist <filename>

=head1 DESCRIPTION

The aii-installfe utility provides a command line interface or
frontend to remotely manage the AII tools. With aii-installfe we can
select if a client node has to be installed or not, to add/update the
AII specific configuration of nodes and to remove nodes from the AII system.
aii-installfe receives as input from the user a lists of nodes
and their installation status (to be installed/booted from local disk).
Regular expressions can be used to specify hostnames
(for example node00[1-9] or node.*). aii-installfe executes the frontend
aii-shellfe on all the specified installation servers.
Servers can be listed in a configuration file (see below).

aii-installfe is part of the AII system of the quattor tool suite
(see http://www.quattor.org for more information).

=head1 COMMANDS

=over 4

=item --boot <hostname|regexp>

Select the boot from local disk for <hostname>. Perl regular
expressions can be used.

=item --bootlist <filename>

Select boot from local disk for hosts listed on <filename>. Hosts have to
be listed one per line. Lines starting with # are comment.

=item --cdburl <url>

URL for CDB location

Known profiles are determined via the C<profiles-info.xml> file.

In case the C<dir:///path/profile/directory> is used,
all profiles in that directory are considered usable via C<file://>
url.

=item --configure <hostname|regexp>

Configure <hostname>. Perl regular expressions can be used.

=item --configurelist <filename>

Configure hosts listed on <filename>. Hosts have to
be listed one per line. Lines starting with # are comment.

=item --install <hostname|regexp>

Select the installation for <hostname>. Perl regular expressions
can be used.

=item --installlist <filename>

Select installation for the hosts listed on <filename>. Hosts have to
be listed one per line. Lines starting with # are comment.

=item --remove <hostname|regexp>

Remove the configuration for <hostname>. Perl regular expressions
can be used.

=item --removelist <filename>

Remove configurations for hosts listed on <filename>. Hosts have to
be listed one per line. Lines starting with # are comment.

=item --removeall

Remove configurations for *ALL* hosts configured. Useful only in case
of problems/test.

=item --reinstall

Remove, (re)configure and (re)install the configuration for <hostname>
or all hostnames that match the Perl regular expression <regexp>
(i.e. C<--reinstall host> is equal to
C<--remove host --configure host --install host>).

=item --rescue <hostname|regexp>

Select the rescue target for <hostname>. Perl regular expressions can be
used.

=item --rescuelist <filename>

Select rescue for the host listed on <filename>. Hosts have to be listed 
onde per line. Lines starting with # are comment.

=item --status <hostname|regexp>

Report the boot status (boot from local disk/install) for <hostname> or
for all hostnames that match the regular expression <regexp>

=item --statuslist <filename>

Report the boot status (boot from local disk/install) for hosts listed
on <filename>. Hosts have to be listed one per line. Lines starting
with # are comment.

=item --firmware

Select the firmware installation target for <hostname>. Perl regular expressions
can be used.

=item --firmwarelist <filename>

Select firmware installation for the hosts listed on <filename>. Hosts have to 
be listed one per line. Lines starting with # are comment.

=item --livecd

Select the livecd installation target for <hostname>. Perl regular expressions
can be used.

=item --livecdlist <filename>

Select livecd installation for the hosts listed on <filename>. Hosts have to
be listed one per line. Lines starting with # are comment.

=back

=head1 OPTIONS

=over 4

=item --servers <user1@server1 user2@server2 ...>

Installation servers to be updated remotely via ssh. Use'@'
to select the remote user to use. E.g:

 servers = john@install-1.my_domain.org

If there are more servers, use ' ' to separate them:

 servers = john@install-1.my_domain.org john@install-2.my_domain.org

If you are running *all* tools (aii-dhcp, aii-nbp, aii-osinstall)
on *this* machine, use just:

 server = localhost

They will be executed *directly* (no ssh required). Of course you can mix:

 servers = localhost john@install-1.asdf.fi

=item --nodhcp

Run do not update DHCP configuration.

=item --nonbp

Do not update NBP configuration.

=item --noosinstall

Do not update OS installer configurations.

=back

=head2 Other Options

=over

=item --help

Displays a help message with all options and default settings.

=item --version

Displays program version information.

=item --verbose

Print verbose details on operations.

=item --debug <1..5>

Set the debugging level to <1..5>.

=item --cfgfile <path>

Use as configuration file <path> instead of the default
configuration file /etc/aii/aii-nbp.conf.

=item --logfile <file>

Store and append log messages in <file>.

=back

=head1 CONFIGURATION FILE

=over 4

Default values of command lines options can be specified in the file
/etc/aii/aii-installfe.conf using syntax:

 <option> = <value>

=back

=head1 AUTHORS

Enrico Ferro <enrico.ferro@pd.infn.it>

Rafael Garcia Leiva <angel.leiva@uam.es>

=cut

#
# Standard Common Application Framework beginning sequence
#

#
# Beginning sequence for EDG initialization
#
BEGIN {

    # use perl libs in /usr/lib/perl
    push(@INC, '/usr/lib/perl');
    push(@INC,'/opt/edg/lib/perl');

}

#------------------------------------------------------------
# Application
#------------------------------------------------------------

package aii_installfe;

use CAF::Application;
use CAF::Reporter;
use LC::Exception qw (SUCCESS throw_error);
use IPC::Run;

use strict;

our @ISA= qw(CAF::Application CAF::Reporter);

my $queue;
eval {
    require Parallel::Queue;
    $queue = Parallel::Queue->new();
};

#
# Public Methods/Functions for CAF
#
sub app_options {

    push(my @array,

        { NAME    => 'cdburl=s',
          HELP    => 'URL for CDB location',
          DEFAULT => undef },

        { NAME    => 'configure=s',
          HELP    => 'Node(s) to be configured (can be a regexp)',
          DEFAULT => undef },

        { NAME    => 'configurelist=s',
          HELP    => 'File with the nodes to be configured',
          DEFAULT => undef },

        { NAME    => 'remove=s',
          HELP    => 'Node(s) to be removed (can be a regexp)',
          DEFAULT => undef },

        { NAME    => 'removelist=s',
          HELP    => 'File with the nodes to be removed',
          DEFAULT => undef },

        { NAME    => 'reinstall=s',
          HELP    => 'Node(s) to be removed, (re)configured and (re)installed (can be a regexp)',
          DEFAULT => undef },

	{ NAME    => 'rescue=s',
	  HELP    => 'Node(s) to be rescued (can be a regexp)',
	  DEFAULT => undef },

	{ NAME    => 'rescuelist=s',
	  HELP    => 'File with the nodes to be rescued',
	  DEFAULT => undef },

        { NAME    => 'removeall',
          HELP    => 'Remove ALL nodes configured',
          DEFAULT => undef },

        { NAME    => 'boot=s',
          HELP    => 'Node(s) to boot from local disk (can be a regexp)',
          DEFAULT => undef },

        { NAME    => 'bootlist=s',
          HELP    => 'File with the nodes to boot from local disk',
          DEFAULT => undef },

        { NAME    => 'install=s',
          HELP    => 'Nodes(s) to be installed (can be regexp)',
          DEFAULT => undef },

        { NAME    => 'installlist=s',
          HELP    => 'File with the nodes to be installed',
          DEFAULT => undef },

        { NAME    => 'status=s',
          HELP    => 'Report current boot/install status for the node ' .
                     '(can be a regexp)',
          DEFAULT => undef },

        { NAME    => 'statuslist=s',
          HELP    => 'File with the nodes to report boot/install status',
          DEFAULT => undef },

	{ NAME    => 'firmware=s',
	  HELP    => 'Nodes(s) to have their firmware image updated ' .
		     '(can be a regexp)',
	  DEFAULT => undef },

	{ NAME    => 'firmwarelist=s',
	  HELP    => 'File with the nodes requiring firmware upgrade',
	  DEFAULT => undef },

	{ NAME    => 'livecd=s',
	  HELP    => 'Node(s) to use the livecd install target ' .
		     '(can be a regexp)',
	  DEFAULT => undef },

        { NAME    => 'livecdlist=s',
	  HELP    => 'File with the nodes requiring livecd install',
	  DEFAULT => undef },

        { NAME    => 'nodhcp',
          HELP    => 'Do not update DHCP server(s)',
          DEFAULT => undef },

        { NAME    => 'nonbp',
          HELP    => 'Do not update NBP configuration',
          DEFAULT => undef },

        { NAME    => 'noosinstall',
          HELP    => 'Do not update OS installer configuration',
          DEFAULT => undef },

        { NAME    => 'servers=s',
          HELP    => 'Installation servers to be udpated',
          DEFAULT => 'localhost' },

        # other common options

        { NAME    => 'logfile=s',
          HELP    => 'path/filename to use for aii-installfe logs',
          DEFAULT => "/var/log/aii-installfe.log" },

        { NAME    => 'cfgfile=s',
          HELP    => 'configuration file for aii-shellfe defaults',
          DEFAULT => '/etc/aii/aii-installfe.conf' },

        { NAME    => 'noaction',
          HELP    => 'do not actually perform operations',
          DEFAULT => undef },

        { NAME    => 'sshdir=s',
          HELP    => 'the directory for ssh/scp',
          DEFAULT => '/usr/bin' }

        # options inherited from CAF
        #   --help
        #   --version
        #   --verbose
        #   --debug
        #   --quiet

        );

    return(\@array);

}

sub _initialize {

    my $self = shift;

    #
    # define application specific data.
    #

    # external version number
    $self->{'VERSION'} = '2.5.0';

    # show setup text
    $self->{'USAGE'} = "Usage: aii-installfe [options]\n";

    #
    # log file policies
    #

    # append to logfile, do not truncate
    $self->{'LOG_APPEND'} = 1;

    # add time stamp before every entry in log
    $self->{'LOG_TSTAMP'} = 1;

    #
    # start initialization of CAF::Application
    #
    unless ($self->SUPER::_initialize(@_)) {
        return(undef);
    }

    # start using log file (could be done later on instead)
    $self->set_report_logfile($self->{'LOG'});

    return(SUCCESS);

}

#############################################################
# Main Program
#############################################################

package main;

use LC::Exception qw (SUCCESS throw_error);
use File::Basename;
use FindBin qw($Bin);

use strict;
use vars qw($this_app %SIG);

#
# Global Attributes:
#

my (@Options) = ();
my (@Servers) = ();
my ($Content) = {};
my $actions = 0;

#
# run_remotely($user_at_host)
#
# run aii-shellfe in a remote AII server
#
sub run_remotely ($) {

    my $user_at_host = shift;
    my ($user, $host);

    ($user, $host) = ('', '');
    ($user, $host) = split(/\@/, $user_at_host);

    $this_app->debug(3, "aii-installfe: user: $user at host: $host");

    if ($user eq '' || $host eq '') {
        $this_app->error("aii-installfe: bad remote user/server "
                         . "specified ($user_at_host)");
        return(0);
    }

    $host = (gethostbyname($host))[0];

    # remote run aii-shellfe on AII server
    my $ssh = $this_app->option('sshdir') . "/ssh";
    my @cmd = ($ssh, "-q", "$user_at_host", "/usr/bin/sudo",
               "$Bin/aii-shellfe", @Options);
    return(run_cmd('remotely', @cmd));
}

#
# run_locally()
#
# run aii-shellfe on the local host
#
sub run_locally {
    my @cmd = ("$Bin/aii-shellfe", @Options);
    $this_app->debug(2, "aii-installfe: run aii-shellfe locally: ");
    return(run_cmd('locally', @cmd));
}

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

    my $content = "";
    if (keys %$Content) {
        push(@cmd, '--file', '-');
        while (my ($opt, $block) = each %$Content) {
            $content .= $block;
        }
    }

    if ($this_app->option('noaction')) {
        $this_app->info("aii-installfe: would run $style: " . join(" ", @cmd));
    } else {
        $this_app->debug(2, "aii-installfe: run $style: " . join(" ", @cmd));
        my $err = "";
        if (!eval { IPC::Run::run(\@cmd, \$content, '2>', \$err) } || $@) {
            $this_app->error("installfe: failed execution of " .  join(" ", @cmd) . "\n$err\n$@");
            return(0);
        }
    }
    return 1;
}

#
# parse_options()
#
# retrieve options for shellfe
#
sub parse_options {

    my $item;

    #
    # retrieve options for shellfe
    #

    push(@Options, '--cdburl', $this_app->option('cdburl'))      if $this_app->option('cdburl');

    push(@Options, '--nodhcp')      if $this_app->option('nodhcp');
    push(@Options, '--nonbp')       if $this_app->option('nonbp');
    push(@Options, '--noosinstall') if $this_app->option('noosinstall');

    push(@Options, '--verbose')     if $this_app->option('verbose');
    push(@Options, '--quiet')       if $this_app->option('quiet');

    if ($this_app->option('debug')) {
        push(@Options, '--debug', $this_app->option('debug'));
    }

    if ($this_app->option('removeall'))   {
        push(@Options, '--removeall');
        $this_app->warn('aii-installfe: remove all option enabled');
        $actions++;
    } else {

        # processing order not important
        for $item (qw(install boot configure remove status firmware livecd rescue reinstall)) {

            if ($this_app->option($item)) {
                push(@Options, "--$item", $this_app->option($item));
                $actions++;
            }

            next if ($item eq 'reinstall');

            my $listoption = "${item}list";
            if ($this_app->option($listoption)) {
                my $filename = $this_app->option($listoption);
                $filename =~ s{[^\w._:/\\~-]}{_}g; # we don't want to pass shell meta to open
                my $content = "";
                if (!open(LIST, $filename)) {
                    $this_app->error("aii-installfe: $listoption '$filename' not readable: $!");
                    return(1);
                }
                foreach my $line (<LIST>) {
                    chomp($line);
                    $line =~ s{#.*}{};
                    next if (!$line || $line =~ m{^\s*$});
                    $content .= "$line,${item}\n";
                }
                $Content->{$listoption} = $content;
                close(LIST);
                $actions++;
            }

        }

    }

    return(0);

}

#------------------------------------------------------------
# main loop
#------------------------------------------------------------

# fix umask
umask (022);

# unbuffer STDOUT & STDERR
autoflush STDOUT 1;
autoflush STDERR 1;

my $server;

#
# initialize the main class.
#
unless ($this_app = aii_installfe->new($0, @ARGV)) {
    throw_error('aii-installfe: cannot start application');
}

# process command line options
$this_app->debug(1, "aii-installfe: reading cmd line");
if (parse_options()) {
    $this_app->error("aii-installfe: failed to process cmd line ");
    exit(1);
}
if (!$actions) {
    $this_app->warn('aii-installfe: no actions specified');
    exit(0);
}


#
# run tools locally (without ssh) or remotely (via ssh)
#

@Servers = split ('\s+', $this_app->option('servers'));

$this_app->verbose("aii-installfe: servers = " . @Servers);

my ($local_server) = grep { /\blocalhost\b/ } @Servers;
my @remote_servers = grep { !/\blocalhost\b/ } @Servers;

my $errcount = 0;

if ($local_server) {
    $this_app->debug(1, "aii-installfe: server: $local_server");
    if (run_locally()) {
	$this_app->error("aii-installfe: failed to run "
			. "aii-shellfe locally on $local_server");
	$errcount++;
    }
}

if (@remote_servers) {
    if ($queue) {
        $queue->setTrapStdio(1);
        $queue->setList(@remote_servers);
        $queue->setWidth(10);
        $queue->setTimeLimit("1h");
        $queue->setAction(\&run_remotely);
        my $result = $queue->run();

        for my $index ( 0 .. $#remote_servers ) {
            my $host = $remote_servers[$index];
            my $status = $result->getStatus($index);
            foreach my $line ( split(/\n+/,$result->getOutput($index))) {
                my $h = $host;
                $h =~ s{.*\@}{}; # remove username: we just care about hostname
                print "[$h] $line\n";
            }
            if ($status eq "fail") {
                $this_app->error("aii-installfe: failed to run "
                                 . "aii-shellfe remotely on $host");
                $errcount++;
            } elsif ($status eq "nofork") {
                $this_app->error("aii-installfe: failed to fork "
                                 . " to run aii-shellfe remotely");
                $errcount++;
            } elsif ($status eq "notime") {
                $this_app->error("aii-installfe: ran out of time "
                                 . "before we could process $host");
                $errcount++;
            }
        }
    } else {
        foreach my $host (@remote_servers) {
            unless (run_remotely($host)) {
                $this_app->error("aii-installfe: failed to run on $host");
                $errcount++;
            }
        }
    }
}

if ($errcount == scalar(@Servers)) {
    $this_app->error("aii-installfe: failed to run "
		     . "aii-shellfe on any defined servers");
    exit(1);
}

exit (0);
