#=============================================================================
# Doloforge::Service::Mysql : add a MySQL database for back-end use
#
# Generate config files from templates, add a config file to the whole, etc.
#
# Full help :
#   perldoc Doloforge::Service::Mysql
#=============================================================================


package Doloforge::Service::Mysql;

use strict;

# This base class implements the constructor and help methods in a generic way.
use base qw(Doloforge::Service);

use DBI;
use DBD::mysql;



#=============================================================================
=encoding utf8

=head1 NAME

Doloforge::Service::Mysql : Add a MySQL database for back-end use

=head1 SYNOPSIS

    # Update the full configuration, running each client/project/service update
    doloforge config --update

=head1 DESCRIPTION

Todo

=cut


#=============================================================================
# Method update
#
# Create DB if it is configured but does not exist.
# This method receives :
#   option: context options passesd on the command-line (quite useless there)
#   client_conf: client configuration file contents (client.$name.yaml)
#   project_conf: project configuration file contents (project.$name.client.$name.yaml)
# This class does not (yet) create the DB for you...
# FIXME : create DB at project update

sub update {
    my $self = shift;

    my ($option, $client_conf, $project_conf) = @_;

    # Copy the service skeleton directory into the project directory.
    if ($self->has_role('nfs_role')) {                                                      # only create shared storage on the NFS server
        $self->skel_copy(
            "$self->{global}->{dir}->{skel}/service/shared/$self->{name}",                  # Skeleton directory to copy (skel/service/shared/[service]/)
            "$self->{global}->{dir}->{shared}/$client_conf->{name}/$project_conf->{name}",  # Where to copy it
            $self->{config}->{developerdirs},                                               # reference to the developers directories, that will have to be chown'ed to the following ownership
            $self->{global}->{permissions}->{user}->[0],                                    # group id of the client who own this project. Developers of the same group will have write-access to files there.
            ($client_conf->{gid} or $client_conf->{name}),
            # This is a special permissions setup here, to allow cron_run() below to restore DBs on the mirror stage, after publishing them from prod to mirror using reverse_rsync
            # We need both the client UID to write the backup to the dump/auto dir on the mirror,
            # and the doloforge user on the mirror to restore the DB, while outputting its log in the same directory.
        );
    }
}


#=============================================================================
# Method cron_run
#
# Run the regular cron tasks on a per-project basis, on the application servers,
# which is mainly DB backup for every project that uses this RDBMS.

sub cron_run {
    my $self = shift;

    my ($client, $project, $date, $interval) = @_;

    # Do not dump the DB is the config does not want it
    #   - the default dump interval is 'daily'
    #   - if a project specifies another interval, take it into account
    #   - an interval of 'none' will never be granted, thus no backup will be attempted
    #   - continue only if the wanted interval is the one we're currently running
    my $dump_interval = (exists($project->{services}->{$self->{name}}->{dump_interval}) ? $project->{services}->{$self->{name}}->{dump_interval} : 'daily');
    return unless ($dump_interval eq $interval);

    # Backup the project database, with basically the following command :
    #   mysqldump --user=MYSQL_USER --password=MYSQL_PWD --opt MYSQL_DATABASE | gzip > DUMP
    # See man pages for how to restore...

    # The following configuration entry will trigger a restore (from the defined stage) instead of a dump :
    #  stages:
    #    mirror:
    #      db_restore_from: prod
    my $restore = (exists($project->{stages}->{$self->{global}->{stage}}->{db_restore_from}) ? $project->{stages}->{$self->{global}->{stage}}->{db_restore_from} : '' );

    # Some projects have manually hacked additionnal databases: don't forget to also dump them
    unless (exists($project->{services}->{$self->{name}}->{dbname})) {
        $self->warn("No database name found in config file for $client->{name}/$project->{name}");
        return;
    }
    my @dbname = ($project->{services}->{$self->{name}}->{dbname});
    push(@dbname, @{$project->{services}->{$self->{name}}->{otherdbs}}) if (exists($project->{services}->{$self->{name}}->{otherdbs}));

    foreach my $dbname (@dbname) {
        # Do not run the dump more than once for a single DB used by many projects (spit a message and get out)
        # Use the dump_interval parameter above to prevent dump for some of the projects that use the same DB
        if (exists($self->{_cron_run_dbname}->{($restore ? 'restore' : 'dump')}->{$dbname})) {
            $self->warn("Already ".($restore ? 'restored' : 'dumped')." $self->{name} database $dbname within ".$self->{_cron_run_dbname}->{($restore ? 'restore' : 'dump')}->{$dbname}." while trying $client->{name}/$project->{name} (use \"dump_interval: none\" to avoid)");
        } else {
            $self->run_sub(
                "cron run for $self->{name} ".($restore ? 'restoring' : 'dumping')." $dbname within $client->{name}/$project->{name}",
                $self->{global}->{permissions}->{user}->[1], $self->{global}->{permissions}->{group}->[1], # Use the doloforge administrator ID to execute the dump
                sub {
                    my $elapsed = time;
                    my $projectdir = "$self->{global}->{dir}->{shared}/$client->{name}/$project->{name}";
                    my $dumpdir = "$self->{name}/_dump/auto"; # This directory belongs to the doloforge administrator, who connects to the RDBMS
                    my $dumpfile  = "mysqldump.$dbname.".($restore || $self->{global}->{stage}).".$date.sql.gz"; # Dumps to the stage file, or restore from the defined stage file
                    my $dumpfiles = "mysqldump.$dbname.".($restore || $self->{global}->{stage}).".*.sql.gz";     # Glob matching all the dump files, in order to restore the lastest one
                    my $dumpglob  = "mysqldump.$dbname.$self->{global}->{stage}.*.sql.gz";                       # Used to search for older dump files for removal on the dump stage (not the restore stage)
                    my $keepdumps = ($self->{config}->{keepdumps} || 5) + 1; # Convert "number of files to keep" to "order from which to remove files" (passed to tail)
                    my $restorelog = "_mysqlrestore.$dbname.$self->{global}->{stage}.log"; # Restore log, which must NOT be removed by subsequent mirroring rsync, thus the leading "_"

                    # Prepare a dump command, in the project context
                    #   - load the project environment
                    #   - make sure the following pipe will report the result code of the first failing command
                    #   - mysqldump, for easy restore with many options
                    #   - gzip the output
                    #   - store in the dump/auto/ directory
                    #   - never erase the previous dump until the new one is completed
                    my $dump_command = join(' ', split(/\s+/, # join(split("")) will simply remove formatting spaces and newlines in the output traces file and actual shell command...
                        "cd $projectdir;
                        source ./_conf/project_env.sh;
                        set -o pipefail -o errexit;
                        mysqldump --host=\$MYSQL_HOST --port=\$MYSQL_TCP_PORT --user=\$MYSQL_USER --password=\$MYSQL_PWD --opt $dbname
                        | gzip --to-stdout --best --rsyncable
                        > $dumpdir/.tmp.mysqldump.sql.gz;
                        mv -f $dumpdir/.tmp.mysqldump.sql.gz $dumpdir/$dumpfile;
                        ls -t $dumpdir/$dumpglob
                        | tail -n +$keepdumps
                        | xargs --no-run-if-empty rm"
                    ));
                    # Restore the dump with something along the lines of :
                    my $restore_command = join(' ', split(/\s+/,
                        "cd $projectdir;
                        source ./_conf/project_env.sh;
                        set -o pipefail -o errexit;
                        gunzip --to-stdout \$(ls -t $dumpdir/$dumpfiles | head -1)
                        | mysql --host=\$MYSQL_HOST --port=\$MYSQL_TCP_PORT --user=\$MYSQL_USER --password=\$MYSQL_PWD $dbname
                        > $dumpdir/$restorelog 2>&1"
                    ));

                    # Keep a trace of what command was run, an how to restore
                    $self->write_file(undef, "$projectdir/$dumpdir/readme_$dbname.txt",
                        "The dump was created with the following command:",
                        "\t($dump_command)",
                        "",
                        "Restore the database with the following command:",
                        "\t($restore_command)",
                    ) unless ($restore);

                    # Actually run the command...
                    my $command = ($restore ? $restore_command : $dump_command);
                    $self->info("Running command : $command");
                    my $rc = system('/bin/bash', '-c', $command);

                    # Test for return code et al, and report to the doloforge administrator
                    if ($rc == -1) {
                        $self->warn("Failed to execute $command: $!");
                    } elsif ($rc & 127) {
                        $self->warn("Child $command died with signal ".($rc & 127).", ".(($rc & 128) ? 'with' : 'without')." coredump");
                    } elsif ($rc >= 256) {
                        $self->warn("Child $command exited with value ".($rc >> 8));
                    } else {
                        $self->info("Finished $command rc=$rc elapsed=".(time - $elapsed));
                    }
                }
            );

            # Mark the DB as processed
            $self->{_cron_run_dbname}->{($restore ? 'restore' : 'dump')}->{$dbname} = "$client->{name}/$project->{name}";
        }
    }
}



1;
__END__

=head1 AUTHOR

Nicolas Huillard <F<nicolas@huillard.net>>, Dolomède

=head1 SEE ALSO

doloforge(1)

=cut
