#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

use strict;

use Data::Dumper;
use DBI;
use Getopt::Long;

my $VERSION = "0.1.0";

my $opt_help    = 0;
my $opt_version = 0;

my $opt_create  = 0;
my $opt_destroy = 0;

my $opt_origin_vg     = undef;
my $opt_origin_lv     = undef;
my $opt_snapshot_name = undef;
my $opt_snapshot_size = undef;
my $opt_mountpoint    = undef;

my $opt_host     = "localhost";
my $opt_user     = "";
my $opt_password = "";
my $opt_port     = 3306;
my $opt_socket   = "";

my $opt_binlog_save = undef;

my $opt_lvcreate = "lvcreate";
my $opt_lvremove = "lvremove";

my %options = (
  'help|?'            => \$opt_help,
  'version|v'         => \$opt_version,

  'create|c'          => \$opt_create,
  'destroy|d'         => \$opt_destroy,

  'origin-vg=s'       => \$opt_origin_vg,
  'origin-lv=s'       => \$opt_origin_lv,
  'snapshot-name=s'   => \$opt_snapshot_name,
  'snapshot-size=s'   => \$opt_snapshot_size,
  'mountpoint=s'      => \$opt_mountpoint,

  'binlog-save=s'     => \$opt_binlog_save, 
 
  'host|h=s'          => \$opt_host,
  'user|u=s'          => \$opt_user,
  'password|p=s'      => \$opt_password,
  'port|P=s'          => \$opt_port,
  'socket|S=s'        => \$opt_socket,
);

sub read_options {
  Getopt::Long::Configure("bundling");
  GetOptions(%options);
  
  &usage() and exit if($opt_help);
  &version() and exit if($opt_version);
};

sub usage {
  my $errmsg = shift;

  if($errmsg) {
    print "ERROR: $errmsg\n\n";
  }

  &version;
  print <<END_OF_USAGE;
Usage: mysql-snapshot.pl [options]

Options:
  --help, -h, -?      Show this help and exit
  --version, -V       Display version and exit

  --create, -c        Create a snapshot

    --binlog-save     File to save binlog information to
    --origin-vg       LVM Volume Group of volume to snapshot
    --origin-lv       LVM Logical Volume of volume to snapshot
    --snapshot-name   Name of snapshot to create
    --snapshot-size   Size of snapshot to create
    --mountpoint      Path to mount snapshot after creation

  --destroy, -d       Destroy a snapshot

    --origin-vg       LVM Volume Group of snapshot
    --snapshot-name   Name of snapshot to destroy

  --host, -h          MySQL host
  --user, -u          MySQL user
  --password, -p      MySQL password
  --port, -P          MySQL port
  --socket, -S        MySQL socket

END_OF_USAGE
}

sub version {
  print <<END_OF_VERSION;
mysql-snapshot ($0) version $VERSION

END_OF_VERSION
};

sub connect {
  my $dbh = DBI->connect("DBI:mysql:"
                          ."host=$opt_host;"
                          ."mysql_port=$opt_port;"
                          ."mysql_socket=$opt_socket",
                          $opt_user, $opt_password);
  unless($dbh) {
    return;
  }

  return $dbh;
}

sub save_binlog
{
  my $dbh = shift;

  my %binlog = (
    "master_log_file" => "",
    "master_log_pos"  => 0,
    "slave_read_log_file"  => "",
    "slave_read_log_pos"   => 0,
    "slave_exec_log_file"  => "",
    "slave_exec_log_pos"   => 0,
  );

  my $sth_m = $dbh->prepare("SHOW MASTER STATUS");
  my $sth_s = $dbh->prepare("SHOW SLAVE STATUS");

  $sth_m->execute;
  if(my $row = $sth_m->fetchrow_hashref)
  {
    $binlog{'master_log_file'} = $row->{'File'};
    $binlog{'master_log_pos'}  = $row->{'Position'};
  }

  $sth_s->execute;
  if(my $row = $sth_s->fetchrow_hashref)
  {
    $binlog{'slave_read_log_file'} = $row->{'Master_Log_File'};
    $binlog{'slave_read_log_pos'}  = $row->{'Read_Master_Log_Pos'};
    $binlog{'slave_exec_log_file'} = $row->{'Relay_Master_Log_File'};
    $binlog{'slave_exec_log_pos'}  = $row->{'Exec_Master_Log_Pos'};
  }

  if($opt_binlog_save)
  {
    open SAVE, ">$opt_binlog_save"
      or die "Couldn't open file to save binary log information: $?";
  } else {
    *SAVE = *STDOUT;
  }

  printf SAVE "master=%s,%i;slave_read=%s,%i;slave_exec=%s,%i\n",
    $binlog{'master_log_file'},
    $binlog{'master_log_pos'},
    $binlog{'slave_read_log_file'},
    $binlog{'slave_read_log_pos'},
    $binlog{'slave_exec_log_file'},
    $binlog{'slave_exec_log_pos'};

}

sub lvm_snapshot
{
  my $origin_vg = shift;
  my $origin_lv = shift;
  my $snapshot_name = shift;
  my $snapshot_size = shift;

  my $command = sprintf("%s -s -L %s -n %s /dev/%s/%s >/dev/null 2>&1",
    $opt_lvcreate, $snapshot_size, $snapshot_name, $origin_vg, $origin_lv
  );

  return 0 if (system($command));
  return 1;
}

sub lvm_remove
{
  my $origin_vg = shift;
  my $snapshot_name = shift;

  my $command = sprintf("%s -f /dev/%s/%s >/dev/null 2>&1",
    $opt_lvremove, $origin_vg, $snapshot_name
  );

  return 0 if (system($command));
  return 1;
}

sub mount
{
  my $origin_vg = shift;
  my $snapshot_name = shift;
  my $mountpoint = shift;

  my $command = sprintf("mkdir -p %s", $mountpoint);
  return 0 if (system($command));

  my $command = sprintf("mount /dev/$origin_vg/$snapshot_name $mountpoint",
    $origin_vg, $snapshot_name, $mountpoint
  );
  return 0 if (system($command));

  return 1;
}

sub unmount
{
  my $origin_vg = shift;
  my $snapshot_name = shift;

  my $command = sprintf("umount /dev/%s/%s",
    $origin_vg, $snapshot_name
  );
  return 0 if (system($command));

  return 1;
}

sub do_create
{
  my $dbh = &connect
    or die "Couldn't connect to MySQL ".$DBI::err;

  $dbh->do("FLUSH TABLES WITH READ LOCK")
    or die "Couldn't FLUSH TABLES WITH READ LOCK: ".$DBI::err;

  &save_binlog($dbh);

  &lvm_snapshot($opt_origin_vg, $opt_origin_lv, $opt_snapshot_name, $opt_snapshot_size)
    or die "Couldn't create snapshot: $?";

  $dbh->do("UNLOCK TABLES");
  $dbh->disconnect;

  &mount($opt_origin_vg, $opt_snapshot_name, $opt_mountpoint)
    or die "Couldn't mount snapshot, try mounting it manually with mount";

  print "ok\n";
}


# for destroy:

sub do_destroy
{
  &unmount($opt_origin_vg, $opt_snapshot_name)
    or die "Couldn't unmount snapshot, maybe it's busy?";
  &lvm_remove($opt_origin_vg, $opt_snapshot_name)
    or die "Couldn't remove snapshot, try removing it manually with lvremove";

  print "ok\n";
}


&read_options;

if(($opt_create + $opt_destroy) != 1)
{
  &usage("You must either create or destroy");
}

&do_create if($opt_create);
&do_destroy if($opt_destroy);
