mirror of https://github.com/sipwise/ngcpcfg.git
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.
440 lines
11 KiB
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
|