#!/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, "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[$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(<prepare(<prepare(<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-db-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-db-creds - synchronizes passwords from constants.yml with MySQL =head1 SYNOPSIS B [I...] =head1 DESCRIPTION B 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 Everything is ok =item B 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 . =head1 AUTHOR Kirill Solomko =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 . =cut