You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ngcpcfg/sbin/ngcp-sync-constants

424 lines
10 KiB

#!/usr/bin/perl
#----------------------------------------------------------------------
# Synchronizes passwords from constants.yml with MySQL
#----------------------------------------------------------------------
use strict;
use warnings;
use English qw( -no_match_vars );
use DBI qw(:sql_types);
use Config::Tiny;
use YAML::XS;
use Getopt::Long;
use IPC::System::Simple qw(system capturex);
use Readonly;
#----------------------------------------------------------------------
Readonly my $CONSTANTS_YML => "/etc/ngcp-config/constants.yml";
Readonly my $DEFAULT_MYSQL_USER => "sipwise";
Readonly my $MYSQL_CREDENTIALS => "/etc/mysql/sipwise.cnf";
Readonly my $DB_CFG => "/etc/default/ngcp-db";
Readonly my $PW_UNDEF => "PW_UNDEF";
Readonly my $DEFAULT_DBHOST => "127.0.0.1";
Readonly my $DEFAULT_DBPORT => "3306";
my $yml = {};
my $dbh;
my $password_length = 20;
my $init_passwords = 0;
my $debug = 0;
my $test_mode = 0;
my $error = 0;
my $mysql_root = 0;
sub Usage {
print <<USAGE;
==
Synchronizes passwords from constants.yml with MySQL
==
$PROGRAM_NAME [options]
Options:
-help|-h|-? -- this help
-root|-r -- use mysql root user without password as DB credentials
-init-passwords|-i -- generate new passwords (constants.yml is updated)
-test|-t -- test mode (no updates)
-verbose|-v -- verbose mode
USAGE
exit 0;
}
GetOptions("h|?|help" => \&Usage,
"i|init-passwords" => \$init_passwords,
"r|root" => \$mysql_root,
"t|test" => \$test_mode,
"v|verbose" => \$debug);
#----------------------------------------------------------------------
sub logger {
my $str = shift || '';
my $mode = shift || 0;
return if $debug == -1;
return if $mode == 1 && $debug <= 0;
my $prefix = $mode < 2 ? "-->" : "Warning:";
printf { $mode == 2 ? *STDERR : *STDOUT } "%s %s\n", $prefix, $str;
return;
}
sub log_info { logger(shift, 0); }
sub log_debug { logger(shift, 1); }
sub log_warn { logger(shift, 2); }
sub pwgen {
my @list = ("a".."z",0..9,"A".."Z");
my @randoms;
for (1..$password_length) {
push @randoms, $list[int(rand($#list))];
}
return join "", @randoms;
}
sub get_sipwise_db_password {
my $mysql_creds = Config::Tiny->read($MYSQL_CREDENTIALS)
or die "Cannot open $MYSQL_CREDENTIALS: $ERRNO";
if (my $mysql_pass = $mysql_creds->{_}{SIPWISE_DB_PASSWORD}) {
$mysql_pass =~ s/^['"]|['"]$//g;
return $mysql_pass;
} else {
die "Cannot parse mysql credentials file $MYSQL_CREDENTIALS";
}
return;
}
sub get_mysql_credentials {
return ('root', '') if $mysql_root;
my $mysql_user = $DEFAULT_MYSQL_USER;
my $mysql_pass = get_sipwise_db_password();
return ($mysql_user, $mysql_pass);
}
sub get_nodename {
my @lines = capturex( [ 0 ], "/usr/sbin/ngcp-nodename");
my $l = shift @lines;
chomp $l;
log_debug("nodename => $l");
return $l;
}
sub connect_db {
my ($dbhost, $dbport) = @_;
my ($mysql_user, $mysql_pass) = get_mysql_credentials();
$dbh = DBI->connect("DBI:mysql:database=mysql;host=$dbhost;port=$dbport",
$mysql_user, $mysql_pass,
{ PrintError => 1 })
or die "Can't connect to MySQL database 'mysql': ". $DBI::errstr;
log_debug("connected to $dbhost:$dbport as $mysql_user");
$dbh->do("SET sql_log_bin=0")
or die "Cannot set sql_log_bin=0: ".$DBI::errstr;
return;
}
sub get_user_pass {
my $h_ref = shift
|| die "No data to work with, check file $CONSTANTS_YML";
my $opts = shift;
my $yml_ref = shift;
my $deep = 0;
my @data;
foreach my $ref (keys %{$h_ref}) {
die "Malformed u/p pair for entry $ref"
unless defined $h_ref->{$ref}{u} && defined $h_ref->{$ref}{p};
my $u_ref = \$h_ref->{$ref}{u};
my $p_ref = \$h_ref->{$ref}{p};
#log_debug(sprintf "%s%s.%s", " "x(($deep+1)*2), $ref, ${$u_ref});
if ($init_passwords) {
${$p_ref} = pwgen();
}
push @data, { user => ${$u_ref}, pass => ${$p_ref} };
}
return \@data;
}
sub adjust_replication_master_info {
my ($user, $pass) = @_;
my $ch = $dbh->prepare("show slave status")
or die "Cannot prepare: ".$DBI::errstr;
$ch->execute() or die "Cannot execute: ".$DBI::errstr;
my $fields = $ch->{NAME};
my $vals = $ch->fetchall_arrayref();
$ch->finish;
my %data = ();
for (my $i=0;$i<scalar @{$fields};$i++) {
$data{$fields->[$i]} = $vals->[0][$i];
}
if (defined $data{Master_Server_Id}) {
$dbh->do("STOP SLAVE");
die "Cannot stop slave: ".$DBI::errstr if $DBI::err;
log_info("* updating replication password for user $user");
$dbh->do("CHANGE MASTER TO MASTER_PASSWORD=?", undef, $pass);
$dbh->do("START SLAVE");
die "Cannot start slave: ".$DBI::errstr if $DBI::err;
}
return;
}
sub sync_user {
my ($user, $pass) = @_;
my $sth_sel = $dbh->prepare(<<SQL);
SELECT count(User), Password = PASSWORD(?) as matched
FROM user
WHERE User = ?
GROUP by matched
ORDER by matched ASC
LIMIT 1
SQL
my $sth_upd = $dbh->prepare(<<SQL);
UPDATE user
SET Password=PASSWORD(?)
WHERE User = ?
AND Password != PASSWORD(?)
SQL
$sth_sel->execute($pass, $user)
or die "Cannot execute: ".$DBI::errstr;
my ($count, $match) = $sth_sel->fetchrow_array();
$count //= 0;
if ($count && !$match) {
if ($debug) {
log_debug(sprintf "%s => %s", $user, $pass);
} else {
log_info(sprintf "%s", $user);
}
unless ($test_mode) {
my $rows = $sth_upd->execute($pass, $user, $pass)
or die "Cannot update: ".$DBI::errstr;
if ($rows != $count) {
log_warn(sprintf
"User update was supposed to affect %d rows but changed %d",
$count, $rows);
}
if ($user eq 'replicator') {
adjust_replication_master_info($user, $pass);
}
}
} elsif (!$count) {
log_debug(sprintf "%s => does not exist in mysql, skipped (check grants.yml and run ngcp-sync-grants)", $user);
} else {
$count = 0;
}
$sth_sel->finish;
$sth_upd->finish;
return $test_mode ? 0 : $count;
}
sub sync_mysql_data {
my $rc = 0;
my $data = get_user_pass($yml->{credentials}{mysql});
# special handling for user sipwise as it is not in constants.yml
push @$data, { user => $DEFAULT_MYSQL_USER,
pass => get_sipwise_db_password() };
foreach my $pair (@{$data}) {
if (defined $pair->{user} and defined $pair->{pass}) {
unless ($pair->{pass}) {
die sprintf "Empty password %s for user %s",
@{$pair}{qw(pass user)};
}
if ($pair->{pass} eq $PW_UNDEF) {
die sprintf "Undefined password %s for user %s",
@{$pair}{qw(pass user)};
}
$rc += sync_user(@{$pair}{qw(user pass)});
}
}
return $rc;
}
sub flush_privs {
return if $test_mode;
log_debug("flush priveleges");
$dbh->do('FLUSH PRIVILEGES')
or die "Can't flush MySQL privileges: ". $DBI::errstr;
return;
}
sub main {
eval {
$yml = YAML::XS::LoadFile($CONSTANTS_YML);
};
die "Can't read constants file: $EVAL_ERROR\n" if $EVAL_ERROR;
my ($dbhost, $dbport);
if (my $db_cfg = Config::Tiny->read($DB_CFG)) {
($dbhost, $dbport) = @{$db_cfg->{_}}{qw(PAIR_DBHOST PAIR_DBPORT)};
} else {
log_warn(sprintf "Cannot open %s: %s, using host=%s port=%s",
$DB_CFG, $ERRNO, $DEFAULT_DBHOST, $DEFAULT_DBPORT);
($dbhost, $dbport) = ($DEFAULT_DBHOST, $DEFAULT_DBPORT);
}
if ($init_passwords and not $test_mode and not -w $CONSTANTS_YML) {
die "$CONSTANTS_YML is not writable";
}
log_info("[TEST MODE]") if $test_mode;
get_nodename();
connect_db($dbhost, $dbport);
$dbh->begin_work or die "Cannot start transaction: ".$DBI::errstr;
eval {
my $rc = sync_mysql_data();
if ($rc) {
flush_privs();
} else {
log_debug("nothing to update");
}
};
if ($@) {
$dbh->rollback if $dbh;
$dbh->disconnect if $dbh;
die "Error: $@";
}
$dbh->commit if $dbh;
$dbh->disconnect if $dbh;
return unless $init_passwords;
return if $test_mode;
log_info("writing new passwords into $CONSTANTS_YML ... ");
YAML::XS::DumpFile($CONSTANTS_YML, $yml);
log_info("done");
return;
}
#----------------------------------------------------------------------
main();
exit $error;
__END__
=pod
=head1 NAME
ngcp-sync-constants - synchronizes passwords from constants.yml with MySQL
=head1 SYNOPSIS
B<ngcp-sync-constants> [I<options>...]
=head1 DESCRIPTION
B<This program> reads constants.yml file, parses it and synchronizes all
required passwords with MySQL.
=head1 OPTIONS
=over 8
=item B<--root>
Use mysql root user without password as DB credentials
=item B<--init-passwords>
New passwords are generated (passwords for "mysql" is not generated to avoid replication problems)
=item B<--test>
No real updates, only for checks
=item B<--verbose>
Verbose mode where all changes are written to STDOUT
=item B<--help>
Print this help message.
=back
=head1 EXIT STATUS
=over 8
=item B<exit code 0>
Everything is ok
=item B<exit code != 0>
Something is wrong, an error message raises
=back
=head1 DIAGNOSTICS
TODO
=head1 CONFIGURATION
/etc/ngcp-config/constants.yml
=head1 BUGS AND LIMITATIONS
Please report problems you notice to the Sipwise Development Team <support@sipwise.com>.
=head1 AUTHOR
Kirill Solomko <ksolomko@sipwise.com>
=head1 LICENSE
Copyright (C) 2016 Sipwise GmbH, Austria
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
=cut