#!/usr/bin/perl
# #
# 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>
#

# 
# #
# ccm, 26.2.0, 1, Tue Apr 07 2026
#

package ccm_fetch;

use strict;
use warnings;

# required for CAF (Common Application Framework)
BEGIN {
    unshift(@INC, '/usr/lib/perl');
    pop @INC if $INC[-1] eq '.';
}

use CAF::Application;
use CAF::Reporter 16.8.1;
use CAF::Object qw(SUCCESS throw_error);
use EDG::WP4::CCM::Fetch::Config qw(NOQUATTOR_FORCE);
our @ISA = qw(CAF::Application CAF::Reporter CAF::Path);

# needed for method overriding
no warnings 'redefine';

#
# application specific options:
#
sub app_options {

    # local lexicals
    my @options;

    # build options
    push (@options,

          { NAME    => 'cfgfile=s',
            DEFAULT => '/etc/ccm.conf',
            HELP    => 'configuration file for CCM' },

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

          { NAME    => 'profile|p=s',
            HELP    => 'URL of profile to fetch' },

          { NAME    => 'profile_failover=s',
            HELP    => 'URL of profile to fetch when --profile is not available' },

          { NAME    => 'profile-time=s',
            HELP    => 'Unix timestamp as modification time of profile' },

          { NAME    => 'foreign',
            HELP    => 'whether a foreign profile or local profile' },

          { NAME    => 'cache_root=s',
            DEFAULT => '/var/lib/ccm',
            HELP    => 'Basepath for the configuration cache' },

          { NAME    => 'get_timeout=i',
            DEFAULT => '30',
            HELP    => 'Timeout in seconds for HTTP GET operation' },

          { NAME    => 'group_readable=s',
            HELP    => 'Group readable profile (value is the groupname)' },

          { NAME    => 'world_readable=i',
            DEFAULT => '0',
            HELP    => 'World readable profile flag 1/0' },

          { NAME    => 'force|f',
            HELP    => 'Fetch regardless of modification times' },

          { NAME    => 'dbformat=s',
            HELP    => 'Format to use for storing profile' },

          { NAME    => NOQUATTOR_FORCE,
            DEFAULT => 0,
            HELP    => 'Fetch even if CCM updates are globally disabled' },

          { NAME    => 'retrieve_retries=i',
            DEFAULT => 3,
            HELP    => 'Number of times fetch will attempt to retrieve a profile' },

          { NAME    => 'lock_retries=i',
            DEFAULT => 3,
            HELP    => 'Number of times fetch will attempt to get the fetch lock'},

          { NAME    => 'retrieve_wait=i',
            DEFAULT => 30,
            HELP    => 'Number of seconds that fetch will wait between retrieve attempts' },

          { NAME    => 'lock_wait=i',
            DEFAULT => 30,
            HELP    =>  'Number of seconds that fetch will wait between lock attempts' },

          { NAME    => 'key_file=s',
            HELP    => 'Absolute file name for key file to use with HTTPS.'},

          { NAME    => 'cert_file=s',
            HELP    => 'Absolute file name for certificate file to use with HTTPS.' },

          { NAME    => 'ca_file=s',
            HELP    => 'File containing a bundle of trusted CA certificates for use with HTTPS.' },

          { NAME    => 'ca_dir=s',
            HELP    => 'Directory containing trusted CA certificates for use with HTTPS' },

          { NAME    => 'trust=s',
            HELP    => 'Comma-separated list of kerberos principals to trust when using encrypted profiles' },

          { NAME    => 'keep_old=i',
            HELP    => 'Number of old profiles to keep before purging' },

          { NAME    => 'purge_time=i',
            HELP    => 'Number of seconds before purging inactive profiles' },

          { NAME    => 'json_typed',
            HELP    => 'Extract typed data from JSON profiles' },

          { NAME    => 'tabcompletion',
            HELP    => 'Create the tabcompletion file (during profile fetch)' },

          { NAME    => 'principal=s',
            HELP    => 'Principal to use for Kerberos setup' },

          { NAME    => 'keytab=s',
            HELP    => 'Keytab to use for Kerberos setup' },

        );

    return \@options;
}

#
# initialise
#
sub _initialize {

    # local lexicals
    my $self = shift;

    # version and usage
    $self->{'VERSION'} = "26.2.0";
    $self->{'USAGE'}   = sprintf("Usage: %s [OPTIONS...]", $0);

    $self->{LOG_APPEND} = 1;
    $self->{LOG_TSTAMP} = 1;

    # initialise
    unless ($self->SUPER::_initialize(@_)) {
        return;
    }

    $self->set_report_logfile($self->{'LOG'});
    # Enable verbose_logfile
    $self->setup_reporter(undef, undef, undef, undef, 1);

    return SUCCESS;
}


#-- Main ---------------------------------------------------------------------------------------------#

package main;

use strict;
use warnings;

# modules
use English;
use CAF::Object qw(SUCCESS CHANGED throw_error);
use File::Basename;
use LC::Exception;
use EDG::WP4::CCM::CCfg qw(initCfg getCfgValue);
use EDG::WP4::CCM::CacheManager;
use EDG::WP4::CCM::Fetch;
use EDG::WP4::CCM::Fetch::Config qw(NOQUATTOR NOQUATTOR_EXITCODE NOQUATTOR_FORCE);
use EDG::WP4::CCM::Fetch::ProfileCache qw($ERROR GetPermissions);

$ENV{PATH} = join(":", qw(/bin /usr/bin /sbin /usr/sbin));

our ($this_app);

# local lexicals
my $fetch   = undef;
my $profile = undef;
my $foreign = undef;
my $config  = undef;


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

# initialise main class
unless ($this_app = ccm_fetch->new($0, @ARGV)) {
    throw_error("Cannot start application");
    exit (1);
}

# get Config path
my $opts = {};
if (defined $this_app->option("cfgfile")) {
    $opts->{CONFIG} = $this_app->option("cfgfile");
}
initCfg($this_app->option("cfgfile"));

my ($dopts, $fopts, $mask) = GetPermissions($this_app, getCfgValue('group_readable'), getCfgValue('world_readable'));
# Set logfile permissions
$this_app->status($this_app->option('logfile'), %$fopts);

if (defined $this_app->option("profile")) {
    $opts->{PROFILE} = $this_app->option("profile");
}
if (defined $this_app->option("foreign")) {
    $opts->{FOREIGN} = $this_app->option("foreign");
}
if (defined $this_app->option("dbformat")) {
    $opts->{DBFORMAT} = $this_app->option("dbformat");
}

# Disable all updates if the flag is present
if (-f NOQUATTOR && ! $this_app->option(NOQUATTOR_FORCE)) {
    $this_app->warn("CCM updates disabled globally (", NOQUATTOR, " present)");
    my $fh = CAF::FileEditor->new(NOQUATTOR);
    $this_app->warn("$fh") if $fh;
    $fh->close();
    exit(NOQUATTOR_EXITCODE);
}

# initialize Fetch object
$fetch = EDG::WP4::CCM::Fetch->new($opts);
if (!$fetch) {
    throw_error("Initialization failed");
    exit (1);
}

# set timeout
$fetch->setTimeout($this_app->option("get_timeout"))
		if defined $this_app->option("get_timeout");

# set force
$fetch->setForce($this_app->option("force"))
		if defined $this_app->option("force");

# set failover profile url
$fetch->setProfileFailover($this_app->option("profile_failover"))
		if defined $this_app->option("profile_failover");

# set cache root
$fetch->setCacheRoot($this_app->option("cache_root"))
		if defined $this_app->option("cache_root");

# set group readable flag
$fetch->setGroupReadable($this_app->option("group_readable"))
    if defined $this_app->option("group_readable");

# set world readable flag
$fetch->setWorldReadable($this_app->option("world_readable"))
    if defined $this_app->option("world_readable");

# we no longer set profile notification time
# since we always want to get something later than what we
# have on disk

# set principal
$fetch->setPrincipal($this_app->option("principal"))
    if defined $this_app->option("principal");

# set keytab
$fetch->setKeytab($this_app->option("keytab"))
    if defined $this_app->option("keytab");

# fetch profile
my $errno = $fetch->fetchProfile();

if (!defined($errno)) {
    $this_app->error("Network error detected");
    exit(2);
} elsif ($errno == $ERROR) {
    $this_app->error("Unable to fetch profile");
    exit(1);
} else {
    # Init cachemanager
    my $cmgr = EDG::WP4::CCM::CacheManager->new($fetch->{CACHE_ROOT}, $fetch->{_CCFG});
    # get Configuration instance (anonymous, latest CID)
    my $cfg = $cmgr->getConfiguration();
    # get name and CID
    my $msg = "latest CID ".$cfg->getConfigurationId();

    my $name = $cfg->getName();
    $msg .= " name $name" if defined($name);

    if ($errno == CHANGED) {
        $this_app->info("Profile updated, new $msg");
    } else {
        $this_app->info("Fetched profile unchanged, $msg");
    }
}

exit (0);


=head1 NAME

ccm-fetch - fetch profiles and store in local cache

=head1 DESCRIPTION

This program retrieves profiles from specified URLs,
subject to modification time constraints, validates them, and stores the
contents in the local configuration cache.

=head1 SYNOPSIS

ccm-fetch-new [I<OPTIONS>]

=head1 OPTIONS

=over 4

=item B<-d>, B<--debug>

Turn on debugging messages.

=item B<-f>, B<--force>

Force fetch to retrieve profiles, regardless of their
modification times.

=item B<--force-quattor>

Execute even if CCM updates are globally disabled. This option exists
to allow administrators to make one-off updates in a controlled manner.

=item B<--cfgfile>=I<file>

Use configuration file I<file>.  Default location:
/etc/ccm.conf.  See below for the parameters that may be
specified in this file.

=item B<--profile>=I<URL>

URL of profile to fetch. You can use either HTTP or HTTPS protocols.

=item B<--profile_failover>=I<URL>

URL of profile to fetch in case the URL in --profile is not available
You can use either HTTP or HTTPS protocols. This option is useful in
case of broken SSL/HTTPS setups (e.g. expired host certificates); a
reconfiguration of the CCM URL is not possible if a new profile cannot
be accessed, as the CCM URL is stored in the profile itself! A
failover URL can be pointing to an auxiliarly URL (e.g. plain HTTP
based) which can be enabled for emergency situations.

=item B<--profile-time>=I<time>

UNIX timestamp to expect as the modification time of the new profile.
This will normally be provided by the notification service. This
argument is supported for compatability reasons, but has no effect.

=item B<--foreign>=I<1/0>

The profile being fetched is a foreign profile and will be stored at
different location (cache_root/foreign)

=back

=head1 CONFIGURATION FILE

The configuration file may specify any of the following parameters.
Where these have the same name as a command line option, the description
is the same.

=over 4

=item debug

=item force

=item profile

=item cache_root

Location of the root of the cache directory hierarchy.  Defaults to
/var/lib/ccm.

=item get_timeout

Timeout in seconds for the HTTP GET operation, when retrieving profiles.

=item lock_retries

Number of times fetch will attempt to get the single-threaded fetch lock
before giving up.

=item lock_wait

Number of seconds that fetch will wait between lock attempts.

=item retrieve_retries

Number of times fetch will attempt to retrieve a profile, when
it has been given a valid accompanying notification time.

=item retrieve_wait

Number of seconds that fetch will wait between retrieve attempts.

=item cert_file

Absolute file name for certificate file to use with HTTPS.

=item key_file

Absolute file name for key file to use with HTTPS.

=item ca_file

File containing a bundle of trusted CA certificates for use with HTTPS.

=item ca_dir

Directory containing trusted CA certificates for use with HTTPS. Hash
symlinks are needed.

=item group_readable

Group readable cache root/profiles (value is the groupname).
If set with valid groupname, the configured cache root
will have its permissions set to 750 and the
the groupname as group (still owned by root).

Following standard UNIX ACL semantics, the C<group_readable> configuration
option is not very useful if C<world_readable> is also true.

=item world_readable

World readable cache root/profiles flag (1/0). If true, the configured cache root
will have its permissions set to 755. If false the permissions will be 700.

These permissions will be set each time C<ccm-fetch> is run.

Following standard UNIX ACL semantics, the C<group_readable> configuration
option is not very useful if C<world_readable> is also true.

The use of C<world_readable> is not recommended
if profiles contain sensitive or private information.

=back

=head1 EXIT STATUSES

This program exits with:

=over

=item 0 on success

=item 3 if updates have been disabled

=item 2 on a network error

=item 1 on a different error

=back

=cut
