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

440 lines
11 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;
use Term::ReadPassword;
#----------------------------------------------------------------------
Readonly my $CONSTANTS_YML => "/etc/ngcp-config/constants.yml";
Readonly my $SIPWISE_EXTRA_CNF => "/etc/mysql/sipwise_extra.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;
my $custom_db_host;
my $custom_db_port;
my $no_warnings = 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
--db-host -- use custom db host
--db-port -- use custom db port
--no-warnings -- suppress warning messages
USAGE
exit 0;
}
GetOptions("h|?|help" => \&Usage,
"i|init-passwords" => \$init_passwords,
"r|root" => \$mysql_root,
"t|test" => \$test_mode,
"v|verbose" => \$debug,
"db-host=s" => \$custom_db_host,
"db-port=s" => \$custom_db_port,
"no-warnings" => \$no_warnings);
#----------------------------------------------------------------------
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 { $no_warnings || 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_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 $dsn = "DBI:mysql:database=mysql;host=${dbhost};port=${dbport}";
my $dbuser = '';
my $dbpass = '';
my $dbauthmsg = "'$SIPWISE_EXTRA_CNF'";
if ($mysql_root) {
$dbuser = 'root';
$dbauthmsg = "user '$dbuser'";
} else {
$dsn .= ";mysql_read_default_file=$SIPWISE_EXTRA_CNF";
}
if ($dbh = DBI->connect($dsn, $dbuser, $dbpass, { PrintError => 0 })) {
log_debug("connected to $dbhost:$dbport using $dbauthmsg");
} else {
warn "Cannot connect to MySQL database 'mysql' using $dbauthmsg: $DBI::errstr\n";
if ($mysql_root) {
while (1) {
$dbpass = read_password("MySQL $dbauthmsg password: ", undef, 1);
$dbh = DBI->connect($dsn, $dbuser, $dbpass, { PrintError => 0 })
or warn "Cannot connect to MySQL database 'mysql' using $dbauthmsg with provided password: $DBI::errstr\n";
last if $dbh;
}
log_debug("connected to $dbhost:$dbport using $dbauthmsg with provided password.");
} else {
exit 1;
}
}
$dbh->do("SET sql_log_bin=0")
or die "Cannot set sql_log_bin=0: $DBI::errstr\n";
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\n";
$ch->execute() or die "Cannot execute: $DBI::errstr\n";
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\n" 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\n" if $DBI::err;
}
return;
}
sub sync_user {
my ($user, $pass) = @_;
my ($mysql_version_raw) = $dbh->selectrow_array("SELECT VERSION()");
my $mysql_version = $mysql_version_raw =~ s/^(\d+\.\d+).+$/$1/r;
my $sth_sel;
if ($mysql_version >= 10.4) {
$sth_sel = $dbh->prepare(<<SQL);
SELECT Host,
JSON_EXTRACT(Priv, '\$.authentication_string') = PASSWORD(?) as matched
FROM global_priv
WHERE User = ?
AND JSON_EXTRACT(Priv, '\$.plugin') = 'mysql_native_password'
GROUP by matched, Host
SQL
} else {
$sth_sel = $dbh->prepare(<<SQL);
SELECT Host, Password = PASSWORD(?) as matched
FROM user
WHERE User = ?
GROUP by matched, Host
SQL
}
my $sth_upd = $dbh->prepare(<<SQL);
SET PASSWORD FOR ?@? = PASSWORD(?)
SQL
$sth_sel->execute($pass, $user)
or die "Cannot execute: $DBI::errstr\n";
my ($count, $changed) = (0,0);
while (my ($host, $matched) = $sth_sel->fetchrow_array()) {
$count++;
next if $matched;
if ($debug) {
log_debug(sprintf "%s@%s => %s", $user, $host, $pass);
} else {
log_info(sprintf "%s@%s", $user, $host);
}
unless ($test_mode) {
$sth_upd->execute($user, $host, $pass)
or die "Cannot update: $DBI::errstr\n";
$changed++;
if ($user eq 'replicator') {
adjust_replication_master_info($user, $pass);
}
}
}
unless ($count) {
log_debug(sprintf "%s => does not exist in mysql, skipped (check grants.yml and run ngcp-sync-grants)", $user);
}
$sth_sel->finish;
$sth_upd->finish;
return $changed;
}
sub sync_mysql_data {
my $rc = 0;
my $data = get_user_pass($yml->{credentials}{mysql});
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 privileges");
$dbh->do('FLUSH PRIVILEGES')
or die "Cannot flush MySQL privileges: $DBI::errstr\n";
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,
$custom_db_host // $DEFAULT_DBHOST,
$custom_db_port // $DEFAULT_DBPORT);
$dbhost = $custom_db_host // $DEFAULT_DBHOST;
$dbport = $custom_db_port // $DEFAULT_DBPORT;
}
$dbhost = $custom_db_host // $dbhost;
$dbport = $custom_db_port // $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\n";
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