modules/adminapi/common/common.cc (1,782 lines of code) (raw):

/* * Copyright (c) 2016, 2024, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * 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, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "modules/adminapi/common/common.h" #include <algorithm> #include <cstdlib> #include <iterator> #include <map> #include <set> #include <string> #include <utility> #include "modules/adminapi/common/cluster_types.h" #include "modules/adminapi/common/dba_errors.h" #include "modules/adminapi/common/metadata_storage.h" #include "modules/adminapi/common/server_features.h" #include "modules/adminapi/common/sql.h" #include "modules/adminapi/common/validations.h" #include "modules/adminapi/mod_dba.h" #include "mysqlshdk/include/shellcore/console.h" #include "mysqlshdk/include/shellcore/interrupt_handler.h" #include "mysqlshdk/include/shellcore/shell_options.h" #include "mysqlshdk/libs/config/config_file_handler.h" #include "mysqlshdk/libs/config/config_server_handler.h" #include "mysqlshdk/libs/db/connection_options.h" #include "mysqlshdk/libs/db/mysql/session.h" #include "mysqlshdk/libs/db/utils_error.h" #include "mysqlshdk/libs/mysql/clone.h" #include "mysqlshdk/libs/mysql/group_replication.h" #include "mysqlshdk/libs/mysql/gtid_utils.h" #include "mysqlshdk/libs/mysql/replication.h" #include "mysqlshdk/libs/mysql/script.h" #include "mysqlshdk/libs/mysql/user_privileges.h" #include "mysqlshdk/libs/textui/progress.h" #include "mysqlshdk/libs/textui/textui.h" #include "mysqlshdk/libs/utils/debug.h" #include "mysqlshdk/libs/utils/utils_file.h" #include "mysqlshdk/libs/utils/utils_general.h" #include "mysqlshdk/libs/utils/utils_lexing.h" #include "mysqlshdk/libs/utils/utils_net.h" #include "mysqlshdk/libs/utils/utils_sqlstring.h" #include "mysqlshdk/libs/utils/utils_string.h" #include "mysqlshdk/libs/utils/version.h" #include "mysqlshdk/shellcore/shell_console.h" namespace mysqlsh { namespace dba { namespace { constexpr const char kSandboxDatadir[] = "sandboxdata"; constexpr uint16_t k_max_port = 65535; /** * Validates the session used to manage the group. * * Checks if the given session is valid for use with AdminAPI. * * @param instance A group session to validate. * * @throws shcore::Exception::runtime_error if session is not valid. */ void validate_gr_session( const std::shared_ptr<mysqlshdk::db::ISession> &group_session) { // TODO(alfredo) - this check seems too extreme, this only matters in the // target instance (and only for some operations) and this function can get // called on other members if (mysqlshdk::gr::is_group_replication_delayed_starting( mysqlsh::dba::Instance(group_session))) throw shcore::Exception::runtime_error( "Cannot perform operation while group replication is " "starting up"); } } // namespace std::string to_string(Cluster_ssl_mode ssl_mode) { switch (ssl_mode) { case Cluster_ssl_mode::AUTO: return kClusterSSLModeAuto; case Cluster_ssl_mode::DISABLED: return kClusterSSLModeDisabled; case Cluster_ssl_mode::REQUIRED: return kClusterSSLModeRequired; case Cluster_ssl_mode::VERIFY_CA: return kClusterSSLModeVerifyCA; case Cluster_ssl_mode::VERIFY_IDENTITY: return kClusterSSLModeVerifyIdentity; case Cluster_ssl_mode::NONE: return "NONE"; } return {}; } Cluster_ssl_mode to_cluster_ssl_mode(std::string_view mode) { if (shcore::str_caseeq(kClusterSSLModeAuto, mode)) return Cluster_ssl_mode::AUTO; if (shcore::str_caseeq(kClusterSSLModeDisabled, mode)) return Cluster_ssl_mode::DISABLED; if (shcore::str_caseeq(kClusterSSLModeRequired, mode)) return Cluster_ssl_mode::REQUIRED; if (shcore::str_caseeq(kClusterSSLModeVerifyCA, mode)) return Cluster_ssl_mode::VERIFY_CA; if (shcore::str_caseeq(kClusterSSLModeVerifyIdentity, mode)) return Cluster_ssl_mode::VERIFY_IDENTITY; throw std::runtime_error( shcore::str_format("Unsupported Cluster SSL-mode: %.*s", static_cast<int>(mode.size()), mode.data())); } std::string to_string(Replication_auth_type auth) { switch (auth) { case Replication_auth_type::PASSWORD: return kReplicationMemberAuthPassword; case Replication_auth_type::CERT_ISSUER: return kReplicationMemberAuthCertIssuer; case Replication_auth_type::CERT_SUBJECT: return kReplicationMemberAuthCertSubject; case Replication_auth_type::CERT_ISSUER_PASSWORD: return kReplicationMemberAuthCertIssuerPassword; case Replication_auth_type::CERT_SUBJECT_PASSWORD: return kReplicationMemberAuthCertSubjectPassword; } return {}; } Replication_auth_type to_replication_auth_type(std::string_view auth) { if (shcore::str_caseeq(kReplicationMemberAuthPassword, auth)) return Replication_auth_type::PASSWORD; if (shcore::str_caseeq(kReplicationMemberAuthCertIssuer, auth)) return Replication_auth_type::CERT_ISSUER; if (shcore::str_caseeq(kReplicationMemberAuthCertSubject, auth)) return Replication_auth_type::CERT_SUBJECT; if (shcore::str_caseeq(kReplicationMemberAuthCertIssuerPassword, auth)) return Replication_auth_type::CERT_ISSUER_PASSWORD; if (shcore::str_caseeq(kReplicationMemberAuthCertSubjectPassword, auth)) return Replication_auth_type::CERT_SUBJECT_PASSWORD; throw std::runtime_error( shcore::str_format("Unsupported replication member authentication: %.*s", static_cast<int>(auth.size()), auth.data())); } std::string get_mysqlprovision_error_string( const shcore::Value::Array_type_ref &errors) { if (!errors) return "mysqlprovision error"; std::vector<std::string> str_errors; if (errors) { for (auto error : *errors) { auto data = error.as_map(); auto error_type = data->get_string("type"); auto error_text = data->get_string("msg"); str_errors.push_back(error_type + ": " + error_text); } } else { str_errors.push_back( "Unexpected error calling mysqlprovision, " "see the shell log for more details"); } return shcore::str_join(str_errors, "\n"); } bool is_option_supported( const mysqlshdk::utils::Version &version, const std::string &option, const std::map<std::string, Option_availability> &options_map) { assert(options_map.find(option) != options_map.end()); auto &opt_avail = options_map.at(option); // if no version was supplied, options can be used regardless of version if (opt_avail.support_in_80.get_major() == 0 && opt_avail.support_in_57.get_major() == 0) return true; if (version.get_major() == 8) { // 8.0 server // if only the 5.7 version was provided to the Option_availability struct, // then assume variable will also be supported in 8.0. This check is enough // since the default Version constructor sets the major version to 0. return version >= opt_avail.support_in_80; } if (version.get_major() == 5 && version.get_minor() == 7) { // 5.7 server if (opt_avail.support_in_57.get_major() == 0) { // if the default constructor for version was used, no 5.7 version was // used on the Option_availability struct, meaning 5.7 is not supported return false; } return version >= opt_avail.support_in_57; } throw std::runtime_error( "Unexpected version found for option support check: '" + version.get_full() + "'."); } void validate_replication_filters(const mysqlshdk::mysql::IInstance &instance, Cluster_type cluster_type) { // SHOW MASTER STATUS includes info about binlog filters, which filter // events from being written to the binlog, at the master auto result = instance.query("SHOW MASTER STATUS"); while (auto row = result->fetch_one()) { if (!row->get_string(2, "").empty() || !row->get_string(3, "").empty()) throw shcore::Exception( instance.descr() + ": instance has binlog filters configured, but they are " "not supported in " + to_display_string(cluster_type, Display_form::THINGS_FULL) + ".", SHERR_DBA_INVALID_SERVER_CONFIGURATION); } // replication_applier_filters and replication_applier_global_filters // have info about replication filters, which filters what's applied from // the relay log at a slave if (instance.get_version() >= mysqlshdk::utils::Version(8, 0, 11)) { bool has_global_filter = false; bool has_filter = false; try { if (instance.queryf_one_int( 0, 0, "SELECT count(*) " "FROM performance_schema.replication_applier_global_filters " "WHERE filter_rule <> ''") > 0) { has_global_filter = true; } if (instance.queryf_one_int(0, 0, "SELECT count(*) FROM " "performance_schema.replication_applier_" "filters WHERE filter_rule <> ''") > 0) { has_filter = true; } } catch (std::exception &e) { log_warning("Error querying for replication filters at %s: %s", instance.descr().c_str(), e.what()); } if (has_global_filter) { throw shcore::Exception( instance.descr() + ": instance has global replication filters " "configured, but they are not supported in " + to_display_string(cluster_type, Display_form::THINGS_FULL) + ".", SHERR_DBA_INVALID_SERVER_CONFIGURATION); } if (has_filter) { throw shcore::Exception( instance.descr() + ": instance has replication filters configured, but they are " "not supported in " + to_display_string(cluster_type, Display_form::THINGS_FULL) + ".", SHERR_DBA_INVALID_SERVER_CONFIGURATION); } } } void resolve_ssl_mode_option(const std::string &option, const std::string &context, const mysqlshdk::mysql::IInstance &instance, Cluster_ssl_mode *ssl_mode) { bool require_secure_transport = instance.get_sysvar_bool("require_secure_transport", false); bool have_ssl = instance.is_ssl_enabled(); // sslMode is DISABLED but instance requires SSL if (*ssl_mode == Cluster_ssl_mode::DISABLED && require_secure_transport) { throw shcore::Exception::argument_error( "The instance '" + instance.descr() + "' requires secure connections, to create the " + context + " either turn off require_secure_transport or use the " + option + " option with 'REQUIRED', 'VERIFY_CA' or " "'VERIFY_IDENTITY' value."); } // sslMode is REQUIRED, VERIFY_CA or VERIFY_IDENTITY if (!have_ssl && (*ssl_mode == Cluster_ssl_mode::REQUIRED || *ssl_mode == Cluster_ssl_mode::VERIFY_CA || *ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY)) { throw shcore::Exception::argument_error( "The instance '" + instance.descr() + "' does not have TLS enabled, to create the " + context + " either use an instance with TLS enabled, remove the " + option + " option or use it with any of 'AUTO' or 'DISABLED'."); } // sslMode is VERIFY_CA or VERIFY_IDENTITY, verify the certificates if (*ssl_mode == Cluster_ssl_mode::VERIFY_CA || *ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY) { ensure_certificates_set(instance, *ssl_mode); } if (*ssl_mode == Cluster_ssl_mode::NONE || *ssl_mode == Cluster_ssl_mode::AUTO) { if (have_ssl) { *ssl_mode = Cluster_ssl_mode::REQUIRED; } else { current_console()->print_note("TLS not available at '" + instance.descr() + "', assuming " + option + " to be DISABLED"); *ssl_mode = Cluster_ssl_mode::DISABLED; } } } /** * Determines the value to use on the instance for the SSL mode * configured for the cluster, based on: * - The instance having SSL available * - The instance having require_secure_transport ON * - The cluster SSL configuration * - The memberSslMode option given by the user * * Assigned to: * REQUIRED if SSL is supported and member_ssl_mode is either AUTO or * REQUIRED, and the cluster uses SSL * VERIFY_CA if SSL is supported and member_ssl_mode is VERIFY_CA on the * cluster * VERIFY_IDENTITY if SSL is supported and member_ssl_mode is VERIFY_CA on * the cluster * DISABLED if SSL is not supported and member_ssl_mode is either unspecified * DISABLED or AUTO and the cluster has SSL disabled. * * Error: * - Cluster has SSL enabled and member_ssl_mode is DISABLED or not specified * - Cluster has SSL enabled and SSL is not supported on the instance * - Cluster has SSL disabled and member_ssl_mode is REQUIRED, VERIFY_CA or * VERIFY_IDENTITY * - Cluster has SSL disabled and the instance has require_secure_transport ON */ void resolve_instance_ssl_mode_option( const mysqlshdk::mysql::IInstance &instance, const mysqlshdk::mysql::IInstance &pinstance, Cluster_ssl_mode *member_ssl_mode) { assert(member_ssl_mode); Cluster_ssl_mode gr_ssl_mode = to_cluster_ssl_mode( pinstance.get_sysvar_string("group_replication_ssl_mode", "")); // check for conflicts between the (deprecated) per-instance memberSslMode // and the group setting switch (gr_ssl_mode) { case Cluster_ssl_mode::REQUIRED: case Cluster_ssl_mode::VERIFY_CA: case Cluster_ssl_mode::VERIFY_IDENTITY: // memberSslMode is DISABLED if (*member_ssl_mode == Cluster_ssl_mode::DISABLED) { throw shcore::Exception::runtime_error( "The cluster has TLS (encryption) enabled. " "To add the instance '" + instance.descr() + "' to the cluster either disable TLS on the cluster, remove the " "memberSslMode option or use it with any of 'AUTO', 'REQUIRED', " "'VERIFY_CA' or 'VERIFY_IDENTITY'."); } // addInstance() can have a different memberSslMode from the cluster, // although that's deprecated if (*member_ssl_mode == Cluster_ssl_mode::AUTO || *member_ssl_mode == Cluster_ssl_mode::NONE) *member_ssl_mode = gr_ssl_mode; break; case Cluster_ssl_mode::NONE: case Cluster_ssl_mode::DISABLED: if (*member_ssl_mode == Cluster_ssl_mode::REQUIRED || *member_ssl_mode == Cluster_ssl_mode::VERIFY_CA || *member_ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY) { throw shcore::Exception::runtime_error( "The cluster has TLS (encryption) disabled. " "To add the instance '" + instance.descr() + "' to the cluster either " "enable TLS on the cluster, remove the memberSslMode option or " "use it with any of 'AUTO' or 'DISABLED'."); } *member_ssl_mode = gr_ssl_mode; break; case Cluster_ssl_mode::AUTO: // not possible assert(0); *member_ssl_mode = gr_ssl_mode; break; } validate_instance_ssl_mode(Cluster_type::GROUP_REPLICATION, instance, *member_ssl_mode); } void validate_instance_ssl_mode(Cluster_type type, const mysqlshdk::mysql::IInstance &instance, Cluster_ssl_mode ssl_mode) { assert(ssl_mode != Cluster_ssl_mode::AUTO); bool have_ssl = instance.is_ssl_enabled(); // The cluster REQUIRES SSL if (ssl_mode == Cluster_ssl_mode::REQUIRED || ssl_mode == Cluster_ssl_mode::VERIFY_CA || ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY) { // Now checks if SSL is actually supported on the instance // SSL is not supported on the instance if (!have_ssl) { throw shcore::Exception::runtime_error( "Instance '" + instance.descr() + "' does not support TLS and cannot join " + to_display_string(type, Display_form::A_THING) + " with TLS (encryption) enabled. Enable TLS support on the instance " "and try again, otherwise it can only be added to " + to_display_string(type, Display_form::A_THING) + " with TLS disabled."); } else { if (ssl_mode == Cluster_ssl_mode::VERIFY_CA || ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY) { // memberSslMode is VERIFY_CA or VERIFY_IDENTITY // verify the certificates ensure_certificates_set(instance, ssl_mode); } } // The cluster has SSL DISABLED } else if (ssl_mode == Cluster_ssl_mode::DISABLED) { bool require_secure_transport = instance.get_sysvar_bool("require_secure_transport", false); // If the instance session requires SSL connections, then it can's // Join a cluster with SSL disabled if (require_secure_transport) { throw shcore::Exception::runtime_error( "The instance '" + instance.descr() + "' is configured to require a secure transport but the " + to_display_string(type, Display_form::THING) + " has TLS disabled. To add the instance to the " + to_display_string(type, Display_form::THING) + ", either turn OFF the require_secure_transport option on the " "instance or enable TLS on the " + to_display_string(type, Display_form::THING) + "."); } } } void validate_instance_member_auth_type( const mysqlshdk::mysql::IInstance &instance, bool is_replica_cluster, Cluster_ssl_mode ssl_mode, std::string_view ssl_mode_option, Replication_auth_type auth_type) { if (auth_type == Replication_auth_type::PASSWORD) return; if (ssl_mode == Cluster_ssl_mode::DISABLED) { if (is_replica_cluster) throw shcore::Exception::runtime_error(shcore::str_format( "Instance '%s' has the \"%.*s\" option with the value " "'DISABLED', which isn't compatible with the \"memberAuthType\" " "value of '%s' inherited from the ClusterSet.", instance.descr().c_str(), static_cast<int>(ssl_mode_option.length()), ssl_mode_option.data(), to_string(auth_type).c_str())); throw shcore::Exception::runtime_error(shcore::str_format( "Instance '%s' has the \"%.*s\" option with the value " "'DISABLED', which means that \"memberAuthType\" only accepts the " "value 'PASSWORD'.", instance.descr().c_str(), static_cast<int>(ssl_mode_option.length()), ssl_mode_option.data())); } if (!instance.is_ssl_enabled()) { std::string_view cluster_set_ctx{ is_replica_cluster ? " (inherited from the ClusterSet)" : ""}; throw shcore::Exception::runtime_error(shcore::str_format( "Instance '%s' does not support TLS, which means that " "\"memberAuthType\"%.*s with value '%s' isn't supported.", instance.descr().c_str(), static_cast<int>(cluster_set_ctx.length()), cluster_set_ctx.data(), to_string(auth_type).c_str())); } // verify the certificates ensure_certificates_set(instance, auth_type); } void validate_instance_member_auth_options(std::string_view context, bool is_replica_cluster, Replication_auth_type auth_type, std::string_view cert_subject) { std::string_view cluster_set_ctx{ is_replica_cluster ? " (inherited from the ClusterSet)" : ""}; switch (auth_type) { case Replication_auth_type::PASSWORD: case Replication_auth_type::CERT_ISSUER: case Replication_auth_type::CERT_ISSUER_PASSWORD: if (!cert_subject.empty()) mysqlsh::current_console()->print_note(shcore::str_format( "The %.*s's SSL mode%.*s is set to %s, which makes the " "'certSubject' option not required. The option will be ignored.", static_cast<int>(context.length()), context.data(), static_cast<int>(cluster_set_ctx.length()), cluster_set_ctx.data(), to_string(auth_type).c_str())); break; case Replication_auth_type::CERT_SUBJECT: case Replication_auth_type::CERT_SUBJECT_PASSWORD: if (cert_subject.empty()) throw shcore::Exception( shcore::str_format( "The %.*s's SSL mode%.*s is set to %s but the 'certSubject' " "option for the instance wasn't supplied.", static_cast<int>(context.length()), context.data(), static_cast<int>(cluster_set_ctx.length()), cluster_set_ctx.data(), to_string(auth_type).c_str()), SHERR_DBA_MISSING_CERT_OPTION); break; default: break; } } void validate_instance_member_auth_options(std::string_view context, Replication_auth_type auth_type, std::string_view cert_issuer, std::string_view cert_subject) { if (auth_type == Replication_auth_type::PASSWORD) { if (cert_issuer.empty() && cert_subject.empty()) return; mysqlsh::current_console()->print_note(shcore::str_format( "The %.*s's SSL mode is set to %s, which makes both the 'certIssuer' " "and 'certSubject' options not required. Both options will be ignored.", static_cast<int>(context.length()), context.data(), to_string(auth_type).c_str())); return; } switch (auth_type) { case Replication_auth_type::CERT_ISSUER: case Replication_auth_type::CERT_ISSUER_PASSWORD: case Replication_auth_type::CERT_SUBJECT: case Replication_auth_type::CERT_SUBJECT_PASSWORD: if (cert_issuer.empty()) throw shcore::Exception( shcore::str_format("The %.*s's SSL mode is set to %s but the " "'certIssuer' option wasn't supplied.", static_cast<int>(context.length()), context.data(), to_string(auth_type).c_str()), SHERR_DBA_MISSING_CERT_OPTION); break; default: break; } switch (auth_type) { case Replication_auth_type::CERT_ISSUER: case Replication_auth_type::CERT_ISSUER_PASSWORD: if (!cert_subject.empty()) mysqlsh::current_console()->print_note(shcore::str_format( "The %.*s's SSL mode is set to %s, which makes the 'certSubject' " "option not required. The option will be ignored.", static_cast<int>(context.length()), context.data(), to_string(auth_type).c_str())); break; case Replication_auth_type::CERT_SUBJECT: case Replication_auth_type::CERT_SUBJECT_PASSWORD: if (cert_subject.empty()) throw shcore::Exception( shcore::str_format( "The %.*s's SSL mode is set to %s but the 'certSubject' option " "for the instance wasn't supplied.", static_cast<int>(context.length()), context.data(), to_string(auth_type).c_str()), SHERR_DBA_MISSING_CERT_OPTION); break; default: break; } } /** * Gets the list of instances belonging to the GR group which are not * registered on the metadata for a specific ReplicaSet. * * @param metadata metadata object which represents the session to the metadata * storage * @param cluster_id the Cluster id * @return a vector of NewInstanceInfo with the list of instances found * as belonging to the GR group but not registered on the Metadata */ std::vector<NewInstanceInfo> get_newly_discovered_instances( const mysqlshdk::mysql::IInstance &group_server, const std::shared_ptr<MetadataStorage> &metadata, Cluster_id cluster_id) { std::vector<mysqlshdk::gr::Member> instances_gr( mysqlshdk::gr::get_members(group_server)); std::vector<Instance_metadata> instances_md_array = metadata->get_all_instances(cluster_id); // Check if the instances_gr list has more members than the instances_md lists // Meaning that an instance was added to the GR group outside of the AdminAPI std::vector<NewInstanceInfo> ret; for (const auto &i : instances_gr) { if (std::find_if(instances_md_array.begin(), instances_md_array.end(), [&i](const Instance_metadata &md) { return md.uuid == i.uuid; }) == instances_md_array.end()) { NewInstanceInfo info; info.member_id = i.uuid; info.host = i.host; info.port = i.port; info.version = i.version; ret.push_back(info); } } return ret; } /** * Gets the list of instances registered on the Metadata, for a specific * ReplicaSet, which do not belong in the GR group. * * @param metadata metadata object which represents the session to the metadata * storage * @param cluster_id the Cluster id * @return a vector of MissingInstanceInfo with the list of instances found * as registered on the Metadata but not present in the GR group. */ std::vector<MissingInstanceInfo> get_unavailable_instances( const mysqlshdk::mysql::IInstance &group_server, const std::shared_ptr<MetadataStorage> &metadata, Cluster_id cluster_id) { std::vector<std::string> instances_gr_array, instances_md_array; for (const mysqlshdk::gr::Member &m : mysqlshdk::gr::get_members(group_server)) instances_gr_array.push_back(m.uuid); std::vector<Instance_metadata> all_instances( metadata->get_all_instances(cluster_id)); for (const auto &i : all_instances) instances_md_array.push_back(i.uuid); // Check the differences between the two lists std::vector<std::string> removed_members; // Sort the arrays in order to be able to use // std::set_difference() std::sort(instances_gr_array.begin(), instances_gr_array.end()); std::sort(instances_md_array.begin(), instances_md_array.end()); // Check if the instances_md list has more members than the instances_gr // lists. Meaning that an instance was removed from the GR group outside // of the AdminAPI std::set_difference(instances_md_array.begin(), instances_md_array.end(), instances_gr_array.begin(), instances_gr_array.end(), std::inserter(removed_members, removed_members.begin())); std::vector<MissingInstanceInfo> ret; for (auto &i : removed_members) { for (const auto &m : all_instances) { if (m.uuid == i) { MissingInstanceInfo info; info.id = m.uuid; info.label = m.label; info.endpoint = m.endpoint; ret.push_back(info); break; } } } return ret; } void parse_fully_qualified_cluster_name(const std::string &name, std::string *out_domain, std::string *out_partition, std::string *out_cluster_group) { // TODO(pjesus): Replace current code by the commented one, when support // for names with a domain part is really supported. *out_domain = ""; *out_cluster_group = name; // size_t p = name.find("::"); // if (p != std::string::npos) { // *out_domain = name.substr(0, p); // *out_cluster_group = name.substr(p + 2); // } else { // *out_domain = ""; // *out_cluster_group = name; // } if (out_partition) *out_partition = ""; // not supported yet // p = out_domain->find("/"); // if (p != std::string::npos) { // *out_partition = out_domain->substr(p + 1); // *out_domain = out_domain->substr(0, p); // } } /** * Validate the given Cluster name. * * A fully qualified cluster name has the following form: * [domain[/partition]::]clusterGroup * * Each component must be: * non-empty and no greater than 63 characters long and start with an alphabetic * or '_' character and just contain alphanumeric or the characters '_', '-' or * '.'. * * @param name of the cluster * @param cluster_type indicates what the name is intended for * @throws shcore::Exception if the name is not valid. */ void SHCORE_PUBLIC validate_cluster_name(const std::string &name, Cluster_type cluster_type) { std::string what; switch (cluster_type) { case Cluster_type::GROUP_REPLICATION: what = "Cluster"; break; case Cluster_type::ASYNC_REPLICATION: what = "ReplicaSet"; break; case Cluster_type::REPLICATED_CLUSTER: what = "ClusterSet"; break; case Cluster_type::NONE: throw shcore::Exception::logic_error("Unknown Cluster_type."); } if (!name.empty()) { std::string domain; std::string partition; std::string group; parse_fully_qualified_cluster_name(name, &domain, &partition, &group); if (domain.length() > k_cluster_name_max_length) { throw shcore::Exception::argument_error( domain + ": The " + what + " domain name can not be greater than 63 characters."); } if (partition.length() > k_cluster_name_max_length) { throw shcore::Exception::argument_error( partition + ": The " + what + " domain partition name can not be greater than 63 " "characters."); } if (group.length() > k_cluster_name_max_length) { throw shcore::Exception::argument_error( group + ": The " + what + " name can not be greater than 63 characters."); } if (!domain.empty()) { if (mysqlshdk::utils::span_keyword( domain, 0, k_cluster_name_allowed_chars) != domain.length()) { throw shcore::Exception::argument_error( "" + what + " name may only contain alphanumeric characters, '_', '-', or '.' " "and may not start with a number (" + domain + ")"); } } if (!partition.empty()) { if (mysqlshdk::utils::span_keyword(partition, 0, k_cluster_name_allowed_chars) != partition.length()) { throw shcore::Exception::argument_error( "" + what + " name may only contain alphanumeric characters, '_', '-', or '.' " "and may not start with a number (" + partition + ")"); } } if (!group.empty()) { if (mysqlshdk::utils::span_keyword( group, 0, k_cluster_name_allowed_chars) != group.length()) { throw shcore::Exception::argument_error( "" + what + " name may only contain alphanumeric characters, '_', '-', or '.' " "and may not start with a number (" + group + ")"); } } else { throw shcore::Exception::argument_error("The " + what + " name cannot be empty."); } } else { throw shcore::Exception::argument_error("The " + what + " name cannot be empty."); } } /** * Validate the given label name. * Cluster name must be non-empty and no greater than 256 characters long and * start with an alphanumeric or '_' character and can only contain * alphanumerics, Period ".", Underscore "_", Hyphen "-" or Colon ":" * characters. * @param name of the cluster * @throws shcore::Exception if the name is not valid. */ void SHCORE_PUBLIC validate_label(const std::string &label) { if (!label.empty()) { bool valid = false; if (label.length() > 256) { throw shcore::Exception::argument_error( "The label can not be greater than 256 characters."); } std::locale locale; valid = std::isalnum(label[0], locale) || label[0] == '_'; if (!valid) { throw shcore::Exception::argument_error( "The label can only start with an alphanumeric or the '_' " "character."); } size_t index = 1; while (valid && index < label.size()) { valid = std::isalnum(label[index], locale) || label[index] == '_' || label[index] == '.' || label[index] == '-' || label[index] == ':'; if (!valid) { std::string msg = "The label can only contain alphanumerics or the '_', '.', '-', " "':' characters. Invalid character '"; msg.append(&(label.at(index)), 1); msg.append("' found."); throw shcore::Exception::argument_error(msg); } index++; } } else { throw shcore::Exception::argument_error("The label can not be empty."); } return; } namespace { /* * Gets Group Replication Group Name variable: group_replication_group_name * * @param session object which represents the session to the instance * @return string with the value of @@group_replication_group_name */ std::string get_gr_replicaset_group_name( const mysqlshdk::mysql::IInstance &instance) { std::string group_name; std::string query("SELECT @@group_replication_group_name"); // Any error will bubble up right away auto result = instance.query(query); auto row = result->fetch_one(); if (row) { group_name = row->get_as_string(0); } return group_name; } } // namespace /** * Validates if the current session instance 'group_replication_group_name' * differs from the one registered in the corresponding Cluster table of the * Metadata * * @param session session to instance that is supposed to belong to a group * @param md_group_name name of the group the member belongs to according to * metadata * @return a boolean value indicating whether the replicaSet * 'group_replication_group_name' is the same as the one registered in the * corresponding replicaset in the Metadata */ bool validate_cluster_group_name(const mysqlshdk::mysql::IInstance &instance, const std::string &md_group_name) { std::string gr_group_name = get_gr_replicaset_group_name(instance); log_info("Group Replication 'group_name' value: %s", gr_group_name.c_str()); log_info("Metadata 'group_name' value: %s", md_group_name.c_str()); // If the value is NULL it means the instance does not belong to any GR group // so it can be considered valid if (gr_group_name == "NULL") return true; return (gr_group_name == md_group_name); } /* * Check for the value of super-read-only and any open sessions * to the instance and raise an exception if super_read_only is turned on. * * @param instance * @param clear_read_only if true, will clear SRO if false, throws error * * @returns true if super_read_only was disabled */ bool validate_super_read_only(const mysqlshdk::mysql::IInstance &instance, std::optional<bool> clear_read_only) { if (auto super_read_only = instance.get_sysvar_bool("super_read_only", false); !super_read_only) return false; auto console = mysqlsh::current_console(); const std::string error_message = "The MySQL instance at '" + instance.descr() + "' currently has the super_read_only system variable set to " "protect it from inadvertent updates from applications.\nYou must " "first unset it to be able to perform any changes to this " "instance.\n" "For more information see: https://dev.mysql.com/doc/refman/en/" "server-system-variables.html#sysvar_super_read_only."; bool super_read_only_changed = false; if (clear_read_only.value_or(true)) { log_info("Disabling super_read_only on the instance '%s'", instance.descr().c_str()); instance.set_sysvar("super_read_only", false); super_read_only_changed = true; } else { console->print_error(error_message); throw shcore::Exception::runtime_error("Server in SUPER_READ_ONLY mode"); } return super_read_only_changed; } /* * Checks if an instance can be rejoined to a given Cluster. * * @param instance * @param metadata Metadata object which represents the session to the metadata * storage * @param cluster_id The Cluster id * @param out_uuid_mistmatch Boolean value that will be set to TRUE if the UUID * changed for the given instance * * @return Indication whether instance can be rejoined and reason if not. * * It is considered that the UUID changed for an instance if the instance is NOT * found using the current UUID but it is found using it's canonical address. */ Instance_rejoinability validate_instance_rejoinable( const mysqlshdk::mysql::IInstance &instance, const std::shared_ptr<MetadataStorage> &metadata, Cluster_id cluster_id, bool *out_uuid_mistmatch) { std::string instance_uuid = instance.get_uuid(); // Ensure that: // 1 - instance is part of the given cluster // 2 - instance is not ONLINE or RECOVERING Instance_metadata imd; bool uuid_in_md = false; try { imd = metadata->get_instance_by_uuid(instance.get_uuid()); uuid_in_md = true; } catch (const shcore::Exception &e) { if (e.code() == SHERR_DBA_MEMBER_METADATA_MISSING) { try { imd = metadata->get_instance_by_address(instance.get_canonical_address()); } catch (const shcore::Exception &ex) { if (e.code() == SHERR_DBA_MEMBER_METADATA_MISSING) { return Instance_rejoinability::NOT_MEMBER; } throw; } } else { throw; } } // If the instance was not found by UUID then it means the UUID changed if (out_uuid_mistmatch) *out_uuid_mistmatch = !uuid_in_md; if (imd.cluster_id != cluster_id) return Instance_rejoinability::NOT_MEMBER; auto state = mysqlshdk::gr::get_member_state(instance); if (state == mysqlshdk::gr::Member_state::ONLINE) return Instance_rejoinability::ONLINE; else if (state == mysqlshdk::gr::Member_state::RECOVERING) return Instance_rejoinability::RECOVERING; else return Instance_rejoinability::REJOINABLE; } /** * Checks if the target instance is a sandbox * * @param instance Object which represents the target instance * @param optional string to store the value of the cnfPath in case the instance * is a sandbox * * @return a boolean value which is true if the target instance is a sandbox or * false if is not. */ bool is_sandbox(const mysqlshdk::mysql::IInstance &instance, std::string *cnfPath) { int port = 0; std::string datadir; mysqlsh::dba::get_port_and_datadir(instance, &port, &datadir); std::string path_separator = datadir.substr(datadir.size() - 1); auto path_elements = shcore::split_string(datadir, path_separator); // Removes the empty element at the end if (!datadir.empty() && (datadir[datadir.size() - 1] == '/' || datadir[datadir.size() - 1] == '\\')) { datadir.pop_back(); } // Sandbox deployment structure is: <root_path>/<port>/sandboxdata // So we validate such structure to determine it is a sandbox if (datadir.length() < strlen(kSandboxDatadir) || datadir.compare(datadir.length() - strlen(kSandboxDatadir), strlen(kSandboxDatadir), kSandboxDatadir) != 0) { // Not a sandbox, we can immediately return false return false; } else { // Check if the my.cnf file is present if (path_elements[path_elements.size() - 3] == std::to_string(port)) { path_elements[path_elements.size() - 2] = "my.cnf"; std::string tmpPath = shcore::str_join(path_elements, path_separator); // Remove the trailing path_separator if (tmpPath.back() == path_separator[0]) tmpPath.pop_back(); if (shcore::is_file(tmpPath)) { if (cnfPath) { *cnfPath = tmpPath; } return true; } else { return false; } } else { return false; } } } // AdminAPI interactive handling specific methods /* * Resolves the .cnf file path * * @param instance Instance object which represents the target instance * * @return the target instance resolved .cnf file path */ std::string prompt_cnf_path(const mysqlshdk::mysql::IInstance &instance) { // Path is not given, let's try to autodetect it std::string cnfPath; // Try to locate the .cnf file path // based on the OS and the default my.cnf path as configured // by our official MySQL packages // Detect the OS auto console = mysqlsh::current_console(); console->print_info(); console->print_info("Detecting the configuration file..."); std::vector<std::string> config_paths, default_paths; if (instance.get_version() >= mysqlshdk::utils::Version(8, 0, 0)) { // use the performance schema table to try and find the existing // configuration files auto result = instance.query( "select DISTINCT VARIABLE_PATH from performance_schema.variables_info " "WHERE VARIABLE_PATH <> ''"); auto row = result->fetch_one(); while (row) { config_paths.emplace_back(row->get_string(0)); row = result->fetch_one(); } } // if no config files were found, try to look in the default paths if (config_paths.empty()) { shcore::OperatingSystem os = shcore::get_os_type(); default_paths = mysqlshdk::config::get_default_config_paths(os); } // Iterate the config_paths found in the instance checking if the user wants // to modify any of them for (const auto &value : config_paths) { // Prompt the user to validate if shall use it or not console->print_info("Found configuration file being used by instance '" + instance.get_connection_options().uri_endpoint() + "' at location: " + value); if (console->confirm("Do you want to modify this file?") == Prompt_answer::YES) { cnfPath = value; break; } } if (cnfPath.empty()) { // Iterate the default_paths to check if the files exist and if so, // set cnfPath for (const auto &value : default_paths) { if (shcore::is_file(value)) { // Prompt the user to validate if shall use it or not console->print_info("Found configuration file at standard location: " + value); if (console->confirm("Do you want to modify this file?") == Prompt_answer::YES) { cnfPath = value; break; } } } } if (cnfPath.empty()) { console->print_info("Default file not found at the standard locations."); bool done = false; std::string tmpPath; while (!done && console->prompt("Please specify the path to the MySQL " "configuration file: ", &tmpPath) == shcore::Prompt_result::Ok) { if (tmpPath.empty()) { done = true; } else { if (shcore::is_file(tmpPath)) { cnfPath = tmpPath; done = true; } else { console->print_info( "The given path to the MySQL configuration file is invalid."); console->print_info(); } } } } return cnfPath; } /* * Generates a prompt with several options * * @param options a vector of strings containing the optional selections * @param defopt the default option * * @return an integer representing the chosen option */ int prompt_menu(const std::vector<std::string> &options, int defopt) { int i = 0; auto console = mysqlsh::current_console(); for (const auto &opt : options) { console->print_info(std::to_string(++i) + ") " + opt); } for (;;) { std::string result; if (defopt > 0) { console->print_info(); if (console->prompt( "Please select an option [" + std::to_string(defopt) + "]: ", &result) != shcore::Prompt_result::Ok) return 0; } else { console->print_info(); if (console->prompt("Please select an option: ", &result) != shcore::Prompt_result::Ok) return 0; } // Note that menu options start at 1, not 0 since that's what users will // input if (result.empty() && defopt > 0) return defopt; std::stringstream ss(result); ss >> i; if (i <= 0 || i > static_cast<int>(options.size())) continue; break; } return i; } /* * Prints a table with configurable contents * * @param column_names the names of the table columns * @param column_labels the labels for the columns * @param documents a Value::Array with the table contents */ void dump_table(const std::vector<std::string> &column_names, const std::vector<std::string> &column_labels, shcore::Value::Array_type_ref documents) { size_t field_count = column_names.size(); std::vector<size_t> max_lengths; max_lengths.reserve(field_count); // Updates the max_length array with the maximum length between column name, // min column length and column max length for (size_t field_index = 0; field_index < field_count; field_index++) { max_lengths.push_back(0); max_lengths[field_index] = std::max<size_t>( max_lengths[field_index], column_labels[field_index].size()); } // Now updates the length with the real column data lengths if (documents) { for (auto map : *documents) { auto document = map.as_map(); for (size_t field_index = 0; field_index < field_count; field_index++) max_lengths[field_index] = std::max<size_t>( max_lengths[field_index], document->get_string(column_names[field_index]).size()); } } //----------- std::vector<std::string> formats(field_count, "%-"); // Calculates the max column widths and constructs the separator line. std::string separator("+"); for (size_t index = 0; index < field_count; index++) { // Creates the format string to print each field formats[index].append(std::to_string(max_lengths[index])); if (index == field_count - 1) formats[index].append("s |"); else formats[index].append("s | "); separator.append(max_lengths[index] + 2, '-'); separator.append(1, '+'); } separator.append("\n"); // Prints the initial separator line and the column headers auto console = mysqlsh::current_console(); console->print(separator); console->print("| "); for (size_t index = 0; index < field_count; index++) { std::string data = shcore::str_format(formats[index].c_str(), column_labels[index].c_str()); console->print(data.c_str()); // Once the header is printed, updates the numeric fields formats // so they are right aligned // if (numerics[index]) // formats[index] = formats[index].replace(1, 1, ""); } console->print_info(); console->print(separator); if (documents) { // Now prints the records for (auto map : *documents) { auto document = map.as_map(); console->print("| "); for (size_t field_index = 0; field_index < field_count; field_index++) { std::string raw_value = document->get_string(column_names[field_index]); std::string data = shcore::str_format(formats[field_index].c_str(), raw_value.c_str()); console->print(data.c_str()); } console->print_info(); } } console->print_info(separator); } /* * Gets the configure instance action returned from MP * * @param opt_map map of the "config_errors" JSON dictionary retured by MP: * result->get_array("config_errors"); * * @return an enum object of ConfigureInstanceAction with the action to be taken */ ConfigureInstanceAction get_configure_instance_action( const shcore::Value::Map_type &opt_map) { std::string action = opt_map.get_string("action"); ConfigureInstanceAction ret_val; if (action == "server_update+config_update") ret_val = ConfigureInstanceAction::UPDATE_SERVER_AND_CONFIG_DYNAMIC; else if (action == "config_update+restart") ret_val = ConfigureInstanceAction::UPDATE_SERVER_AND_CONFIG_STATIC; else if (action == "config_update") ret_val = ConfigureInstanceAction::UPDATE_CONFIG; else if (action == "server_update") ret_val = ConfigureInstanceAction::UPDATE_SERVER_DYNAMIC; else if (action == "server_update+restart") ret_val = ConfigureInstanceAction::UPDATE_SERVER_STATIC; else if (action == "remove_opt+restart") ret_val = ConfigureInstanceAction::REMOVE_OPTION_RESTART; else if (action == "restart") ret_val = ConfigureInstanceAction::RESTART; else ret_val = ConfigureInstanceAction::UNDEFINED; return ret_val; } /* * Presents the required configuration changes and prompts the user for * confirmation * * @param result the return value of the MP operation * @param print_note a boolean value indicating if the column 'note' should be * presented or not */ void print_validation_results(const shcore::Value::Map_type_ref &result, bool print_note) { if (!result->has_key("config_errors")) return; auto console = mysqlsh::current_console(); console->print_note("Some configuration options need to be fixed:"); auto config_errors = result->get_array("config_errors"); if (print_note) { for (auto option : *config_errors) { auto opt_map = option.as_map(); ConfigureInstanceAction action = get_configure_instance_action(*opt_map); std::string note; switch (action) { case ConfigureInstanceAction::UPDATE_SERVER_AND_CONFIG_DYNAMIC: { note = "Update the server variable and the config file"; break; } case ConfigureInstanceAction::UPDATE_SERVER_AND_CONFIG_STATIC: { note = "Update the config file and restart the server"; break; } case ConfigureInstanceAction::UPDATE_CONFIG: { note = "Update the config file"; break; } case ConfigureInstanceAction::UPDATE_SERVER_DYNAMIC: { note = "Update the server variable"; break; } case ConfigureInstanceAction::UPDATE_SERVER_STATIC: { note = "Update read-only variable and restart the server"; break; } case ConfigureInstanceAction::REMOVE_OPTION_RESTART: { note = "Remove the option and restart the server"; break; } case ConfigureInstanceAction::RESTART: { note = "Restart the server"; break; } default: { note = "Undefined action"; break; } } (*opt_map)["note"] = shcore::Value(std::move(note)); } dump_table({"option", "current", "required", "note"}, {"Variable", "Current Value", "Required Value", "Note"}, config_errors); } else { dump_table({"option", "current", "required"}, {"Variable", "Current Value", "Required Value"}, config_errors); } for (auto option : *config_errors) { auto opt_map = option.as_map(); opt_map->erase("note"); } } std::string get_report_host_address( const mysqlshdk::db::Connection_options &cnx_opts, const mysqlshdk::db::Connection_options &group_cnx_opts) { // Set login credentials to connect to instance if needed. // NOTE: It is assumed that the same login credentials can be used to // connect to all cluster instances. auto target_cnx_opts = cnx_opts; if (!target_cnx_opts.has_user()) { target_cnx_opts.set_login_options_from(group_cnx_opts); } std::string instance_address = target_cnx_opts.as_uri(mysqlshdk::db::uri::formats::only_transport()); // Connect to instance to try to get report_host value. std::string md_address; log_debug("Connecting to instance '%s' to determine report host.", instance_address.c_str()); try { auto target_instance = mysqlsh::dba::Instance::connect_raw(target_cnx_opts); log_debug("Successfully connected to instance"); // Get the instance report host value. md_address = target_instance->get_canonical_address(); log_debug("Closed connection to the instance '%s'.", instance_address.c_str()); } catch (const std::exception &err) { log_debug("Failed to connect to instance '%s': %s", instance_address.c_str(), err.what()); } return (!md_address.empty()) ? md_address : instance_address; } std::unique_ptr<mysqlshdk::config::Config> create_server_config( mysqlshdk::mysql::IInstance *instance, const std::string &srv_cfg_handler_name, bool silent) { auto cfg = std::make_unique<mysqlshdk::config::Config>(); // Get the capabilities to use set persist by the server. std::optional<bool> can_set_persist = instance->is_set_persist_supported(); // Add server configuration handler depending on SET PERSIST support. cfg->add_handler( srv_cfg_handler_name, std::unique_ptr<mysqlshdk::config::IConfig_handler>( std::make_unique<mysqlshdk::config::Config_server_handler>( instance, can_set_persist.value_or(false) ? mysqlshdk::mysql::Var_qualifier::PERSIST : mysqlshdk::mysql::Var_qualifier::GLOBAL))); if (!silent) { if (!can_set_persist.has_value()) { auto console = mysqlsh::current_console(); std::string warn_msg = "Instance '" + instance->descr() + "' cannot persist Group Replication configuration since MySQL " "version " + instance->get_version().get_base() + " does not support the SET PERSIST command (MySQL version >= 8.0.11 " "required). Please use the dba.<<<configureLocalInstance>>>" "() command locally to persist the changes."; console->print_warning(warn_msg); } else if (*can_set_persist == false) { auto console = mysqlsh::current_console(); std::string warn_msg = "Instance '" + instance->descr() + "' will not load the persisted cluster configuration upon reboot " "since " "'persisted-globals-load' is set to 'OFF'. Please use the dba." "<<<configureLocalInstance>>>() command locally to persist the " "changes " "or set 'persisted-globals-load' to 'ON' on the configuration file."; console->print_warning(warn_msg); } } return cfg; } void add_config_file_handler(mysqlshdk::config::Config *cfg, const std::string &handler_name, const std::string &server_uuid, const std::string &mycnf_path, const std::string &output_mycnf_path) { // Add configuration handle to update option file (if provided). if (mycnf_path.empty() && output_mycnf_path.empty()) { throw std::logic_error("No option file path was provided."); } if (output_mycnf_path.empty()) { // Read and update mycnf. cfg->add_handler(handler_name, std::unique_ptr<mysqlshdk::config::IConfig_handler>( new mysqlshdk::config::Config_file_handler( server_uuid, mycnf_path, mycnf_path))); } else if (mycnf_path.empty()) { // Update output_mycnf (creating it if needed). cfg->add_handler(handler_name, std::unique_ptr<mysqlshdk::config::IConfig_handler>( new mysqlshdk::config::Config_file_handler( server_uuid, output_mycnf_path))); } else { // Read from mycnf but update output_mycnf (creating it if needed). cfg->add_handler(handler_name, std::unique_ptr<mysqlshdk::config::IConfig_handler>( new mysqlshdk::config::Config_file_handler( server_uuid, mycnf_path, output_mycnf_path))); } } std::string resolve_gr_local_address( const std::optional<std::string> &local_address, const std::optional<std::string> &communication_stack, const std::string &raw_report_host, int port, bool check_if_busy, bool quiet) { assert(!raw_report_host.empty()); // First we need to get the report host. bool generated = false; bool comm_stack_mysql = shcore::str_caseeq(communication_stack.value_or(""), kCommunicationStackMySQL); // if report host is an IPv6 address, surround it with [] // The [] are necessary for IPv6 because we later have to append a colon and // the port and we need to distinguish between colon in the Ip address and the // colon that separates the IP and the port. std::string report_host = raw_report_host; if (mysqlshdk::utils::Net::is_ipv6(raw_report_host)) { report_host = "[" + raw_report_host + "]"; } std::string local_host; int local_port; if (!local_address.has_value() || local_address->empty()) { // If communicationStack is set to 'mysql' local_address must default to the // network address on which MySQL is listening to: report_host:port if (comm_stack_mysql) { local_host = report_host; local_port = port; } else { // No local address specified, use instance host and port * 10 + 1. local_host = report_host; local_port = port * 10 + 1; generated = true; } if (!quiet) { auto console = mysqlsh::current_console(); console->print_note( "Group Replication will communicate with other members using '" + local_host + ":" + std::to_string(local_port) + "'. Use the localAddress option to override."); console->print_info(); } } else if (local_address->front() == '[' && local_address->back() == ']') { // It is an IPV6 address (no port specified), since it is surrounded by []. // NOTE: Must handle this situation first to avoid splitting IPv6 addresses // in the middle next. local_host = *local_address; local_port = port * 10 + 1; } else { // Parse the given address host:port (both parts are optional). // Note: Get the last ':' in case a IPv6 address is used, e.g. [::1]:123. size_t pos = local_address->find_last_of(":"); if (pos != std::string::npos) { // Local address with ':' separating host and port. local_host = local_address->substr(0, pos); std::string str_local_port = local_address->substr(pos + 1, local_address->length()); // No host part, use instance report host. if (local_host.empty()) { local_host = report_host; } // No port part, then use instance port * 10 +1. if (str_local_port.empty()) { local_port = port * 10 + 1; generated = true; } else { // Convert port string value to int try { local_port = std::stoi(str_local_port); } catch (const std::exception &) { // Error if the port cannot be converted to an integer (not valid). throw shcore::Exception::argument_error( "Invalid port '" + str_local_port + "' for localAddress option. The port must be an integer between " "1 and 65535."); } } } else { // No separator found ':'. // First, if the value only has digits assume it is a port. if (std::all_of(local_address->cbegin(), local_address->cend(), ::isdigit)) { local_port = std::stoi(*local_address); local_host = report_host; } else { // Otherwise, assume only the host part was provided. local_host = *local_address; local_port = port * 10 + 1; generated = true; } } } // Check the port value, if out of range then issue an error. if (local_port <= 0 || local_port > k_max_port) { std::string msg; if (generated) { msg = "Automatically generated port for localAddress falls out of valid " "range."; } else { msg = "Invalid port '" + std::to_string(local_port) + "' for localAddress option."; } msg += " The port must be an integer between 1 and 65535."; if (generated) { msg += " Please use the localAddress option to manually set a valid value."; } throw shcore::Exception::argument_error(msg); } // Verify if the port is already in use, skip if the communication protocol is // "MySQL" if (check_if_busy && !comm_stack_mysql) { bool port_busy = false; try { port_busy = mysqlshdk::utils::Net::is_port_listening(local_host, local_port); } catch (...) { // Ignore any error while checking if the port is busy, let GR try later // and possibly fail (e.g., if a wrong host is used). } if (port_busy) { throw shcore::Exception::runtime_error( "The port '" + std::to_string(local_port) + "' for localAddress option is already in use. Specify an " "available port to be used with localAddress option or free port '" + std::to_string(local_port) + "'."); } } // Return the final local address value to use for GR. return local_host + ":" + std::to_string(local_port); } std::vector<Instance_gtid_info> filter_primary_candidates( const mysqlshdk::mysql::IInstance &server, const std::vector<Instance_gtid_info> &gtid_info, const std::function<bool(const Instance_gtid_info &, const Instance_gtid_info &)> &on_conflit) { using mysqlshdk::mysql::Gtid_set_relation; if (gtid_info.empty()) return {}; std::vector<Instance_gtid_info> candidates; const Instance_gtid_info *freshest_instance = &gtid_info[0]; candidates.push_back(gtid_info[0]); for (const auto &inst : gtid_info) { if (freshest_instance == &inst) continue; // ignore the first auto rel = mysqlshdk::mysql::compare_gtid_sets( server, freshest_instance->gtid_executed, inst.gtid_executed); switch (rel) { // Conflicting GTID sets case Gtid_set_relation::DISJOINT: case Gtid_set_relation::INTERSECTS: if (!on_conflit) throw shcore::Exception("Conflicting transaction sets between " + freshest_instance->server + " and " + inst.server, SHERR_DBA_DATA_ERRANT_TRANSACTIONS); if (!on_conflit(*freshest_instance, inst)) return candidates; break; case Gtid_set_relation::CONTAINED: // this has more transactions than the best candidate candidates.clear(); candidates.push_back(inst); freshest_instance = &inst; break; case Gtid_set_relation::EQUAL: // this has the same tx set as the best candidate(s) candidates.push_back(inst); break; case Gtid_set_relation::CONTAINS: // this has fewer transactions than the best candidate break; } } return candidates; } void validate_replication_channel_startup_status( const mysqlshdk::mysql::Replication_channel &channel, bool *out_io_on, bool *out_applier_on) { using mysqlshdk::mysql::Replication_channel; if (out_io_on) *out_io_on = false; if (out_applier_on) *out_applier_on = false; // 1st check connection thread if (channel.receiver.state == Replication_channel::Receiver::ON) { // channel is ok, now go check other stuff if (out_io_on) *out_io_on = true; } else { // channel is stopped, not good if (channel.receiver.last_error.code != 0) { if (channel.receiver.last_error.code == ER_ACCESS_DENIED_ERROR) { current_console()->print_error( "Authentication error in replication channel '" + channel.channel_name + "': " + mysqlshdk::mysql::to_string(channel.receiver.last_error)); throw shcore::Exception("Authentication error in replication channel", SHERR_DBA_REPLICATION_AUTH_ERROR); } else { current_console()->print_error( "Receiver error in replication channel '" + channel.channel_name + "': " + mysqlshdk::mysql::to_string(channel.receiver.last_error)); throw shcore::Exception("Error found in replication receiver thread", SHERR_DBA_REPLICATION_CONNECT_ERROR); } } } // then check coordinator thread, if there's one if (channel.coordinator.state != Replication_channel::Coordinator::NONE) { if (channel.coordinator.state == Replication_channel::Coordinator::ON) { // ok, check next } else { if (channel.coordinator.last_error.code != 0) { current_console()->print_error( "Coordinator error in replication channel '" + channel.channel_name + "': " + mysqlshdk::mysql::to_string(channel.coordinator.last_error)); throw shcore::Exception("Error found in replication coordinator thread", SHERR_DBA_REPLICATION_COORDINATOR_ERROR); } } } // finally, check applier threads for (const auto &applier : channel.appliers) { if (applier.state == Replication_channel::Applier::ON) { // ok, check next if (out_applier_on) *out_applier_on = true; } else { if (applier.last_error.code != 0) { current_console()->print_error( "Applier error in replication channel '" + channel.channel_name + "': " + mysqlshdk::mysql::to_string(applier.last_error)); throw shcore::Exception("Error found in replication applier thread", SHERR_DBA_REPLICATION_APPLIER_ERROR); } } } } // TODO(alfredo): check GR recovery channel after instance join with this void check_replication_startup(const mysqlshdk::mysql::IInstance &instance, const std::string &channel_name) { using mysqlshdk::mysql::Replication_channel; Replication_channel channel; log_debug("%s: waiting for replication i/o thread for channel %s", instance.descr().c_str(), channel_name.c_str()); // This checks whether the slave starts correctly try { channel = mysqlshdk::mysql::wait_replication_done_connecting(instance, channel_name); } catch (shcore::Error &e) { throw shcore::Exception::mysql_error_with_code(e.what(), e.code()); } log_debug("%s", mysqlshdk::mysql::format_status(channel, true).c_str()); bool io_on = false; bool sql_on = false; switch (channel.status()) { case Replication_channel::CONNECTING: throw shcore::Exception("Timeout waiting for replication to start", SHERR_DBA_REPLICATION_START_TIMEOUT); case Replication_channel::ON: break; default: try { validate_replication_channel_startup_status(channel, &io_on, &sql_on); } catch (const shcore::Exception &e) { throw shcore::Exception(instance.descr() + ": " + e.what(), e.code()); } if (!io_on || !sql_on) { throw shcore::Exception("Replication thread not in expected state", SHERR_DBA_REPLICATION_START_ERROR); } break; } } bool wait_for_gtid_set_safe(const mysqlshdk::mysql::IInstance &target_instance, const std::string &gtid_set, const std::string &channel_name, int timeout, bool cancelable) { size_t total_transactions_primary, missing_transactions; // The GTID wait will not abort if an error occurs, so calling it without a // timeout will just freeze forever. To prevent that, we call the wait with // smaller incremental timeouts and wait for errors during that. // wait for at most 10s at a time int incremental_timeout = std::min(timeout, 10); if (incremental_timeout == 0) incremental_timeout = 2; int time_elapsed = 0; bool stop = false; shcore::Interrupt_handler intr( [&stop]() { stop = true; return true; }, !cancelable); bool sync_res; // Get the Progress_reporting style option in use auto progress_reporting = current_shell_options()->get().progress_reporting; std::unique_ptr<mysqlshdk::textui::Progress_vt100> progress_bar; const auto update_progress = [](mysqlshdk::textui::Progress_vt100 *bar, uint64_t total, size_t missing) { bar->set_current(total - missing); bar->update(); }; // Get the total count of GTID_EXECUTED from the primary auto gtid_set_primary = mysqlshdk::mysql::Gtid_set::from_normalized_string(gtid_set); total_transactions_primary = gtid_set_primary.count(); using Progress_reporting = Shell_options::Storage::Progress_reporting; if (progress_reporting == Progress_reporting::PROGRESSBAR) { // Init the progress bar progress_bar = std::make_unique<mysqlshdk::textui::Progress_vt100>(0); progress_bar->set_label("** Transactions replicated"); progress_bar->set_total(total_transactions_primary); progress_bar->start(); // Always terminate the progress bar when going out of the scope of the // function. That must happen regardless if the function ended successfully // or not, or because of an exception, to ensure the output is cleared out. shcore::Scoped_callback end_progress_bar( [&progress_bar]() { progress_bar->end(); }); // Update the progress bar to show the current state of the transactions // missing auto gtid_set_target = mysqlshdk::mysql::Gtid_set::from_gtid_executed(target_instance); missing_transactions = mysqlshdk::mysql::estimate_gtid_set_size( gtid_set_primary.subtract(gtid_set_target, target_instance)); update_progress(progress_bar.get(), total_transactions_primary, missing_transactions); } do { // Every minute, get the executed gtid_set at the target instance to check // how many transactions are still missing to provide that info to the user if (time_elapsed != 0 && time_elapsed % 60 == 0) { auto gtid_set_target = mysqlshdk::mysql::Gtid_set::from_gtid_executed(target_instance); missing_transactions = mysqlshdk::mysql::estimate_gtid_set_size( gtid_set_primary.subtract(gtid_set_target, target_instance)); switch (progress_reporting) { case Progress_reporting::PROGRESSBAR: { update_progress(progress_bar.get(), total_transactions_primary, missing_transactions); break; } case Progress_reporting::SIMPLE: { // Simple progress reporting, just print info about the transactions // left mysqlsh::current_console()->print_info( "** " + std::to_string(missing_transactions) + " of " + std::to_string(total_transactions_primary) + " transactions left"); break; } default: // Nothing to do break; } } sync_res = mysqlshdk::mysql::wait_for_gtid_set(target_instance, gtid_set, incremental_timeout); if (!sync_res) { time_elapsed += incremental_timeout; // check for replication errors if the sync timed out // the check function will throw an exception if so. // both i/o and applier errors are checked check_replication_startup(target_instance, channel_name); // continue waiting if there were no errors } else { // wait succeeded, get out of here if (progress_reporting == Progress_reporting::PROGRESSBAR) { update_progress(progress_bar.get(), total_transactions_primary, 0); } break; } } while ((timeout == 0 || time_elapsed < timeout) && !stop); if (stop) throw cancel_sync(); return sync_res; } void wait_for_apply_retrieved_trx( const mysqlshdk::mysql::IInstance &target_instance, const std::string &channel_name, std::chrono::seconds timeout, bool silent) { log_info("Waiting %d seconds for received transactions to be applied at '%s'", static_cast<int>(timeout.count()), target_instance.descr().c_str()); DBUG_EXECUTE_IF("dba_wait_for_apply_retrieved_trx_timeout", { throw shcore::Exception( shcore::str_format( "Timeout waiting for received transactions to be applied on " "instance '%s' (debug)", target_instance.descr().c_str()), SHERR_DBA_GTID_SYNC_TIMEOUT); }); std::string gtid_set = mysqlshdk::mysql::get_received_gtid_set(target_instance, channel_name); // optimize (common?) case where the GTIDs have already been applied if (mysqlshdk::mysql::test_for_gtid_set( target_instance, mysqlshdk::mysql::Gtid_set::from_string(gtid_set))) return; if (!silent) { auto console = mysqlsh::current_console(); console->print_info(shcore::str_format( "** Waiting for received transactions to be applied at '%s'...", target_instance.descr().c_str())); } try { if (!mysqlshdk::mysql::wait_for_gtid_set(target_instance, gtid_set, timeout.count())) { throw shcore::Exception( shcore::str_format( "Timeout waiting for received transactions to be applied on " "instance '%s'", target_instance.descr().c_str()), SHERR_DBA_GTID_SYNC_TIMEOUT); } } catch (const shcore::Error &e) { throw shcore::Exception::mysql_error_with_code(e.what(), e.code()); } } void execute_script(const std::shared_ptr<Instance> &group_server, const std::string &script, const std::string &context) { auto console = mysqlsh::current_console(); bool first_error = true; mysqlshdk::mysql::execute_sql_script( *group_server, script, [&console, &first_error, &context](std::string_view err) { if (first_error) { console->print_error(context); first_error = false; } console->print_error(std::string{err}); }); } void handle_deprecated_option(const std::string &deprecated_name, const std::string &new_name, bool already_set, bool fall_back_to_new_option, const std::string &additional_info) { assert(!deprecated_name.empty()); std::string message; if (!new_name.empty()) { if (already_set) { // If the value is already set (the new name was already read) then errors // out throw shcore::Exception::argument_error( "Cannot use the " + deprecated_name + " and " + new_name + " options simultaneously. The " + deprecated_name + " option is deprecated, please use the " + new_name + " option instead."); } else { // Otherwise just prints a warning if (fall_back_to_new_option) { message = "The " + deprecated_name + " option is deprecated in favor of " + new_name + ". " + new_name + " will be set instead."; } else { message = "The " + deprecated_name + " option is deprecated. Please use the " + new_name + " option instead."; } } } else { message = "The " + deprecated_name + " option is deprecated and will be removed in a future release."; } if (!additional_info.empty()) { message += " " + additional_info; } auto console = current_console(); console->print_warning(message); console->print_info(); } TargetType::Type get_instance_type( const MetadataStorage &metadata, const mysqlshdk::mysql::IInstance &target_instance) { Cluster_type cluster_type; bool has_metadata = false; bool gr_active; try { mysqlshdk::utils::Version md_version; has_metadata = metadata.check_metadata(&md_version, &cluster_type); gr_active = mysqlshdk::gr::is_active_member(target_instance); } catch (const shcore::Error &error) { auto e = shcore::Exception::mysql_error_with_code(error.what(), error.code()); log_warning("Error querying GR member state: %s: %i %s", target_instance.descr().c_str(), error.code(), error.what()); if (error.code() == ER_NO_SUCH_TABLE) { gr_active = false; } else if (error.code() == ER_TABLEACCESS_DENIED_ERROR) { throw std::runtime_error(shcore::str_format( "Unable to detect state for instance '%s'. Please check account " "privileges.", target_instance.get_canonical_address().c_str())); } else { throw shcore::Exception::mysql_error_with_code(error.what(), error.code()); } } if (has_metadata) { if (cluster_type == Cluster_type::GROUP_REPLICATION && gr_active) return TargetType::InnoDBCluster; if (cluster_type == Cluster_type::GROUP_REPLICATION && !gr_active) { // InnoDB cluster but with GR stopped if (metadata.check_cluster_set(&target_instance)) return TargetType::InnoDBClusterSetOffline; return TargetType::StandaloneInMetadata; } if (gr_active) { // GR running but instance is not in the metadata, could be: // - member was added to the cluster by hand // - UUID of member changed // - MD in the group belongs to a different cluster // - others if (cluster_type != Cluster_type::NONE) { log_warning( "Instance %s is running Group Replication, but does not belong to " "a InnoDB cluster", target_instance.descr().c_str()); } return TargetType::GroupReplication; } if (cluster_type == Cluster_type::ASYNC_REPLICATION) return TargetType::AsyncReplicaSet; return TargetType::StandaloneWithMetadata; } if (gr_active) return TargetType::GroupReplication; try { auto channels = mysqlshdk::mysql::get_incoming_channels(target_instance); // filter out GR channels, since they don't indicate AR is configured channels.erase( std::remove_if( channels.begin(), channels.end(), [](const mysqlshdk::mysql::Replication_channel &ch) { return ch.channel_name == mysqlshdk::gr::k_gr_applier_channel || ch.channel_name == mysqlshdk::gr::k_gr_recovery_channel; }), channels.end()); if (!channels.empty()) return TargetType::AsyncReplication; auto slaves = mysqlshdk::mysql::get_slaves(target_instance); if (!slaves.empty()) return TargetType::AsyncReplication; } catch (const shcore::Error &error) { switch (error.code()) { case ER_TABLEACCESS_DENIED_ERROR: case ER_SPECIFIC_ACCESS_DENIED_ERROR: throw std::runtime_error(shcore::str_format( "Unable to detect state for instance '%s'. Please check account " "privileges.", target_instance.get_canonical_address().c_str())); default: throw shcore::Exception::mysql_error_with_code(error.what(), error.code()); } } return TargetType::Standalone; } Cluster_check_info get_cluster_check_info(const MetadataStorage &metadata, Instance *group_server, bool skip_version_check) { Cluster_check_info state; auto group_instance = group_server ? group_server : metadata.get_md_server().get(); // Retrieves the instance configuration type from the perspective of the // active session try { state.source_type = get_instance_type(metadata, *group_instance); } catch (const shcore::Exception &e) { if (mysqlshdk::db::is_server_connection_error(e.code())) { throw shcore::Exception::mysql_error_with_code( group_instance->descr() + ": " + e.what(), e.code()); } else { log_warning("Error detecting GR instance: %s", e.what()); state.source_type = TargetType::Unknown; } } // If it is a GR instance, validates the instance state if (state.source_type == TargetType::GroupReplication || state.source_type == TargetType::InnoDBCluster) { validate_gr_session(group_instance->get_session()); // Retrieves the instance cluster statues from the perspective of the // active session state = get_replication_group_state(*group_instance, state.source_type); // On IDC we want to also determine whether the quorum is just Normal or if // All the instances are ONLINE if (state.source_type == TargetType::InnoDBCluster) { if (state.quorum == ReplicationQuorum::States::Normal) { try { if (metadata.check_all_members_online()) { state.quorum |= ReplicationQuorum::States::All_online; } } catch (const shcore::Error &e) { log_error( "Error while verifying all members in InnoDB Cluster are ONLINE: " "%s", e.what()); throw; } } if (metadata.check_cluster_set(group_instance)) { state.source_type = TargetType::InnoDBClusterSet; } } } else if (state.source_type == TargetType::AsyncReplicaSet) { auto instance = metadata.get_instance_by_uuid(group_instance->get_uuid()); if (instance.primary_master) { state.source_state = ManagedInstance::OnlineRW; } else { state.source_state = ManagedInstance::OnlineRO; } state.quorum = ReplicationQuorum::States::Normal; } else { state.quorum = ReplicationQuorum::States::Normal; state.source_state = ManagedInstance::Offline; } if (!skip_version_check) { state.source_version = group_instance->get_version(); } return state; } mysqlshdk::db::Ssl_options prepare_replica_ssl_options( const mysqlshdk::mysql::IInstance &instance, Cluster_ssl_mode ssl_mode, Replication_auth_type auth_type) { mysqlshdk::db::Ssl_options ssl_options; if (ssl_mode == Cluster_ssl_mode::VERIFY_CA || ssl_mode == Cluster_ssl_mode::VERIFY_IDENTITY || auth_type != Replication_auth_type::PASSWORD) { ssl_options = read_ssl_client_options( instance, ssl_mode != Cluster_ssl_mode::REQUIRED || auth_type != Replication_auth_type::PASSWORD); } switch (ssl_mode) { case Cluster_ssl_mode::NONE: case Cluster_ssl_mode::AUTO: assert(0); // these are supposed to be resolved before reaching here [[fallthrough]]; case Cluster_ssl_mode::DISABLED: ssl_options.set_mode(mysqlshdk::db::Ssl_mode::Disabled); break; case Cluster_ssl_mode::REQUIRED: ssl_options.set_mode(mysqlshdk::db::Ssl_mode::Required); break; case Cluster_ssl_mode::VERIFY_CA: ssl_options.set_mode(mysqlshdk::db::Ssl_mode::VerifyCa); break; case Cluster_ssl_mode::VERIFY_IDENTITY: ssl_options.set_mode(mysqlshdk::db::Ssl_mode::VerifyIdentity); break; } return ssl_options; } namespace cluster_topology_executor_ops { void validate_add_rejoin_options(Group_replication_options options, const std::string &communication_stack) { // ipAllowList cannot be used by addInstance/rejoinInstance when the // communication stack in use by the Cluster is 'MySQL' if (communication_stack == kCommunicationStackMySQL && options.ip_allowlist.has_value()) { throw shcore::Exception::argument_error(shcore::str_format( "Cannot use '%s' when the Cluster's communication stack is " "'%s'", options.ip_allowlist_option_name.c_str(), kCommunicationStackMySQL)); } } bool is_member_auto_rejoining( const std::shared_ptr<mysqlsh::dba::Instance> &target_instance) { // Check if instance was doing auto-rejoin and let the user know that the // rejoin operation will override the auto-rejoin if (mysqlshdk::gr::is_running_gr_auto_rejoin(*target_instance) || mysqlshdk::gr::is_group_replication_delayed_starting(*target_instance)) { auto console = current_console(); console->print_note( "The instance '" + target_instance->get_connection_options().uri_endpoint() + "' is running auto-rejoin process, which will be cancelled."); console->print_info(); return true; } return false; } void ensure_not_auto_rejoining( const std::shared_ptr<mysqlsh::dba::Instance> &target_instance) { current_console()->print_note("Cancelling active GR auto-initialization at " + target_instance->descr()); mysqlshdk::gr::stop_group_replication(*target_instance); } void check_comm_stack_support( const std::shared_ptr<mysqlsh::dba::Instance> &target_instance, Group_replication_options *gr_options, const std::string &communication_stack) { if (communication_stack == kCommunicationStackMySQL && !supports_mysql_communication_stack(target_instance->get_version())) { auto console = mysqlsh::current_console(); console->print_error("Cannot join instance '" + target_instance->descr() + "' to Cluster: The Group Replication protocol stack " "in used by the Cluster ('" + kCommunicationStackMySQL + "') is not supported in the instance"); throw shcore::Exception("Unsupported Group Replication protocol stack '" + std::string(kCommunicationStackMySQL) + "'", SHERR_DBA_UNSUPPORTED_COMMUNICATION_PROTOCOL); } // Set the communication stack protocol to be used only if the target // instance supports it if (supports_mysql_communication_stack(target_instance->get_version())) { gr_options->communication_stack = communication_stack; } } void ensure_instance_check_installed_schema_version( const std::shared_ptr<mysqlsh::dba::Instance> &target_instance, mysqlshdk::utils::Version lowest_cluster_version) { try { // Check instance version compatibility according to Group Replication. mysqlshdk::gr::check_instance_version_compatibility(*target_instance, lowest_cluster_version); } catch (const std::runtime_error &) { auto console = mysqlsh::current_console(); console->print_error( "Cannot join instance '" + target_instance->descr() + "' to cluster: instance version is incompatible with the cluster."); throw; } // Print a warning if the instance is only read compatible. if (mysqlshdk::gr::is_instance_only_read_compatible(*target_instance, lowest_cluster_version)) { auto console = mysqlsh::current_console(); console->print_warning("The instance '" + target_instance->descr() + "' is only read compatible with the cluster, thus " "it will join the cluster in R/O mode."); } } void log_used_gr_options(const Group_replication_options &gr_options) { if (gr_options.local_address.has_value() && !gr_options.local_address->empty()) { log_info("Using Group Replication local address: %s", gr_options.local_address->c_str()); } if (gr_options.group_seeds.has_value() && !gr_options.group_seeds->empty()) { log_info("Using Group Replication group seeds: %s", gr_options.group_seeds->c_str()); } if (gr_options.exit_state_action.has_value() && !gr_options.exit_state_action->empty()) { log_info("Using Group Replication exit state action: %s", gr_options.exit_state_action->c_str()); } if (gr_options.member_weight.has_value()) { log_info("Using Group Replication member weight: %s", std::to_string(*gr_options.member_weight).c_str()); } if (gr_options.consistency.has_value() && !gr_options.consistency->empty()) { log_info("Using Group Replication failover consistency: %s", gr_options.consistency->c_str()); } if (gr_options.expel_timeout.has_value()) { log_info("Using Group Replication expel timeout: %s", std::to_string(*gr_options.expel_timeout).c_str()); } if (gr_options.auto_rejoin_tries.has_value()) { log_info("Using Group Replication auto-rejoin tries: %s", std::to_string(*gr_options.auto_rejoin_tries).c_str()); } } void validate_local_address_ip_compatibility( const std::shared_ptr<mysqlsh::dba::Instance> &target_instance, const std::string &local_address, const std::string &group_seeds, mysqlshdk::utils::Version lowest_cluster_version) { // local_address must have some value assert(!local_address.empty()); // Validate that the group_replication_local_address is valid for the version // of the target instance. if (!mysqlshdk::gr::is_endpoint_supported_by_gr( local_address, target_instance->get_version())) { auto console = mysqlsh::current_console(); console->print_error("Cannot join instance '" + target_instance->descr() + "' to cluster: unsupported localAddress value."); throw shcore::Exception::argument_error(shcore::str_format( "Cannot use value '%s' for option localAddress because it has " "an IPv6 address which is only supported by Group Replication " "from MySQL version >= 8.0.14 and the target instance version is %s.", local_address.c_str(), target_instance->get_version().get_base().c_str())); } // Validate that the localAddress values of the cluster members are // compatible with the target instance we are adding auto local_address_list = shcore::str_split(group_seeds, ","); std::vector<std::string> unsupported_addresses; for (const auto &raw_local_addr : local_address_list) { std::string local_addr = shcore::str_strip(raw_local_addr); if (!mysqlshdk::gr::is_endpoint_supported_by_gr( local_addr, target_instance->get_version())) { unsupported_addresses.push_back(local_addr); } } if (!unsupported_addresses.empty()) { std::string value_str = (unsupported_addresses.size() == 1) ? "value" : "values"; auto console = mysqlsh::current_console(); console->print_error( "Cannot join instance '" + target_instance->descr() + "' to cluster: unsupported localAddress value on the cluster."); throw shcore::Exception::runtime_error(shcore::str_format( "Instance does not support the following localAddress %s of the " "cluster: '%s'. IPv6 " "addresses/hostnames are only supported by Group " "Replication from MySQL version >= 8.0.14 and the target instance " "version is %s.", value_str.c_str(), shcore::str_join(unsupported_addresses.cbegin(), unsupported_addresses.cend(), ", ") .c_str(), target_instance->get_version().get_base().c_str())); } // validate that the value of the localAddress is supported by all // existing cluster members if (!mysqlshdk::gr::is_endpoint_supported_by_gr(local_address, lowest_cluster_version)) { auto console = mysqlsh::current_console(); console->print_error( "Cannot join instance '" + target_instance->descr() + "' to cluster: localAddress value not supported by the cluster."); throw shcore::Exception::argument_error(shcore::str_format( "Cannot use value '%s' for option localAddress because it has " "an IPv6 address which is only supported by Group Replication " "from MySQL version >= 8.0.14 but the cluster " "contains at least one member with version %s.", local_address.c_str(), lowest_cluster_version.get_base().c_str())); } } } // namespace cluster_topology_executor_ops } // namespace dba } // namespace mysqlsh