sub require_mysql { return if ($require_mysql++); $mysql::use_global_login = 1; &foreign_require("mysql"); if (!$mysql::config{'login'}) { $mysql::config{'login'} = 'root'; $mysql::mysql_login ||= 'root'; $mysql::authstr = &mysql::make_authstr(); } %mconfig = &foreign_config("mysql"); $password_func = $mysql::password_func || "password"; $mysql_user_size = $config{'mysql_user_size'} || 16; } # check_depends_mysql(&dom) # Ensure that a sub-server has a parent server with MySQL enabled sub check_depends_mysql { return undef if (!$_[0]->{'parent'}); local $parent = &get_domain($_[0]->{'parent'}); return $text{'setup_edepmysql'} if (!$parent->{'mysql'}); return undef; } # check_anti_depends_mysql(&dom) # Ensure that a parent server without MySQL does not have any children with it sub check_anti_depends_mysql { if (!$_[0]->{'mysql'}) { local @subs = &get_domain_by("parent", $_[0]->{'id'}); foreach my $s (@subs) { return $text{'setup_edepmysqlsub'} if ($s->{'mysql'}); } } return undef; } # check_mysql_clash(&domain, [field]) # Returns 1 if some MySQL user or database is used by another domain sub check_mysql_clash { local ($d, $field) = @_; local @doms = grep { $_->{'mysql'} && $_->{'id'} ne $d->{'id'} } &list_domains(); # Check for DB clash if (!$field || $field eq 'db') { foreach my $od (@doms) { foreach my $db (split(/\s+/, $od->{'db_mysql'})) { if ($db eq $d->{'db'}) { return &text('setup_emysqldbdom', $d->{'db'}, &show_domain_name($od)); } } } } # Check for user clash if (!$d->{'parent'} && (!$field || $field eq 'user')) { foreach my $od (@doms) { if (&mysql_user($d) eq &mysql_user($od)) { return &text('setup_emysqluserdom', &mysql_user($d), &show_domain_name($od)); } } } return undef; } # setup_mysql(&domain, [no-db]) # Create a new MySQL database, user and permissions sub setup_mysql { local ($d, $nodb) = @_; local $tmpl = &get_template($d->{'template'}); &require_mysql(); $d->{'mysql_user'} = &mysql_user($d); local $user = $d->{'mysql_user'}; if ($d->{'provision_mysql'}) { # Create the user on provisioning server if (!$d->{'parent'}) { &$first_print($text{'setup_mysqluser_provision'}); my $info = { 'user' => $user, 'domain-owner' => '' }; if ($d->{'mysql_enc_pass'}) { $info->{'encpass'} = $d->{'mysql_enc_pass'}; } else { $info->{'pass'} = &mysql_pass($d); } local @hosts = &unique(map { &to_ipaddress($_) } &get_mysql_hosts($d, 2)); $info->{'remote'} = \@hosts; my $conns = &get_mysql_user_connections($d, 0); $info->{'conns'} = $conns if ($conns); my ($ok, $msg) = &provision_api_call( "provision-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('setup_emysqluser_provision', $msg)); return 0; } # Configure MySQL module to use that system from now on my $mysql_host = $msg =~ /host=(\S+)/ ? $1 : undef; my $mysql_user = $msg =~ /owner_user=(\S+)/ ? $1 : undef; my $mysql_pass = $msg =~ /owner_pass=(\S+)/ ? $1 : undef; my @mdoms = grep { $_->{'mysql'} && $_->{'id'} ne $d->{'id'} } &list_domains(); if (!$mysql::config{'host'} || !@mdoms) { # Configure MySQL module $mysql::config{'host'} = $mysql_host; $mysql::config{'login'} = $mysql_user; $mysql::config{'pass'} = $mysql_pass; $mysql::mysql_login = $mysql_user; $mysql::mysql_pass = $mysql_pass; $mysql::authstr = &mysql::make_authstr(); &mysql::save_module_config(\%mysql::config, 'mysql'); } elsif ($mysql::config{'host'} ne $mysql_host) { # Mis-match with current setting!? &$second_print(&text('setup_emysqluser_provclash', $mysql::config{'host'}, $mysql_host)); return 0; } &$second_print(&text('setup_mysqluser_provisioned', $mysql_host)); } } else { # Create the user local @hosts = &get_mysql_hosts($d, 1); if (&indexof("%", @hosts) >= 0 && &indexof("localhost", @hosts) < 0 && &indexof("127.0.0.1", @hosts) < 0) { # Always add localhost if % was allowed push(@hosts, "localhost"); } local $wild = &substitute_domain_template($tmpl->{'mysql_wild'}, $d); if (!$d->{'parent'}) { &$first_print($text{'setup_mysqluser'}); local $cfunc = sub { local $encpass = &encrypted_mysql_pass($d); foreach my $h (@hosts) { &mysql::execute_sql_logged($mysql::master_db, "delete from user where host = '$h' ". "and user = '$user'"); &execute_user_creation_sql($h, $user,$encpass); if ($wild && $wild ne $d->{'db'}) { &add_db_table($h, $wild, $user); } &set_mysql_user_connections($d, $h, $user, 0); } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($cfunc); &$second_print($text{'setup_done'}); } } # Create the initial DB (if requested) local $ok; if (!$nodb && $tmpl->{'mysql_mkdb'} && !$d->{'no_mysql_db'}) { local $opts = &default_mysql_creation_opts($d); $ok = &create_mysql_database($d, $d->{'db'}, $opts); } else { # No DBs can exist $ok = 1; $d->{'db_mysql'} = ""; } # Save the initial password if ($tmpl->{'mysql_nopass'}) { &set_mysql_pass($d, &mysql_pass($d, 1)); } return $ok; } # add_db_table(host, db, user) # Adds an entry to the db table, with all permission columns set to Y sub add_db_table { local ($host, $db, $user) = @_; local @str = &mysql::table_structure($mysql::master_db, 'db'); local ($s, @fields, @yeses); foreach $s (@str) { if ($s->{'field'} =~ /_priv$/i) { push(@fields, $s->{'field'}); push(@yeses, "'Y'"); } } local $qdb = "e_mysql_database($db); &mysql::execute_sql_logged($mysql::master_db, "delete from db where host = '$host' and db = '$qdb' and user = '$user'"); &mysql::execute_sql_logged($mysql::master_db, "insert into db (host, db, user, ".join(", ", @fields).") values ('$host', '$qdb', '$user', ".join(", ", @yeses).")"); } # delete_mysql(&domain, [preserve-remote]) # Delete mysql databases, the domain's mysql user and all permissions for both sub delete_mysql { local ($d, $preserve) = @_; &require_mysql(); my @dblist = &unique(split(/\s+/, $d->{'db_mysql'})); # If MySQL is hosted remotely, don't delete the DB on the assumption that # other servers sharing the DB will still be using it if ($preserve && &remote_mysql($d)) { &$first_print(&text('delete_mysqldb', join(" ", @dblist))); &$second_print(&text('delete_mysqlpreserve', $mysql::config{'host'})); return 1; } # Get the domain's users, so we can remove their MySQL logins local @users = &list_domain_users($d, 1, 1, 1, 0); # First remove the databases if ($d->{'db_mysql'}) { &delete_mysql_database($d, @dblist); } if ($d->{'provision_mysql'}) { # Remove the main user on the provisioning server if (!$d->{'parent'}) { &$first_print($text{'delete_mysqluser_provision'}); my $info = { 'user' => &mysql_user($d), 'host' => $mysql::config{'host'} }; my ($ok, $msg) = &provision_api_call( "unprovision-mysql-login", $info, 0); if ($ok) { &$second_print($text{'setup_done'}); } else { &$second_print(&text('delete_emysqluser_provision', $msg)); return 0; } } # Take away access from mailbox users foreach my $u (@users) { my @mydbs = grep { $_->{'type'} eq 'mysql' } @{$u->{'dbs'}}; if (@mydbs) { &delete_mysql_database_user($d, $u->{'user'}); } } # If this was the last domain with MySQL enabled on the system, # turn off use of the remote host that if it gets enabled again, a # new host and login are used my @mdoms = grep { $_->{'mysql'} && $_->{'id'} ne $d->{'id'} } &list_domains(); if (!@mdoms && $mysql::config{'host'}) { delete($mysql::config{'host'}); $mysql::authstr = &mysql::make_authstr(); &mysql::save_module_config(\%mysql::config, 'mysql'); } } else { # Remove the main user locally &$first_print($text{'delete_mysqluser'}) if (!$d->{'parent'}); local $dfunc = sub { local $user = &mysql_user($d); local $tmpl = &get_template($d->{'template'}); local $wild = &substitute_domain_template( $tmpl->{'mysql_wild'}, $d); if (!$d->{'parent'}) { # Delete the user and any database permissions &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = '$user'"); &mysql::execute_sql_logged($mysql::master_db, "delete from db where user = '$user'"); } if ($wild && $wild ne $d->{'db'}) { # Remove any wildcard entry for the user &mysql::execute_sql_logged($mysql::master_db, "delete from db where db = '$wild'"); } # Remove any other users. This has to be done here, as when # users in the domain are deleted they won't be able to find # their database privileges anymore. foreach my $u (@users) { foreach my $udb (@{$u->{'dbs'}}) { if ($udb->{'type'} eq 'mysql') { local $myuser = &mysql_username($u->{'user'}); &mysql::execute_sql_logged( $mysql::master_db, "delete from user where user = ?", $myuser); &mysql::execute_sql_logged( $mysql::master_db, "delete from db where user = ?", $myuser); } } } &mysql::execute_sql_logged( $mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($dfunc); &$second_print($text{'setup_done'}) if (!$d->{'parent'}); } return 1; } # modify_mysql(&domain, &olddomain) # Changes the mysql user's password if needed sub modify_mysql { local ($d, $oldd) = @_; local $tmpl = &get_template($d->{'template'}); &require_mysql(); local $rv = 0; local $changeduser = $d->{'user'} ne $oldd->{'user'} && !$tmpl->{'mysql_nouser'} ? 1 : 0; local $olduser = &mysql_user($oldd); local $user = &mysql_user($d, $changeduser); local $oldencpass = &encrypted_mysql_pass($oldd); local $encpass = &encrypted_mysql_pass($d); local @dbnames = map { $_->{'name'} } &domain_databases($d, [ "mysql" ]); if ($encpass ne $oldencpass && !$d->{'parent'} && !$oldd->{'parent'} && (!$tmpl->{'mysql_nopass'} || $d->{'mysql_pass'})) { # Change MySQL password, for a top-level server that isn't being # converted from a sub-server if ($d->{'provision_mysql'}) { # Change on provisioning server &$first_print($text{'save_mysqlpass_provision'}); my $info = { 'user' => &mysql_user($d), 'host' => $mysql::config{'host'} }; if ($d->{'mysql_enc_pass'}) { $info->{'encpass'} = $d->{'mysql_enc_pass'}; } else { $info->{'pass'} = &mysql_pass($d); } my ($ok, $msg) = &provision_api_call("modify-mysql-login", $info, 0); if (!$ok) { &$second_print(&text('save_emysqlpass_provision',$msg)); } else { &$second_print($text{'setup_done'}); } $rv++; } else { # Change locally &$first_print($text{'save_mysqlpass'}); if (&mysql_user_exists($d)) { local $pfunc = sub { &execute_password_change_sql( $olduser, $encpass); }; &execute_for_all_mysql_servers($pfunc); &$second_print($text{'setup_done'}); $rv++; } else { &$second_print($text{'save_nomysql'}); } } } if (!$d->{'parent'} && $oldd->{'parent'}) { # Server has been converted to a parent .. need to create user, and # change access to old DBs $d->{'mysql_user'} = &mysql_user($d, 1); local $user = $d->{'mysql_user'}; local @hosts = &get_mysql_hosts($d); # If hashed passwords are in use, generate a random MySQL password # for the new MySQL user if ($tmpl->{'hashpass'}) { $d->{'mysql_pass'} = &random_password(8); delete($d->{'mysql_enc_pass'}); } if ($d->{'provision_mysql'}) { # Change on provisioning server .. first create new user &$first_print($text{'setup_mysqluser_provision'}); my $info = { 'user' => $user, 'domain-owner' => '' }; if ($d->{'mysql_enc_pass'}) { $info->{'encpass'} = $d->{'mysql_enc_pass'}; } else { $info->{'pass'} = &mysql_pass($d); } local @hosts = map { &to_ipaddress($_) } @hosts; $info->{'remote'} = \@hosts; my $conns = &get_mysql_user_connections($d, 0); $info->{'conns'} = $conns if ($conns); my ($ok, $msg) = &provision_api_call( "provision-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('setup_emysqluser_provision', $msg)); } # Then take away DBs from old user if ($ok && @dbnames) { my $info = { 'user' => $olduser, 'host' => $mysql::config{'host'}, 'remove-database' => \@dbnames }; ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision', $msg)); } } # Grant to new user if ($ok && @dbnames) { my $info = { 'user' => $user, 'host' => $mysql::config{'host'}, 'add-database' => \@dbnames }; ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision2', $msg)); } } if ($ok) { &$second_print($text{'setup_done'}); } } else { # Change locally &$first_print($text{'setup_mysqluser'}); local $wild = &substitute_domain_template( $tmpl->{'mysql_wild'}, $d); local $encpass = &encrypted_mysql_pass($d); local $pfunc = sub { local $h; foreach $h (@hosts) { &execute_user_creation_sql($h, $user,$encpass); if ($wild && $wild ne $d->{'db'}) { &add_db_table($h, $wild, $user); } &set_mysql_user_connections($d, $h, $user, 0); } foreach my $db (@dbnames) { local $qdb = "e_mysql_database($db); &mysql::execute_sql_logged($mysql::master_db, "update db set user = ? where user = ? and ". "(db = ? or db = ?)", $user, $olduser, $db, $qdb); } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($pfunc); &$second_print($text{'setup_done'}); } $rv++; } elsif ($d->{'parent'} && !$oldd->{'parent'}) { # Server has changed from parent to sub-server .. need to remove the # old user and update all DB permissions if ($d->{'provision_mysql'}) { # Update on provisioning server .. first remove ownership # of all DBs &$first_print($text{'save_mysqluser_provision'}); my ($ok, $msg) = (1, undef); if (@dbnames) { my $info = { 'user' => $olduser, 'host' => $mysql::config{'host'}, 'remove-database' => \@dbnames }; ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision', $msg)); } } # Then remove the user if ($ok && $mysql::config{'host'}) { my $info = { 'user' => $olduser, 'host' => $mysql::config{'host'} }; ($ok, $msg) = &provision_api_call( "unprovision-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser_provision',$msg)); } } # Then grant DBs to new user if ($ok && @dbnames) { my $info = { 'user' => $user, 'host' => $mysql::config{'host'}, 'add-database' => \@dbnames }; ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision2', $msg)); } } if ($ok) { &$second_print($text{'setup_done'}); } } else { # Update locally &$first_print($text{'save_mysqluser'}); local $pfunc = sub { &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = ?", $olduser); &mysql::execute_sql_logged($mysql::master_db, "update db set user = ? where user = ?", $user, $olduser); &mysql::execute_sql_logged($master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($pfunc); &$second_print($text{'setup_done'}); $rv++; } } elsif ($user ne $olduser && !$d->{'parent'}) { # MySQL user in a parent domain has changed, perhaps due to username # change. Need to update user in DB and all db entries if ($d->{'provision_mysql'}) { # Rename on provisioning server &$first_print($text{'save_mysqluser_provision'}); my $info = { 'user' => $olduser, 'host' => $mysql::config{'host'}, 'new-user' => $user }; my ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print(&text('save_emysqluser_provision',$msg)); } else { &$second_print($text{'setup_done'}); } $rv++; } else { # Rename locally &$first_print($text{'save_mysqluser'}); if (&mysql_user_exists($oldd)) { $d->{'mysql_user'} = $user; local $pfunc = sub { &mysql::execute_sql_logged($mysql::master_db, "update user set user = ? where user = ?", $user, $olduser); &mysql::execute_sql_logged($mysql::master_db, "update db set user = ? where user = ?", $user, $olduser); &mysql::execute_sql_logged($master_db, "flush privileges"); }; &execute_for_all_mysql_servers($pfunc); &$second_print($text{'setup_done'}); $rv++; } else { &$second_print($text{'save_nomysql'}); } } } elsif ($user ne $olduser && $d->{'parent'} && @dbnames) { # Sub-server has moved to a new user .. change ownership of DBs if ($d->{'provision_mysql'}) { # Change on provisioning server, by removing DBs from the old # owner's list, and added to new owner's list &$first_print($text{'save_mysqluser2_provision'}); my $info = { 'user' => $olduser, 'host' => $mysql::config{'host'}, 'remove-database' => \@dbnames }; my ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision', $msg)); } else { # Add databases back to the new owner my $info = { 'user' => $user, 'host' => $mysql::config{'host'}, 'add-database' => \@dbnames }; my ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print( &text('save_emysqluser2_provision2', $msg)); } else { &$second_print($text{'setup_done'}); } } $rv++; } else { # Change locally &$first_print($text{'save_mysqluser2'}); local $pfunc = sub { foreach my $db (@dbnames) { local $qdb = "e_mysql_database($db); &mysql::execute_sql_logged($mysql::master_db, "update db set user = ? where user = ? ". "and (db = ? or db = ?)", $user, $olduser, $db, $qdb); } &mysql::execute_sql_logged($master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($pfunc); $rv++; &$second_print($text{'setup_done'}); } } if ($d->{'group'} ne $oldd->{'group'} && $tmpl->{'mysql_chgrp'}) { # Unix group has changed - fix permissions on all DB files &$first_print($text{'save_mysqlgroup'}); foreach my $db (&domain_databases($d, [ "mysql" ])) { local $dd = &get_mysql_database_dir($db->{'name'}); if ($dd) { &system_logged("chgrp -R $d->{'group'} ". quotemeta($dd)); } } &$second_print($text{'setup_done'}); } return $rv; } # clone_mysql(&domain, &old-domain) # Copy all databases and their contents to a new domain sub clone_mysql { local ($d, $oldd) = @_; &$first_print($text{'clone_mysql'}); # Re-create each DB with a new name local %dbmap; my @dbs = &domain_databases($oldd, [ 'mysql' ]); foreach my $db (@dbs) { local $newname = $db->{'name'}; local $newprefix = &fix_database_name($d->{'prefix'}, 'mysql'); local $oldprefix = &fix_database_name($oldd->{'prefix'}, 'mysql'); if ($newname eq $oldd->{'db'} && $oldd->{'db'} eq &database_name($oldd)) { # If the DB name was the primary database for the old domain, # set the new DB name to be the primary database for the new # domain $newname = $d->{'db'}; } elsif ($newname !~ s/\Q$oldprefix\E/$newprefix/) { # Otherwise, just replace the DB name prefix. If that isn't # possible, prepend the new prefix as a last resort or just # use the new prefix if this is the only database in the domain &$second_print(&text('clone_mysqlprefix', $newname, $oldprefix, $newprefix)); if (@dbs == 1 && !&check_mysql_database_clash($d, $newprefix)) { # This domain has only one database, so we can just use # the new prefix directly (as long as it doesn't clash) $newname = $newprefix; } else { # Prepend new prefix $newname = $newprefix.$newname; } &$second_print(&text('clone_mysqlprefix2', $newname)); } if (&check_mysql_database_clash($d, $newname)) { &$second_print(&text('clone_mysqlclash', $newname)); next; } &push_all_print(); &set_all_null_print(); local $opts = &get_mysql_creation_opts($oldd, $db->{'name'}); local $ok = &create_mysql_database($d, $newname, $opts); &pop_all_print(); if (!$ok) { &$second_print(&text('clone_mysqlcreate', $newname)); } else { $dbmap{$newname} = $db->{'name'}; } } &$second_print(&text('clone_mysqldone', scalar(keys %dbmap))); # Copy across contents if (%dbmap) { &require_mysql(); &$first_print($text{'clone_mysqlcopy'}); foreach my $db (&domain_databases($d, [ 'mysql' ])) { local $oldname = $dbmap{$db->{'name'}}; local $temp = &transname(); local $err = &mysql::backup_database($oldname, $temp, 0, 1, 0, undef, undef, undef, undef, 1); if ($err) { &$second_print(&text('clone_mysqlbackup', $oldname, $err)); next; } local ($ex, $out) = &mysql::execute_sql_file($db->{'name'}, $temp); &unlink_file($temp); if ($ex) { &$second_print(&text('clone_mysqlrestore', $db->{'name'}, $out)); next; } } &$second_print($text{'setup_done'}); } if (!$d->{'parent'}) { # Duplicate allowed hosts local @allowed = &get_mysql_allowed_hosts($oldd); &save_mysql_allowed_hosts($d, \@allowed); } } # validate_mysql(&domain) # Make sure all MySQL databases exist, and that the admin user exists sub validate_mysql { local ($d) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Check login on provisioning server my ($ok, $msg) = &provision_api_call( "check-mysql-login", { 'user' => &mysql_user($d) }); if (!$ok) { return &text('validate_emysqlcheck', $msg); } elsif ($msg !~ /host=(\S+)/) { return &text('validate_emysqluser', &mysql_user($d)); } elsif ($1 ne $mysql::config{'host'}) { return &text('validate_emysqluserhost', $1, $mysql::config{'host'}); } # Check DBs on provisioning server foreach my $db (&domain_databases($d, [ "mysql" ])) { my ($ok, $msg) = &provision_api_call( "check-mysql-database", { 'database' => $db->{'name'} }); if (!$ok) { return &text('validate_emysqlcheck', $db->{'name'}, $msg); } elsif ($msg !~ /host=(\S+)/) { return &text('validate_emysql', $db->{'name'}); } } } else { # Check locally local %got = map { $_, 1 } &mysql::list_databases(); foreach my $db (&domain_databases($d, [ "mysql" ])) { $got{$db->{'name'}} || return &text('validate_emysql', $db->{'name'}); } if (!&mysql_user_exists($d)) { return &text('validate_emysqluser', &mysql_user($d)); } } return undef; } # disable_mysql(&domain) # Modifies the mysql user for this domain so that he cannot login sub disable_mysql { local ($d) = @_; &require_mysql(); if ($d->{'parent'}) { &$second_print($text{'save_nomysqlpar'}); } elsif ($d->{'provision_mysql'}) { # Lock on provisioning server &$first_print($text{'disable_mysqluser_provision'}); my $info = { 'user' => &mysql_user($d), 'host' => $mysql::config{'host'}, 'lock' => '' }; my ($ok, $msg) = &provision_api_call("modify-mysql-login", $info, 0); if (!$ok) { &$second_print(&text('disable_emysqluser_provision', $msg)); return 0; } else { &$second_print($text{'setup_done'}); return 1; } } else { # Lock locally &$first_print($text{'disable_mysqluser'}); local $user = &mysql_user($d); if ($oldpass = &mysql_user_exists($d)) { local $dfunc = sub { &execute_password_change_sql($user, "'".("0" x 41)."'"); }; &execute_for_all_mysql_servers($dfunc); $d->{'disabled_oldmysql'} = $oldpass; &$second_print($text{'setup_done'}); return 1; } else { &$second_print($text{'save_nomysql'}); return 0; } } } # enable_mysql(&domain) # Puts back the original password for the mysql user so that he can login again sub enable_mysql { local ($d) = @_; &require_mysql(); if ($d->{'parent'}) { &$second_print($text{'save_nomysqlpar'}); return 0; } elsif ($d->{'provision_mysql'}) { # Unlock on provisioning server &$first_print($text{'enable_mysql_provision'}); my $info = { 'user' => &mysql_user($d), 'host' => $mysql::config{'host'}, 'unlock' => '' }; my ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); if (!$ok) { &$second_print(&text('enable_emysql_provision', $msg)); return 0; } else { &$second_print($text{'setup_done'}); return 1; } } else { # Un-lock locally &$first_print($text{'enable_mysql'}); local $user = &mysql_user($d); if (&mysql_user_exists($d)) { local $efunc = sub { if ($d->{'disabled_oldmysql'}) { local $qpass = &mysql_escape( $d->{'disabled_oldmysql'}); &execute_password_change_sql($user, "'$qpass'"); } else { local $pass = &mysql_pass($d); local $qpass = &mysql_escape($pass); &execute_password_change_sql($user, "$password_func('$qpass')"); } &mysql::execute_sql($master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($efunc); delete($d->{'disabled_oldmysql'}); &$second_print($text{'setup_done'}); return 1; } else { &$second_print($text{'save_nomysql'}); return 0; } } } # mysql_user_exists(&domain) # Returns his password if a mysql user exists for the domain's user, or undef sub mysql_user_exists { &require_mysql(); local $user = &mysql_user($_[0]); local $u; eval { # Try old password column first local $main::error_must_die = 1; $u = &mysql::execute_sql($mysql::master_db, "select password from user where user = ?", $user); }; if ($@ || @{$u->{'data'}} && $u->{'data'}->[0]->[0] eq '') { # Try new mysql user table format if the password query failed, or # if there was no password eval { local $main::error_must_die = 1; $u = &mysql::execute_sql($mysql::master_db, "select authentication_string from user where user = ?", $user); }; } foreach my $r (@{$u->{'data'}}) { return $r->[0] if ($r->[0]); } return undef; } # check_warnings_mysql(&dom, &old-domain) # Return warning if a MySQL database or user with a clashing name exists. # This can be overridden to allow a takeover of the DB. sub check_warnings_mysql { local ($d, $oldd) = @_; $d->{'mysql'} && (!$oldd || !$oldd->{'mysql'}) || return undef; if ($d->{'provision_mysql'}) { # DB clash on provisioning server my ($ok, $msg) = &provision_api_call( "check-mysql-database", { 'database' => $d->{'db'} }); return &text('provision_emysqldbcheck', $msg) if (!$ok); if ($msg =~ /host=/) { return &text('provision_emysqldb', $d->{'db'}); } # User clash on provisioning server if (!$d->{'parent'}) { my ($ok, $msg) = &provision_api_call( "check-mysql-login", { 'user' => &mysql_user($d) }); return &text('provision_emysqlcheck', $msg) if (!$ok); if ($msg =~ /host=/) { return &text('provision_emysql', &mysql_user($d)); } } } else { # DB clash on local &require_mysql(); local @dblist = &mysql::list_databases(); return &text('setup_emysqldb', $d->{'db'}) if (&indexof($d->{'db'}, @dblist) >= 0); # User clash on local if (!$d->{'parent'}) { return &text('setup_emysqluser', &mysql_user($d)) if (&mysql_user_exists($d)); } } return undef; } # backup_mysql(&domain, file) # Dumps this domain's mysql database to a backup file sub backup_mysql { local ($d, $file) = @_; &require_mysql(); # Find all domain's databases local $tmpl = &get_template($d->{'template'}); local $wild = &substitute_domain_template($tmpl->{'mysql_wild'}, $d); local @alldbs = &list_all_mysql_databases($d); local @dbs; if ($wild) { $wild =~ s/\%/\.\*/g; $wild =~ s/_/\./g; @dbs = grep { /^$wild$/i } @alldbs; } push(@dbs, split(/\s+/, $d->{'db_mysql'})); @dbs = &unique(@dbs); # Filter out any excluded DBs my @exclude = &get_backup_db_excludes($d); my %exclude = map { $_, 1 } @exclude; @dbs = grep { !$exclude{$_} } @dbs; # Create base backup file with meta-information local @hosts = &get_mysql_allowed_hosts($d); local %info = ( 'hosts' => join(' ', @hosts), 'remote' => $mysql::config{'host'} ); &write_as_domain_user($d, sub { &write_file($file, \%info) }); # Back them all up local $db; local $ok = 1; foreach $db (@dbs) { &$first_print(&text('backup_mysqldump', $db)); local $dbfile = $file."_".$db; # Limit tables to those that aren't excluded my %texclude = map { $_, 1 } map { (split(/\./, $_))[1] } grep { /^\Q$db\E\./ || /^\*\./ } @exclude; my $tables; if (%texclude) { $tables = [ grep { !$texclude{$_} } &mysql::list_tables($db) ]; } local $err = &mysql::backup_database($db, $dbfile, 0, 1, undef, undef, undef, $tables, $d->{'user'}, 1); if (!$err) { $err = &validate_mysql_backup($dbfile); } if ($err) { &$second_print(&text('backup_mysqldumpfailed', "
$err
")); $ok = 0; } elsif ($config{'gzip_mysql'}) { # Backup worked .. gzip the file unlink($dbfile.".gz"); # Prevent malicious symlink my $out = &backquote_logged( "gzip ".quotemeta($dbfile)." 2>&1"); if ($?) { &$second_print(&text('backup_mysqlgzipfailed', "
$out
")); $ok = 0; } else { &$second_print($text{'setup_done'}); } } else { # No need to compress &$second_print($text{'setup_done'}); } } return $ok; } # restore_mysql(&domain, file, &opts, &allopts, homeformat, &oldd, asowner) # Restores this domain's mysql database from a backup file, and re-creates # the mysql user. sub restore_mysql { local ($d, $file, $opts, $allopts, $homefmt, $oldd, $asd) = @_; local %info; &read_file($file, \%info); &require_mysql(); # Re-grant allowed hosts from backup + local local @lhosts; if (!$d->{'parent'} && $info{'hosts'}) { &$first_print($text{'restore_mysqlgrant'}); @lhosts = &get_mysql_allowed_hosts($d); push(@lhosts, split(/\s+/, $info{'hosts'})); if (&indexof("%", @lhosts) >= 0 && &indexof("localhost", @lhosts) < 0 && &indexof("127.0.0.1", @lhosts) < 0) { # If all hosts were allowed previously via % but localhost was # not, add it now. This is needed because some MySQL versions # (such as the one seen on Ubuntu 12.04) do not allow localhost # connections even if % is granted push(@lhosts, "localhost"); } @lhosts = &unique(@lhosts); &save_mysql_allowed_hosts($d, \@lhosts); &$second_print($text{'setup_done'}); } # If in replication mode, AND the remote MySQL system is the same on both # systems, do nothing if ($allopts->{'repl'} && $mysql::config{'host'} && $info{'remote'} && $mysql::config{'host'} eq $info{'remote'}) { &$first_print($text{'restore_mysqldummy'}); &$second_print(&text('restore_mysqlsameremote', $info{'remote'})); return 1; } # For DBs that exist already, save their user lists for later restore local (%userdbs, %userpasses); foreach my $db (&domain_databases($d, [ 'mysql' ])) { foreach my $u (&list_mysql_database_users($d, $db->{'name'})) { if ($u->[0] ne $d->{'user'} && $u->[0] ne 'root' && $u->[0] ne $mysql::config{'login'}) { push(@{$userdbs{$u->[0]}}, $db->{'name'}); $userpasses{$u->[0]} = $u->[1]; } } } if (!$d->{'wasmissing'}) { # Only delete and re-create databases if this domain was not created # as part of the restore process. &$first_print($text{'restore_mysqldrop'}); { local $first_print = \&null_print; # supress messages local $second_print = \&null_print; # First clear out all current databases and the MySQL login &delete_mysql($d); # Now re-set up the login only &setup_mysql($d, 1); } &$second_print($text{'setup_done'}); } # Re-grant allowed hosts, as deleting and re-creating DBs may have cleared them if (@lhosts) { &save_mysql_allowed_hosts($d, \@lhosts); } # Work out which databases are in backup local ($dbfile, @dbs); foreach $dbfile (glob($file."_*")) { if (-r $dbfile) { $dbfile =~ /\Q$file\E_(.*)\.gz$/ || $dbfile =~ /\Q$file\E_(.*)$/; push(@dbs, [ $1, $dbfile ]); } } # Turn off quotas for the domain, to prevent the import failing &disable_quotas($d); # Finally, import the data my $rv = 1; my %created; foreach my $db (@dbs) { my $clash = &check_mysql_database_clash($d, $db->[0]); if ($clash && $d->{'wasmissing'}) { # DB already exists, silently ignore it if not empty. # This can happen during a restore when MySQL is on a remote # system. my @tables = &mysql::list_tables($db->[0], 1); if (@tables) { next; } } &$first_print(&text('restore_mysqlload', $db->[0])); if ($clash && !$d->{'wasmissing'}) { # DB already exists, and this isn't a newly created domain &$second_print($text{'restore_mysqlclash'}); $rv = 0; last; } &$indent_print(); if (!$clash) { &create_mysql_database($d, $db->[0]); $created{$db->[0]} = 1; } &$outdent_print(); if ($db->[1] =~ /(.*)\.gz$/) { # Need to uncompress first unlink("$1"); # To prevent malicious link overwrite local $out = &backquote_logged( "gunzip ".quotemeta($db->[1])." 2>&1"); if ($?) { &$second_print(&text('restore_mysqlgunzipfailed', "
$out
")); $rv = 0; last; } $db->[1] =~ s/\.gz$//; } local ($ex, $out); if ($asd) { # As the domain owner ($ex, $out) = &mysql::execute_sql_file($db->[0], $db->[1], &mysql_user($d), &mysql_pass($d, 1)); } else { # As master admin ($ex, $out) = &mysql::execute_sql_file($db->[0], $db->[1]); } if ($ex) { &$second_print(&text('restore_mysqlloadfailed', "
$out
")); $rv = 0; last; } else { &$second_print($text{'setup_done'}); } } # If the restore re-created a domain, the list of databases should be synced # to those in the backup if ($d->{'wasmissing'}) { $d->{'db_mysql'} = join(" ", map { $_->[0] } @dbs); } # Grant back permissions to any users who had access to the restored DBs # previously foreach my $uname (keys %userdbs) { my @grant = grep { $created{$_} } @{$userdbs{$uname}}; if (@grant) { &create_mysql_database_user($d, \@grant, $uname, undef, $userpasses{$uname}); } } # Put quotas back &enable_quotas($d); return $rv; } # validate_mysql_backup(file) # Returns an error message if a file doesn't look like a valid MySQL backup sub validate_mysql_backup { local ($dbfile) = @_; open(DBFILE, $dbfile); local $first = ; close(DBFILE); if ($first =~ /^mysqldump:.*error/) { return $first; } return undef; } # mysql_user(&domain, [always-new]) # Returns the MySQL login name for a domain sub mysql_user { &require_mysql(); if ($_[0]->{'parent'}) { # Get from parent domain return &mysql_user(&get_domain($_[0]->{'parent'}), $_[1]); } return $_[0]->{'mysql_user'} if (defined($_[0]->{'mysql_user'}) && !$_[1]); return length($_[0]->{'user'}) > $mysql_user_size ? substr($_[0]->{'user'}, 0, $mysql_user_size) : $_[0]->{'user'}; } # set_mysql_user(&domain, newuser) # Updates a domain object with a new MySQL username sub set_mysql_user { &require_mysql(); $_[0]->{'mysql_user'} = length($_[1]) > $mysql_user_size ? substr($_[1], 0, $mysql_user_size) : $_[1]; } # mysql_username(username) # Adjusts a username to be suitable for MySQL sub mysql_username { &require_mysql(); return length($_[0]) > $mysql_user_size ? substr($_[0], 0, $mysql_user_size) : $_[0]; } # set_mysql_pass(&domain, [password]) # Updates a domain object to use the specified login for mysql. Does not # actually change the database - that must be done by modify_mysql. sub set_mysql_pass { local ($d, $pass) = @_; if (defined($pass)) { $d->{'mysql_pass'} = $pass; } else { delete($d->{'mysql_pass'}); } delete($d->{'mysql_enc_pass'}); # Clear encrypted password, as we # have a plain password now } # mysql_pass(&domain, [neverquote]) # Returns the plain-text password for the MySQL admin for this domain sub mysql_pass { if ($_[0]->{'parent'}) { # Password comes from parent domain local $parent = &get_domain($_[0]->{'parent'}); return &mysql_pass($parent); } return defined($_[0]->{'mysql_pass'}) ? $_[0]->{'mysql_pass'} : $_[0]->{'pass'}; } # mysql_enc_pass(&domain) # If this domain has only a pre-encrypted MySQL password, return it sub mysql_enc_pass { return $_[0]->{'mysql_enc_pass'}; } # mysql_escape(string) # Returns a string with quotes escaped, for use in SQL sub mysql_escape { local $rv = $_[0]; $rv =~ s/'/''/g; return $rv; } # mysql_size(&domain, dbname, [size-only]) # Returns the size, number of tables in a database, and size included in a # domain's Unix quota. sub mysql_size { &require_mysql(); local ($size, $qsize); local $dd = &get_mysql_database_dir($_[1]); if ($dd) { # Can check actual on-disk size $size = &disk_usage_kb($dd)*1024; local @dst = stat($dd); if (&has_group_quotas() && &has_mysql_quotas() && $dst[5] == $_[0]->{'gid'}) { $qsize = $size; } } else { # Use 'show table status' $size = 0; eval { local $main::error_must_die = 1; my $rv = &mysql::execute_sql($_[1], "show table status"); foreach my $r (@{$rv->{'data'}}) { $size += $r->[6]; } }; } local @tables; if (!$_[2]) { eval { # Make sure DBI errors don't cause a total failure local $main::error_must_die = 1; if ($d->{'provision_mysql'}) { # Stop supports_views from trying to access the # 'mysql' DB $mysql::supports_views_cache = 0; } @tables = &mysql::list_tables($_[1], 1); }; } return ($size, scalar(@tables), $qsize); } # check_mysql_database_clash(&domain, dbname) # Check if some MySQL database already exists sub check_mysql_database_clash { local ($d, $name) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Check on provisioning server my ($ok, $msg) = &provision_api_call( "check-mysql-database", { 'database' => $name }); &error(&text('provision_emysqldbcheck', $msg)) if (!$ok); return $msg =~ /host=/ ? 1 : 0; } else { # Check locally local @dblist = &mysql::list_databases(); return &indexof($name, @dblist) >= 0 ? 1 : 0; } } # create_mysql_database(&domain, dbname, &opts) # Add one database to this domain, and grants access to it to the user sub create_mysql_database { local ($d, $dbname, $opts) = @_; &require_mysql(); local @dbs = split(/\s+/, $d->{'db_mysql'}); if ($d->{'provision_mysql'}) { # Create the database on the provisioning server &$first_print(&text('setup_mysqldb_provision', $dbname)); my $info = { 'user' => &mysql_user($d), 'database' => $dbname }; $info->{'charset'} = $opts->{'charset'} if ($opts->{'charset'}); $info->{'collate'} = $opts->{'collate'} if ($opts->{'collate'}); my ($ok, $msg) = &provision_api_call( "provision-mysql-database", $info, 0); if (!$ok) { &$second_print(&text('setup_emysqldb_provision', $msg)); return 0; } &$second_print($text{'setup_done'}); } else { # Create the database locally, unless it already exists if (&indexof($dbname, &mysql::list_databases()) < 0) { &$first_print(&text('setup_mysqldb', $dbname)); &mysql::execute_sql_logged($mysql::master_db, "create database ".&mysql::quotestr($dbname). ($opts->{'charset'} ? " character set $opts->{'charset'}" : ""). ($opts->{'collate'} ? " collate $opts->{'collate'}" : "")); } else { &$first_print(&text('setup_mysqldbimport', $dbname)); } # Make the DB accessible to the domain owner &grant_mysql_database($d, $dbname); &$second_print($text{'setup_done'}); } push(@dbs, $dbname); $d->{'db_mysql'} = join(" ", &unique(@dbs)); return 1; } # grant_mysql_database(&domain, dbname) # Adds MySQL permission entries to grant the domain owner access to some DB, # and sets file ownership so that quotas work. sub grant_mysql_database { local ($d, $dbname) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Call remote API to grant access my $info = { 'user' => &mysql_user($d), 'host' => $mysql::config{'host'}, 'add-database' => $dbname }; my ($ok, $msg) = &provision_api_call("modify-mysql-login", $info, 0); &error(&text('user_emysqlprov', $msg)) if (!$ok); } else { # Add db entries for the user for each host local $pfunc = sub { local $h; local @hosts = &get_mysql_hosts($d); local $user = &mysql_user($d); foreach $h (@hosts) { &add_db_table($h, $dbname, $user); } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($pfunc); # Set group ownership of database directory, to enforce quotas local $dd = &get_mysql_database_dir($dbname); local $tmpl = &get_template($d->{'template'}); if ($tmpl->{'mysql_chgrp'} && $dd) { &system_logged("chgrp -R $d->{'group'} ".quotemeta($dd)); &system_logged("chmod +s ".quotemeta($dd)); } } } # delete_mysql_database(&domain, dbname, ...) # Remove one or more MySQL database from this domain sub delete_mysql_database { local ($d, @dbnames) = @_; &require_mysql(); local @dbs = split(/\s+/, $d->{'db_mysql'}); local @missing; local $failed = 0; if ($d->{'provision_mysql'}) { # Delete on provisioning server &$first_print(&text('delete_mysqldb_provision', join(", ", @dbnames))); foreach my $db (@dbnames) { my $info = { 'database' => $db, 'host' => $mysql::config{'host'} }; my ($ok, $msg) = &provision_api_call( "unprovision-mysql-database", $info, 0); if (!$ok) { &$second_print( &text('delete_emysqldb_provision', $msg)); $failed++; } @dbs = grep { $_ ne $db } @dbs; } } else { # Delete locally local @dblist = &mysql::list_databases(); &$first_print(&text('delete_mysqldb', join(", ", @dbnames))); foreach my $db (@dbnames) { local $qdb = "e_mysql_database($db); if (&indexof($db, @dblist) >= 0) { # Drop the DB &mysql::execute_sql_logged( $mysql::master_db, "drop database ". &mysql::quotestr($db)); if (defined(&mysql::delete_database_backup_job)) { &mysql::delete_database_backup_job($db); } } else { push(@missing, $db); &$second_print(&text('delete_mysqlmissing', $db)); $failed++; } @dbs = grep { $_ ne $db } @dbs; } # Drop permissions foreach my $db (@dbnames) { &revoke_mysql_database($d, $db); } } $d->{'db_mysql'} = join(" ", &unique(@dbs)); if (!$failed) { &$second_print($text{'setup_done'}); } } # revoke_mysql_database(&domain, dbname) # Remove a domain's access to a MySQL database, by delete from the db table. # Also resets group permissions. sub revoke_mysql_database { local ($d, $dbname) = @_; &require_mysql(); local @oldusers = &list_mysql_database_users($d, $dbname); local @users = &list_domain_users($d, 1, 1, 1, 0); local @unames = ( &mysql_user($d), map { &mysql_username($_->{'user'}) } @users ); # Take away MySQL permissions for users in this domain local $dfunc = sub { local $qdbname = "e_mysql_database($dbname); foreach my $uname (@unames) { &mysql::execute_sql_logged($mysql::master_db, "delete from db where (db = ? or db = ?) and user = ?", $dbname, $qdbname, $uname); } &mysql::execute_sql_logged($mysql::master_db, "flush privileges"); }; &execute_for_all_mysql_servers($dfunc); # If any users had access to this DB only, remove them too local $dfunc = sub { local $duser = &mysql_user($d); foreach my $up (grep { $_->[0] ne $duser } @oldusers) { local $o = &mysql::execute_sql($mysql::master_db, "select user from db where user = '$up->[0]'"); if (!@{$o->{'data'}}) { &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = '$up->[0]'"); } } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($dfunc); # Fix group owner, if the DB still exists, by setting to the owner of the # 'mysql' database local $tmpl = &get_template($d->{'template'}); local $dd = &get_mysql_database_dir($dbname); if ($tmpl->{'mysql_chgrp'} && $dd && -d $dd) { local @st = stat("$dd/../mysql"); local $group = scalar(@st) ? $st[5] : "mysql"; &system_logged("chgrp -R $group ".quotemeta($dd)); } } # get_mysql_database_dir(db) # Returns the directory in which a DB's files are stored, or undef if unknown. # If MySQL is running remotely, this will always return undef. sub get_mysql_database_dir { local ($db) = @_; &require_mysql(); return undef if ($config{'provision_mysql'}); return undef if ($mysql::config{'host'} && $mysql::config{'host'} ne 'localhost' && &to_ipaddress($mysql::config{'host'}) ne &to_ipaddress(&get_system_hostname())); my $mysql_dir; my $conf = &mysql::get_mysql_config(); my ($mysqld) = grep { $_->{'name'} eq 'mysqld' } @$conf; my $dir; if ($mysqld) { $dir = &mysql::find_value("datadir", $mysqld->{'members'}); } $dir ||= $mysql::config{'mysql_data'}; return undef if (!-d $dir); local $escdb = $db; $escdb =~ s/-/\@002d/g; if (-d "$mysql::config{'mysql_data'}/$escdb") { return "$mysql::config{'mysql_data'}/$escdb"; } else { return "$mysql::config{'mysql_data'}/$db"; } } # get_mysql_hosts(&domain, [always-from-template]) # Returns the allowed MySQL hosts for some domain, to be used when creating. # Uses hosts the user has currently by default, or those from the template. # If always-from-template == 1, then hosts already granted will never be used. # Instead, those from the template will be used. # If always-from-template == 2, then template hosts will be used AND we will # assume that we're connecting to a remote system. sub get_mysql_hosts { local ($d, $always) = @_; &require_mysql(); local @hosts; if (!$always) { @hosts = &get_mysql_allowed_hosts($d); } if (!@hosts) { # Fall back to those from template local $tmpl = &get_template($d->{'template'}); @hosts = $tmpl->{'mysql_hosts'} eq "none" ? ( ) : split(/\s+/, &substitute_domain_template( $tmpl->{'mysql_hosts'}, $d)); @hosts = ( 'localhost' ) if (!@hosts); if ($always == 2 || $mysql::config{'host'} && $mysql::config{'host'} ne 'localhost') { # Add this host too, as we are talking to a remote server push(@hosts, &get_system_hostname()); local $myip = &to_ipaddress(&get_system_hostname()); push(@hosts, $myip) if ($myip); } } return &unique(@hosts); } # list_mysql_database_users(&domain, db) # Returns a list of MySQL users and passwords who can access some database sub list_mysql_database_users { local ($d, $db) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Fetch from provisioning server my $info = { 'host' => $mysql::config{'host'}, 'database' => $db }; my ($ok, $msg) = &provision_api_call( "list-provision-mysql-users", $info, 1); &error(&text('user_emysqllist', $msg)) if (!$ok); my @rv; foreach my $u (@$msg) { push(@rv, [ $u->{'name'}, $u->{'values'}->{'pass'} ]); } return @rv; } else { # Query local MySQL server local $qdb = "e_mysql_database($db); local $d; eval { # Try old password column first local $main::error_must_die = 1; $d = &mysql::execute_sql($mysql::master_db, "select user.user,user.password from user,db where db.user = user.user and (db.db = '$db' or db.db = '$qdb')"); }; if ($@ || @{$d->{'data'}} && $d->{'data'}->[0]->[1] eq '') { # Try new mysql user table format if the password query failed, # or if the password was empty eval { local $main::error_must_die = 1; $d = &mysql::execute_sql($mysql::master_db, "select user.user,user.authentication_string from user,db where db.user = user.user and (db.db = '$db' or db.db = '$qdb')"); }; } local (@rv, %done); foreach my $u (@{$d->{'data'}}) { push(@rv, $u) if (!$done{$u->[0]}++); } return @rv; } } # check_mysql_user_clash(&domain, username) # Returns 1 if some user exists on the MySQL server sub check_mysql_user_clash { local ($d, $user) = @_; &require_mysql(); return 1 if ($user eq 'root'); # Never available if ($d->{'provision_mysql'}) { # Query provisioning server my ($ok, $msg) = &provision_api_call( "check-mysql-login", { 'user' => $user }); &error(&text('provision_emysqlcheck', $msg)) if (!$ok); return $msg =~ /host=/ ? 1 : 0; } else { # Check locally local $rv = &mysql::execute_sql($mysql::master_db, "select user from user where user = ?", $user); return @{$rv->{'data'}} ? 1 : 0; } } # create_mysql_database_user(&domain, &dbs, username, password, [mysql-pass]) # Adds one mysql user, who can access multiple databases sub create_mysql_database_user { local ($d, $dbs, $user, $pass, $encpass) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Create on provisioning server my $info = { 'user' => $user }; if ($encpass) { $info->{'encpass'} = $encpass; } else { $info->{'pass'} = $pass; } local @hosts = map { &to_ipaddress($_) } &get_mysql_hosts($d, 2); $info->{'remote'} = \@hosts; $info->{'database'} = $dbs; my $conns = &get_mysql_user_connections($d, 1); $info->{'conns'} = $conns if ($conns); my ($ok, $msg) = &provision_api_call( "provision-mysql-login", $info, 0); if (!$ok) { &error(&text('setup_emysqluser_provision', $msg)); } } else { # Create locally local $myuser = &mysql_username($user); local @hosts = &get_mysql_hosts($d); local $qpass = &mysql_escape($pass); local $h; local $cfunc = sub { foreach $h (@hosts) { &mysql::execute_sql_logged($mysql::master_db, "delete from user where host = '$h' ". "and user = '$user'"); &execute_user_creation_sql($h, $myuser, $encpass ? "'$encpass'" : "$password_func('$qpass')"); local $db; foreach $db (@$dbs) { &add_db_table($h, $db, $myuser); } &set_mysql_user_connections($d, $h, $myuser, 1); } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($cfunc); } } # delete_mysql_database_user(&domain, username) # Removes one database user and his access to all databases sub delete_mysql_database_user { local ($d, $user) = @_; &require_mysql(); local $myuser = &mysql_username($user); if ($d->{'provision_mysql'}) { # Delete on provisioning server my $info = { 'user' => $myuser, 'host' => $mysql::config{'host'} }; my ($ok, $msg) = &provision_api_call( "unprovision-mysql-login", $info, 0); &error(&text('user_emysqldelete', $msg)) if (!$ok); } else { # Delete locally local $dfunc = sub { &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = ?", $myuser); &mysql::execute_sql_logged($mysql::master_db, "delete from db where user = ?", $myuser); &mysql::execute_sql_logged($mysql::master_db, "flush privileges"); }; &execute_for_all_mysql_servers($dfunc); } } # modify_mysql_database_user(&domain, &olddbs, &dbs, oldusername, username, # [password], [encrypted-password]) # Renames or changes the password for a database user, and his list of allowed # mysql databases sub modify_mysql_database_user { local ($d, $olddbs, $dbs, $olduser, $user, $pass, $encpass) = @_; &require_mysql(); local $myuser = &mysql_username($user); local $myolduser = &mysql_username($olduser); if ($d->{'provision_mysql'}) { # Update on provisioning server my $info = { 'user' => $myolduser, 'host' => $mysql::config{'host'} }; if ($olduser ne $user) { $info->{'new-user'} = $myuser; } if ($encpass) { $info->{'encpass'} = $encpass; } elsif (defined($pass)) { $info->{'pass'} = $pass; } if (join(" ", @$dbs) ne join(" ", @$olddbs)) { $info->{'database'} = join("\0", @$dbs); } if (keys %$info > 1) { my ($ok, $msg) = &provision_api_call( "modify-mysql-login", $info, 0); &error(&text('user_emysqlprov', $msg)) if (!$ok); } } else { # Update locally local $mfunc = sub { if ($olduser ne $user) { # Change the username &mysql::execute_sql_logged($mysql::master_db, "update user set user = ? where user = ?", $myuser, $myolduser); &mysql::execute_sql_logged($mysql::master_db, "update db set user = ? where user = ?", $myuser, $myolduser); } if (defined($pass)) { # Change the password if ($encpass) { &execute_password_change_sql($myuser, "'$encpass'"); } else { local $qpass = &mysql_escape($pass); &execute_password_change_sql($myuser, "$password_func('$qpass')"); } } if (join(" ", @$dbs) ne join(" ", @$olddbs)) { # Update accessible database list local @hosts = &get_mysql_hosts($d); &mysql::execute_sql_logged($mysql::master_db, "delete from db where user = ?", $myuser); local $h; foreach $h (@hosts) { local $db; foreach $db (@$dbs) { &add_db_table($h, $db, $myuser); } } } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($mfunc); } } # list_mysql_tables(database) # Returns a list of tables in the given database sub list_mysql_tables { &require_mysql(); return &mysql::list_tables($_[0], 1); } # get_database_host_mysql() # Returns the hostname of the server on which MySQL is actually running sub get_database_host_mysql { &require_mysql(); return $mysql::config{'host'} || 'localhost'; } # sysinfo_mysql() # Returns the MySQL version sub sysinfo_mysql { &require_mysql(); return ( ) if ($config{'provision_mysql'}); local $ver = &mysql::get_mysql_version(); return ( [ $text{'sysinfo_mysql'}, $ver ] ); } sub startstop_mysql { local ($typestatus) = @_; &require_mysql(); return ( ) if ($config{'provision_mysql'} || !&mysql::is_mysql_local()); # cannot stop/start remote local $r = defined($typestatus->{'mysql'}) ? $typestatus->{'mysql'} == 1 : &mysql::is_mysql_running(); local @links = ( { 'link' => '/mysql/', 'desc' => $text{'index_mymanage'}, 'manage' => 1 } ); if ($r == 1) { return ( { 'status' => 1, 'name' => $text{'index_myname'}, 'desc' => $text{'index_mystop'}, 'restartdesc' => $text{'index_myrestart'}, 'longdesc' => $text{'index_mystopdesc'}, 'links' => \@links } ); } elsif ($r == 0) { return ( { 'status' => 0, 'name' => $text{'index_myname'}, 'desc' => $text{'index_mystart'}, 'longdesc' => $text{'index_mystartdesc'}, 'links' => \@links } ); } else { return ( ); } } sub stop_service_mysql { &require_mysql(); return &mysql::stop_mysql(); } sub start_service_mysql { &require_mysql(); return &mysql::start_mysql(); } # quote_mysql_database(name) # Returns a mysql database name with % and _ characters escaped sub quote_mysql_database { local ($db) = @_; $db =~ s/_/\\_/g; $db =~ s/%/\\%/g; return $db; } # show_template_mysql(&tmpl) # Outputs HTML for editing MySQL related template options sub show_template_mysql { local ($tmpl) = @_; &require_mysql(); # Default database name template print &ui_table_row(&hlink($text{'tmpl_mysql'}, "template_mysql"), &none_def_input("mysql", $tmpl->{'mysql'}, $text{'tmpl_mysqlpat'}, 1, 0, undef, [ "mysql" ]). &ui_textbox("mysql", $tmpl->{'mysql'}, 20)); # Enforced suffix for database names print &ui_table_row(&hlink($text{'tmpl_mysql_suffix'}, "template_mysql_suffix"), &none_def_input("mysql_suffix", $tmpl->{'mysql_suffix'}, $text{'tmpl_mysqlpat'}, 0, 0, undef, [ "mysql_suffix" ]). &ui_textbox("mysql_suffix", $tmpl->{'mysql_suffix'} eq "none" ? undef : $tmpl->{'mysql_suffix'}, 20)); # Additional host wildcards to add # Deprecated, so only show if already set if ($tmpl->{'mysql_wild'}) { print &ui_table_row(&hlink($text{'tmpl_mysql_wild'}, "template_mysql_wild"), &none_def_input("mysql_wild", $tmpl->{'mysql_wild'}, $text{'tmpl_mysqlpat'}, 1, 0, undef, [ "mysql_wild" ]). &ui_textbox("mysql_wild", $tmpl->{'mysql_wild'}, 20)); } # Additonal allowed hosts print &ui_table_row(&hlink($text{'tmpl_mysql_hosts'}, "template_mysql_hosts"), &none_def_input("mysql_hosts", $tmpl->{'mysql_hosts'}, $text{'tmpl_mysqlh'}, 0, 0, undef, [ "mysql_hosts" ]). &ui_textbox("mysql_hosts", $tmpl->{'mysql_hosts'} eq "none" ? "" : $tmpl->{'mysql_hosts'}, 40)); # Create DB at virtual server creation? print &ui_table_row(&hlink($text{'tmpl_mysql_mkdb'}, "template_mysql_mkdb"), &ui_radio("mysql_mkdb", $tmpl->{'mysql_mkdb'}, [ [ 1, $text{'yes'} ], [ 0, $text{'no'} ], ($tmpl->{'default'} ? ( ) : ( [ "", $text{'default'} ] ) )])); # Update MySQL username to match domain? print &ui_table_row(&hlink($text{'tmpl_mysql_nouser'}, "template_mysql_nouser"), &ui_radio("mysql_nouser", $tmpl->{'mysql_nouser'}, [ [ 0, $text{'yes'} ], [ 1, $text{'no'} ], ($tmpl->{'default'} ? ( ) : ( [ "", $text{'default'} ] ) )])); # Update MySQL password to match domain? if (!$tmpl->{'hashpass'}) { print &ui_table_row(&hlink($text{'tmpl_mysql_nopass2'}, "template_mysql_nopass"), &ui_radio("mysql_nopass", $tmpl->{'mysql_nopass'}, [ [ 0, $text{'tmpl_mysql_nopass_sync'} ], [ 1, $text{'tmpl_mysql_nopass_same'} ], [ 2, $text{'tmpl_mysql_nopass_random'} ], ($tmpl->{'default'} ? ( ) : ( [ "", $text{'default'} ] ) )])); } # Make MySQL DBs group-owned by domain, for quotas? if (-d $mysql::config{'mysql_data'} && !$config{'provision_mysql'}) { print &ui_table_row(&hlink($text{'tmpl_mysql_chgrp'}, "template_mysql_chgrp"), &ui_radio("mysql_chgrp", $tmpl->{'mysql_chgrp'}, [ [ 1, $text{'yes'} ], [ 0, $text{'no'} ], ($tmpl->{'default'} ? ( ) : ( [ "", $text{'default'} ] ) )])); } if ($mysql::mysql_version >= 4.1 && $config{'mysql'}) { # Default MySQL character set print &ui_table_row(&hlink($text{'tmpl_mysql_charset'}, "template_mysql_charset"), &ui_select("mysql_charset", $tmpl->{'mysql_charset'}, [ $tmpl->{'default'} ? ( ) : ( [ "", "<$text{'tmpl_mysql_charsetdef'}>" ] ), [ "none", "<$text{'tmpl_mysql_charsetnone'}>" ], map { [ $_->[0], $_->[0]." (".$_->[1].")" ] } &list_mysql_character_sets() ])); } if ($mysql::mysql_version >= 5 && $config{'mysql'}) { # Default MySQL collation order print &ui_table_row(&hlink($text{'tmpl_mysql_collate'}, "template_mysql_collate"), &ui_select("mysql_collate", $tmpl->{'mysql_collate'}, [ $tmpl->{'default'} ? ( ) : ( [ "", "<$text{'tmpl_mysql_charsetdef'}>" ] ), [ "none", "<$text{'tmpl_mysql_charsetnone'}>" ], map { $_->[0] } &list_mysql_collation_orders() ])); } # Max DB connections for domain owner my $c = $tmpl->{'mysql_conns'}; $c = "" if ($c eq "none"); print &ui_table_row(&hlink($text{'tmpl_mysql_conns'}, "template_mysql_conns"), &none_def_input("mysql_conns", $tmpl->{'mysql_conns'}, $text{'tmpl_mysql_maxconns'}, 0, 0, $text{'tmpl_mysql_unlimited'}). &ui_textbox("mysql_conns", $c, 5)); # Max DB connections for mailbox users my $uc = $tmpl->{'mysql_uconns'}; $uc = "" if ($uc eq "none"); print &ui_table_row(&hlink($text{'tmpl_mysql_uconns'}, "template_mysql_uconns"), &none_def_input("mysql_uconns", $tmpl->{'mysql_uconns'}, $text{'tmpl_mysql_maxconns'}, 0, 0, $text{'tmpl_mysql_unlimited'}). &ui_textbox("mysql_uconns", $uc, 5)); } # parse_template_mysql(&tmpl) # Updates MySQL related template options from %in sub parse_template_mysql { local ($tmpl) = @_; &require_mysql(); # Save MySQL-related settings if ($in{'mysql_mode'} == 1) { $tmpl->{'mysql'} = undef; } else { $in{'mysql'} =~ /^\S+$/ || &error($text{'tmpl_emysql'}); $tmpl->{'mysql'} = $in{'mysql'}; } if (defined($in{'mysql_wild_mode'})) { if ($in{'mysql_wild_mode'} == 1) { $tmpl->{'mysql_wild'} = undef; } else { $in{'mysql_wild'} =~ /^\S*$/ || &error($text{'tmpl_emysql_wild'}); $tmpl->{'mysql_wild'} = $in{'mysql_wild'}; } } if ($in{'mysql_hosts_mode'} == 0) { $tmpl->{'mysql_hosts'} = "none"; } elsif ($in{'mysql_hosts_mode'} == 1) { $tmpl->{'mysql_hosts'} = undef; } else { $in{'mysql_hosts'} =~ /\S/ || &error($text{'tmpl_emysql_hosts'}); $tmpl->{'mysql_hosts'} = $in{'mysql_hosts'}; } if ($in{'mysql_suffix_mode'} == 0) { $tmpl->{'mysql_suffix'} = "none"; } elsif ($in{'mysql_suffix_mode'} == 1) { $tmpl->{'mysql_suffix'} = undef; } else { $in{'mysql_suffix'} =~ /\S/ || &error($text{'tmpl_emysql_suffix'}); $tmpl->{'mysql_suffix'} = $in{'mysql_suffix'}; } $tmpl->{'mysql_mkdb'} = $in{'mysql_mkdb'}; if (!$tmpl->{'hashpass'}) { $tmpl->{'mysql_nopass'} = $in{'mysql_nopass'}; } $tmpl->{'mysql_nouser'} = $in{'mysql_nouser'}; if (-d $mysql::config{'mysql_data'} && !$config{'provision_mysql'}) { $tmpl->{'mysql_chgrp'} = $in{'mysql_chgrp'}; } if ($mysql::mysql_version >= 4.1 && $config{'mysql'}) { $tmpl->{'mysql_charset'} = $in{'mysql_charset'}; $tmpl->{'mysql_collate'} = $in{'mysql_collate'}; } $in{'mysql_conns_mode'} < 2 || $in{'mysql_conns'} =~ /^[1-9]\d*$/ || &error($text{'tmpl_emysql_conns'}); $tmpl->{'mysql_conns'} = &parse_none_def("mysql_conns"); $in{'mysql_uconns_mode'} < 2 || $in{'mysql_uconns'} =~ /^[1-9]\d*$/ || &error($text{'tmpl_emysql_conns'}); $tmpl->{'mysql_uconns'} = &parse_none_def("mysql_uconns"); } # creation_form_mysql(&domain) # Returns options for a new mysql database sub creation_form_mysql { &require_mysql(); local $rv; if ($mysql::mysql_version >= 4.1) { local $tmpl = &get_template($_[0]->{'template'}); # Character set local @charsets = &list_mysql_character_sets(); local $cs = $tmpl->{'mysql_charset'}; $cs = "" if ($cs eq "none"); $rv .= &ui_table_row($text{'database_charset'}, &ui_select("mysql_charset", $cs, [ [ undef, "<$text{'default'}>" ], map { [ $_->[0], $_->[0]." (".$_->[1].")" ] } @charsets ])); # Collation order local $cl = $tmpl->{'mysql_collate'}; $cl = "" if ($cs eq "none"); local @colls = &list_mysql_collation_orders(); if (@colls) { local %csmap = map { $_->[0], $_->[1] } @charsets; $rv .= &ui_table_row($text{'database_collate'}, &ui_select("mysql_collate", $cl, [ [ undef, "<$text{'default'}>" ], map { [ $_->[0], $_->[0]." (".$csmap{$_->[1]}.")" ] } @colls ])); } } return $rv; } # creation_parse_mysql(&domain, &in) # Parse the form generated by creation_form_mysql, and return a structure # for passing to create_mysql_database sub creation_parse_mysql { local ($d, $in) = @_; local $opts = { 'charset' => $in->{'mysql_charset'}, 'collate' => $in->{'mysql_collate'} }; return $opts; } # get_mysql_allowed_hosts(&domain) # Returns a list of hostnames or IP addresses from which a domain's user is # allowed to connect to MySQL. sub get_mysql_allowed_hosts { local ($d) = @_; &require_mysql(); if ($d->{'provision_mysql'}) { # Query provisioning server my $info = { 'host' => $mysql::config{'host'}, 'user' => &mysql_user($d) }; my ($ok, $msg) = &provision_api_call( "list-provision-mysql-users", $info, 1); &error(&text('user_emysqllist', $msg)) if (!$ok); return split(/\s+/, $msg->[0]->{'values'}->{'hosts'}->[0]); } else { # Get from local DB local $data = &mysql::execute_sql($mysql::master_db, "select distinct host from user where user = ?", &mysql_user($d)); return map { $_->[0] } @{$data->{'data'}}; } } # save_mysql_allowed_hosts(&domain, &hosts) # Sets the list of hosts from which this domain's MySQL user can connect. # Returns undef on success, or an error message on failure. sub save_mysql_allowed_hosts { local ($d, $hosts) = @_; &require_mysql(); local $user = &mysql_user($d); if ($d->{'provision_mysql'}) { # Call the remote API my $info = { 'user' => $user, 'host' => $mysql::config{'host'}, 'remote' => $hosts }; my ($ok, $msg) = &provision_api_call("modify-mysql-login", $info, 0); return &text('user_emysqlprovips', $msg) if (!$ok); } else { # Update MySQL permissions locally local @dbs = &domain_databases($d, [ 'mysql' ]); foreach my $sd (&get_domain_by("parent", $d->{'id'})) { push(@dbs, &domain_databases($sd, [ 'mysql' ])); } local $ufunc = sub { # Update the user table entry for the main user local $encpass = &encrypted_mysql_pass($d); &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = '$user'"); &mysql::execute_sql_logged($mysql::master_db, "delete from db where user = '$user'"); foreach my $h (@$hosts) { &execute_user_creation_sql($h, $user, $encpass); foreach my $db (@dbs) { &add_db_table($h, $db->{'name'}, $user); } &set_mysql_user_connections($d, $h, $user, 0); } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($ufunc); # Add db table entries for all users, and user table entries # for mailboxes local $ufunc = sub { my %allusers; foreach my $db (@dbs) { foreach my $u (&list_mysql_database_users( $d, $db->{'name'})) { # Re-populate db table for this db and user next if ($u->[0] eq $user || $u->[0] eq 'root' || $u->[0] eq $mysql::config{'login'}); &mysql::execute_sql_logged($mysql::master_db, "delete from db where user = ? and ". "db = ?", $u->[0], $db->{'name'}); foreach my $h (@$hosts) { &add_db_table($h, $db->{'name'}, $u->[0]); } $allusers{$u->[0]} = $u; } } # Re-populate user table foreach my $u (values %allusers) { &mysql::execute_sql_logged($mysql::master_db, "delete from user where user = ?", $u->[0]); foreach my $h (@$hosts) { &execute_user_creation_sql($h, $u->[0], "'$u->[1]'"); &set_mysql_user_connections($d, $h, $u->[0], 1); } } &mysql::execute_sql_logged($mysql::master_db, 'flush privileges'); }; &execute_for_all_mysql_servers($ufunc); } return undef; } # has_mysql_quotas() # Returns 1 if the filesystem for user quotas includes the MySQL data dir. # Will never be true when using external quota programs. sub has_mysql_quotas { &require_mysql(); return &has_home_quotas() && $mysql::config{'mysql_data'} && $config{'home_quotas'} && &is_under_directory($config{'home_quotas'}, $mysql::config{'mysql_data'}); } # encrypted_mysql_pass(&domain) # Returns the encrypted MySQL password for a domain, suitable for use in SQL. # This can either be a quoted string like 'xxxyyyzzz', or a function call # like password('smeg') sub encrypted_mysql_pass { local ($d) = @_; if ($d->{'mysql_enc_pass'}) { return "'$d->{'mysql_enc_pass'}'"; } else { local $qpass = &mysql_escape(&mysql_pass($d)); return "$password_func('$qpass')"; } } # check_mysql_login(dbname, dbuser, dbpass) # Tries to login to MySQL with the given credentials, returning undef on failure sub check_mysql_login { local ($dbname, $dbuser, $dbpass) = @_; &require_mysql(); local $main::error_must_die = 1; local $mysql::mysql_login = $dbuser; local $mysql::mysql_pass = $dbpass; eval { &mysql::execute_sql($dbname, "show tables") }; local $err = $@; if ($err) { $err =~ s/\s+at\s+.*\sline//g; return $err; } return undef; } # execute_for_all_mysql_servers(code) # Calls some code multiple times, once for each MySQL server on which users # need to be created or managed. sub execute_for_all_mysql_servers { local ($code) = @_; &require_mysql(); local @repls = split(/\s+/, $config{'mysql_replicas'}); if (!@repls) { # Just do for this system &$code; } else { # Call for this system and all replicas local $thishost = $mysql::config{'host'}; local %done; foreach my $host ($thishost, @repls) { local $ip = &to_ipaddress($host); next if ($ip && $done{$ip}++); $mysql::config{'host'} = $host; &$code; } $mysql::config{'host'} = $thishost; } } # list_mysql_collation_orders() # Returns a list of supported collation orders. Each row is an array ref of # a code and character set it can work with. sub list_mysql_collation_orders { &require_mysql(); local @rv; if ($config{'provision_mysql'}) { if ($mysql::config{'host'}) { # Query provisioning DB system local $d = &mysql::execute_sql( "information_schema", "show collation"); @rv = map { [ $_->[0], $_->[1] ] } @{$d->{'data'}}; } else { # No MySQL host yet @rv = ( ); } } else { # Query local DB if ($mysql::mysql_version >= 5) { local $d = &mysql::execute_sql( $mysql::master_db, "show collation"); @rv = map { [ $_->[0], $_->[1] ] } @{$d->{'data'}}; } } return sort { lc($a->[0]) cmp lc($b->[0]) } @rv; } # list_mysql_character_sets() # Returns a list of supported character sets. Each row is an array ref of # a code and character set name sub list_mysql_character_sets { &require_mysql(); if ($config{'provision_mysql'}) { if ($mysql::config{'host'}) { # Query provisioning DB system return &mysql::list_character_sets("information_schema"); } else { # No MySQL host yet return ( ); } } else { # Query local DB return &mysql::list_character_sets(); } } # validate_database_name_mysql(&domain, name) # Checks if a MySQL database name is valid sub validate_database_name_mysql { local ($d, $dbname) = @_; $dbname =~ /^[a-z0-9\_\-]+$/i || return $text{'database_ename'}; local $maxlen; if ($d->{'provision_mysql'}) { # Just assume that the DB name max is 64 chars $maxlen = 64; } else { # Get the DB name max from the mysql.db table &require_mysql(); local @str = &mysql::table_structure($mysql::master_db, "db"); local ($dbcol) = grep { lc($_->{'field'}) eq 'db' } @str; $maxlen = $dbcol && $dbcol->{'type'} =~ /\((\d+)\)/ ? $1 : 64; } length($dbname) <= $maxlen || return &text('database_enamelen', $maxlen); return undef; } # default_mysql_creation_opts(&domain) # Returns default options for a new MySQL DB in some domain sub default_mysql_creation_opts { local ($d) = @_; local $tmpl = &get_template($d->{'template'}); local %opts; if ($tmpl->{'mysql_charset'} && $tmpl->{'mysql_charset'} ne 'none') { $opts{'charset'} = $tmpl->{'mysql_charset'}; } if ($tmpl->{'mysql_collate'} && $tmpl->{'mysql_collate'} ne 'none') { $opts{'collate'} = $tmpl->{'mysql_collate'}; } return \%opts; } # get_mysql_creation_opts(&domain, db) # Returns a hash ref of database creation options for an existing DB sub get_mysql_creation_opts { local ($d, $dbname) = @_; &require_mysql(); local $data = &mysql::execute_sql($dbname, "show create database ". &mysql::quotestr($dbname)); local $sql = $data->{'data'}->[0]->[1]; local $opts = { }; if ($sql =~ /CHARACTER\s+SET\s+(\S+)/i) { $opts->{'charset'} = $1; } if ($sql =~ /COLLATE\s+(\S+)/i) { $opts->{'collate'} = $1; } return $opts; } # list_all_mysql_databases([&domain]) # Returns the names of all known MySQL databases sub list_all_mysql_databases { local ($d) = @_; local $prov = $d ? $d->{'provision_mysql'} : $config{'provision_mysql'}; &require_mysql(); if ($prov) { # From provisioning server local $info = { 'feature' => 'mysqldb' }; my ($ok, $msg) = &provision_api_call( "list-provision-history", $info, 1); if (!$ok) { &error($msg); } return map { $_->{'values'}->{'mysql_database'}->[0] } @$msg; } else { # Local list return &mysql::list_databases(); } } # set_mysql_user_connections(&domain, hostname, username, is-mailbox) # Sets the max connections for a user if defined in the template sub set_mysql_user_connections { local ($d, $host, $user, $mailbox) = @_; local $conns = &get_mysql_user_connections($d, $mailbox); if ($conns) { &mysql::execute_sql_logged($mysql::master_db, "update user set max_user_connections = ? ". "where user = ? and host = ?", $conns, $user, $host); } } # get_mysql_user_connections(&domain, is-mailbox) # Returns the max connections to MySQL from a template sub get_mysql_user_connections { local ($d, $mailbox) = @_; local $tmpl = &get_template($d->{'template'}); local $conns = $tmpl->{$mailbox ? 'mysql_uconns' : 'mysql_conns'}; $conns = undef if ($conns eq "none"); return $conns; } # list_mysql_size_settings("small"|"medium"|"large"|"huge") # Returns an array of tupes for MySQL my.cnf settings for some size # diff my-large.cnf my-huge.cnf | grep ">" | grep -v "#" | grep = | perl -ne 'print "[ \"$1\", \"$2\" ],\n" if (/(\S+)\s*=\s*(\S+)/)' sub list_mysql_size_settings { local ($size) = @_; &require_mysql(); my $myver = &mysql::get_mysql_version(); my $cachedir = &compare_versions($myver, "5.1.3") > 0 ? "table_open_cache" : "table_cache"; my $tc = &compare_versions($myver, "5.7") < 0; if ($size eq "small") { return ([ "key_buffer_size", "16K" ], [ "max_allowed_packet", "1M" ], [ $cachedir, "4" ], [ "sort_buffer_size", "64K" ], [ "read_buffer_size", "256K" ], [ "read_rnd_buffer_size", "256K" ], [ "net_buffer_length", "2K" ], [ "myisam_sort_buffer_size", undef ], [ "thread_stack", "128K" ], [ "thread_cache_size", undef ], [ "query_cache_size", undef ], [ "thread_concurrency", undef ], [ "key_buffer_size", "8M", "isamchk" ], [ "sort_buffer_size", "8M", "isamchk" ], [ "read_buffer", undef, "isamchk" ], [ "write_buffer", undef, "isamchk" ], [ "key_buffer_size", "8M", "myisamchk" ], [ "sort_buffer_size", "8M", "myisamchk" ], [ "read_buffer", undef, "myisamchk" ], [ "write_buffer", undef, "myisamchk" ]); } elsif ($size eq "medium") { return ([ "key_buffer_size", "16M" ], [ "max_allowed_packet", "1M" ], [ $cachedir, "64" ], [ "sort_buffer_size", "512K" ], [ "read_buffer_size", "256K" ], [ "net_buffer_length", "8K" ], [ "read_rnd_buffer_size", "512K" ], [ "myisam_sort_buffer_size", "8M" ], [ "thread_stack", undef ], [ "thread_cache_size", undef ], [ "query_cache_size", undef ], [ "thread_concurrency", undef ], [ "key_buffer_size", "20M", "isamchk" ], [ "sort_buffer_size", "20M", "isamchk" ], [ "read_buffer", "2M", "isamchk" ], [ "write_buffer", "2M", "isamchk" ], [ "key_buffer_size", "20M", "myisamchk" ], [ "sort_buffer_size", "20M", "myisamchk" ], [ "read_buffer", "2M", "myisamchk" ], [ "write_buffer", "2M", "myisamchk" ]); } elsif ($size eq "large") { return ([ "key_buffer_size", "256M" ], [ "max_allowed_packet", "1M" ], [ $cachedir, "256" ], [ "sort_buffer_size", "1M" ], [ "read_buffer_size", "1M" ], [ "net_buffer_length", undef ], [ "read_rnd_buffer_size", "4M" ], [ "myisam_sort_buffer_size", "64M" ], [ "thread_stack", undef ], [ "thread_cache_size", "8" ], [ "query_cache_size", "16M" ], [ "thread_concurrency", $tc ? "8" : undef ], [ "key_buffer_size", "128M", "isamchk" ], [ "sort_buffer_size", "128M", "isamchk" ], [ "read_buffer", "2M", "isamchk" ], [ "write_buffer", "2M", "isamchk" ], [ "key_buffer_size", "128M", "myisamchk" ], [ "sort_buffer_size", "128M", "myisamchk" ], [ "read_buffer", "2M", "myisamchk" ], [ "write_buffer", "2M", "myisamchk" ]); } elsif ($size eq "huge") { return ([ "key_buffer_size", "384M" ], [ "max_allowed_packet", "1M" ], [ $cachedir, "512" ], [ "sort_buffer_size", "2M" ], [ "read_buffer_size", "2M" ], [ "net_buffer_length", undef ], [ "read_rnd_buffer_size", "8M" ], [ "myisam_sort_buffer_size", "64M" ], [ "thread_stack", undef ], [ "thread_cache_size", "8" ], [ "query_cache_size", "32M" ], [ "thread_concurrency", $tc ? "8" : undef ], [ "key_buffer_size", "256M", "isamchk" ], [ "sort_buffer_size", "256M", "isamchk" ], [ "read_buffer", "2M", "isamchk" ], [ "write_buffer", "2M", "isamchk" ], [ "key_buffer_size", "256M", "myisamchk" ], [ "sort_buffer_size", "256M", "myisamchk" ], [ "read_buffer", "2M", "myisamchk" ], [ "write_buffer", "2M", "myisamchk" ]); } return ( ); } # execute_user_creation_sql(host, user, password-sql) # Create a MySQL user and set his password sub execute_user_creation_sql { my ($host, $user, $encpass) = @_; foreach my $sql (&get_user_creation_sql($host, $user,$encpass)) { &mysql::execute_sql_logged($mysql::master_db, $sql); if ($sql =~ /flush\s+privileges/) { sleep(1); } } } # get_user_creation_sql(host, user, password-sql) # Returns SQL to add a user, with SSL fields if needed sub get_user_creation_sql { my ($host, $user, $encpass) = @_; if (&compare_versions($mysql::mysql_version, "5.7.6") >= 0) { return ("insert into user (host, user, ssl_type, ssl_cipher, x509_issuer, x509_subject, plugin, authentication_string) values ('$host', '$user', '', '', '', '', 'mysql_native_password', $encpass)"); } elsif (&compare_versions($mysql::mysql_version, 5) >= 0) { return ("insert into user (host, user, ssl_type, ssl_cipher, x509_issuer, x509_subject) values ('$host', '$user', '', '', '', '')", "flush privileges", "set password for '$user'\@'$host' = $encpass"); } else { return ("insert into user (host, user, password) values ('$host', '$user', $encpass)"); } } # execute_password_change_sql(user, pass-str) # Update a MySQL user's password for all hosts sub execute_password_change_sql { my ($user, $encpass) = @_; my $d = &mysql::execute_sql($mysql::master_db, "select host from user where user = ?", $user); foreach my $host (&unique(map { $_->[0] } @{$d->{'data'}})) { my $sql; if (&compare_versions($mysql::mysql_version, "5.7.6") >= 0) { $sql = "update user set authentication_string = $encpass ". "where user = '$user' and host = '$host'"; } else { $sql = "set password for '$user'\@'$host' = $encpass"; } &mysql::execute_sql_logged($mysql::master_db, $sql); } &mysql::execute_sql_logged($mysql::master_db, "flush privileges"); } # mysql_password_synced(&domain) # Returns 1 if a domain's MySQL password will change along with its admin pass sub mysql_password_synced { my ($d) = @_; if ($d->{'parent'}) { my $parent = &get_domain($d->{'parent'}); return &mysql_password_synced($parent); } if ($d->{'hashpass'}) { # Hashed passwords are being used return 0; } if ($d->{'mysql_pass'}) { # Separate password set return 0; } my $tmpl = &get_template($d->{'template'}); if ($tmpl->{'mysql_nopass'}) { # Syncing disabled in the template return 0; } return 1; } # remote_mysql(&domain) # Returns 1 if the domain's MySQL DB is on a remote system sub remote_mysql { local ($d) = @_; &require_mysql(); return $mysql::config{'host'}; } $done_feature_script{'mysql'} = 1; 1;