modules/adminapi/mod_dba.cc (2,228 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/mod_dba.h" #include <mysqld_error.h> #include <algorithm> #include <memory> #include <random> #include <string> #include "utils/error.h" #ifndef WIN32 #include <sys/un.h> #endif #include <iterator> #include <utility> #include <vector> #include "modules/adminapi/common/clone_options.h" #include "modules/adminapi/common/cluster_topology_executor.h" #include "modules/adminapi/common/dba_errors.h" #include "modules/adminapi/common/group_replication_options.h" #include "modules/adminapi/common/metadata_management_mysql.h" #include "modules/adminapi/common/metadata_storage.h" #include "modules/adminapi/common/preconditions.h" #include "modules/adminapi/common/server_features.h" #include "modules/adminapi/common/sql.h" #include "modules/adminapi/common/validations.h" #include "modules/adminapi/dba/check_instance.h" #include "modules/adminapi/dba/configure_instance.h" #include "modules/adminapi/dba/configure_local_instance.h" #include "modules/adminapi/dba/create_cluster.h" #include "modules/adminapi/dba/upgrade_metadata.h" #include "modules/adminapi/dba_utils.h" #include "modules/mod_mysql_resultset.h" #include "modules/mod_shell.h" #include "modules/mod_utils.h" #include "modules/mysqlxtest_utils.h" #include "mysqlshdk/include/scripting/object_factory.h" #include "mysqlshdk/include/scripting/type_info/custom.h" #include "mysqlshdk/include/scripting/type_info/generic.h" #include "mysqlshdk/include/scripting/types_cpp.h" #include "mysqlshdk/include/shellcore/utils_help.h" #include "mysqlshdk/libs/db/mysql/session.h" #include "mysqlshdk/libs/mysql/async_replication.h" #include "mysqlshdk/libs/mysql/group_replication.h" #include "mysqlshdk/libs/mysql/replication.h" #include "mysqlshdk/libs/mysql/utils.h" #include "mysqlshdk/libs/utils/logger.h" #include "mysqlshdk/libs/utils/utils_file.h" #include "mysqlshdk/libs/utils/utils_general.h" #include "mysqlshdk/libs/utils/utils_net.h" #include "mysqlshdk/libs/utils/utils_path.h" #include "mysqlshdk/libs/utils/utils_sqlstring.h" #include "mysqlshdk/libs/utils/utils_string.h" #include "mysqlshdk/shellcore/shell_console.h" /* Sessions used by AdminAPI ========================= The following is a list of the types of MySQL sessions that exist in the AdminAPI. These are conceptual types of sessions and don't necessarily use different classes (they're all db::mysql::Session) or different session object instances. Shell session ------------- The session opened by the shell to connect to the server the user gives in the command line, \connect or shell.connect(). Use by DevAPI and the SQL mode, but not supposed to be used by AdminAPI, except for figuring out what is the cluster being managed. Target member session --------------------- A session to the MySQL server instance that is being operated on for cluster operations, like the member being added or removed from the group. Only instance local operations are meant to be performed with this session. Group-wide operations must go through the Group session. Metadata session ---------------- The session to the MySQL server that stores the metadata. The general design is meant to allow the metadata to be stored in a location different from the cluster being managed, although the implementation does not allow that today.. The MySQL session used for metadata operations is not shared with the shell session. When making changes to the cluster, the metadata session must be read-write and thus, it will connect to the primary by default when created. The connection to primary will not be required if the option sets the option for that. Only metadata operations may use this session (that is, reads and writes on the mysql_innodb_cluster_metadata schema). Group session ------------- A session to a member of a GR group (usually the primary), that will be used to query the performance_schema to know the live status of the group/cluster. Also the member from which group-wide changes are applied, like creating the replication user account for new members. Thus, if changes to the cluster are going to be made, the group session must be connected to the primary. If possible, the group session will shared the same session as the metadata, but not always. Scenarios --------- The shell's target is the primary of the group being managed: Metadata - connect to target Group - share from metadata Member - connect to shell target or param URI The shell's target is the secondary of the group being managed and connectToPrimary is true: Metadata - find primary from metadata in target and connect Group - share from metadata Member - connect to shell target or param URI The shell's target is the secondary of the group being managed and connectToPrimary is false: Metadata - connect to target Group - share from metadata Member - connect to shell target or param URI Note: in the future, if the managed cluster is not where the metadata resides, the metadata schema should contain information about where to find the real metadata. */ namespace mysqlsh { namespace dba { namespace sandbox { struct Op_data { std::string name; std::string progressive; std::string past; }; static std::map<std::string, Op_data> Operations_text{ {"delete", {"deleteSandboxInstance", "Deleting", "deleted"}}, {"kill", {"killSandboxInstance", "Killing", "killed"}}, {"stop", {"stopSandboxInstance", "Stopping", "stopped"}}, {"start", {"startSandboxInstance", "Starting", "started"}}}; } // namespace sandbox namespace { void validate_port(int port, const std::string &name) { if (port < 1024 || port > 65535) throw shcore::Exception::argument_error( "Invalid value for '" + name + "': Please use a valid TCP port number >= 1024 and <= 65535"); } void throw_instance_op_error(const shcore::Array_t &errors) { if (!errors || errors->empty()) return; std::vector<std::string> str_errors; str_errors.reserve(errors->size()); for (const 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); } throw shcore::Exception::runtime_error(shcore::str_join(str_errors, "\n")); } } // namespace using mysqlshdk::db::uri::formats::only_transport; /* * Global helper text for setOption and setInstance Option namespaces */ REGISTER_HELP(NAMESPACE_TAG, "@li tag:@<option@>: built-in and user-defined tags to be " "associated to the Cluster."); REGISTER_HELP_DETAIL_TEXT(NAMESPACE_TAG_DETAIL_CLUSTER, R"*( <b>Tags</b> Tags make it possible to associate custom key/value pairs to a Cluster, storing them in its metadata. Custom tag names can be any string starting with letters and followed by letters, numbers and _. Tag values may be any JSON value. If the value is null, the tag is deleted. )*"); REGISTER_HELP_DETAIL_TEXT(NAMESPACE_TAG_DETAIL_REPLICASET, R"*( <b>Tags</b> Tags make it possible to associate custom key/value pairs to a ReplicaSet, storing them in its metadata. Custom tag names can be any string starting with letters and followed by letters, numbers and _. Tag values may be any JSON value. If the value is null, the tag is deleted. )*"); REGISTER_HELP_DETAIL_TEXT(NAMESPACE_TAG_INSTANCE_DETAILS_EXTRA, R"*( The following pre-defined tags are available: @li _hidden: bool - instructs the router to exclude the instance from the list of possible destinations for client applications. @li _disconnect_existing_sessions_when_hidden: bool - instructs the router to disconnect existing connections from instances that are marked to be hidden. @note '_hidden' and '_disconnect_existing_sessions_when_hidden' can be useful to shut down the instance and perform maintenance on it without disrupting incoming application traffic. )*"); REGISTER_HELP(OPT_MEMBER_AUTH_TYPE, "@li memberAuthType: controls the authentication type to use for " "the internal replication accounts."); REGISTER_HELP( OPT_CERT_ISSUER, "@li certIssuer: common certificate issuer to use when 'memberAuthType' " "contains either \"CERT_ISSUER\" or \"CERT_SUBJECT\"."); REGISTER_HELP(OPT_CERT_SUBJECT, "@li certSubject: instance's certificate subject to use when " "'memberAuthType' contains \"CERT_SUBJECT\"."); REGISTER_HELP_DETAIL_TEXT(OPT_MEMBER_AUTH_TYPE_EXTRA, R"*( The memberAuthType option supports the following values: @li PASSWORD: account authenticates with password only. @li CERT_ISSUER: account authenticates with client certificate, which must match the expected issuer (see 'certIssuer' option). @li CERT_SUBJECT: account authenticates with client certificate, which must match the expected issuer and subject (see 'certSubject' option). @li CERT_ISSUER_PASSWORD: combines both "CERT_ISSUER" and "PASSWORD" values. @li CERT_SUBJECT_PASSWORD: combines both "CERT_SUBJECT" and "PASSWORD" values. )*"); /* * Global helper text for InnoDB Cluster configuration options used in: * - dba.createCluster() * - Cluster.addInstance() * - Cluster.setOption() * - Cluster.setInstanceOption() * - ClusterSet.createReplicaCluster() */ REGISTER_HELP(CLUSTER_OPT_MEMBER_SSL_MODE, "@li memberSslMode: SSL mode for communication channels opened " "by Group Replication from one server to another."); REGISTER_HELP(CLUSTER_OPT_EXIT_STATE_ACTION, "@li exitStateAction: string value indicating the group " "replication exit state action."); REGISTER_HELP(CLUSTER_OPT_MEMBER_WEIGHT, "@li memberWeight: integer value with a percentage weight for " "automatic primary election on failover."); REGISTER_HELP(CLUSTER_OPT_FAILOVER_CONSISTENCY, "@li failoverConsistency: string value indicating the " "consistency guarantees that the cluster provides."); REGISTER_HELP(CLUSTER_OPT_CONSISTENCY, "@li consistency: string value indicating the " "consistency guarantees that the cluster provides."); REGISTER_HELP(CLUSTER_OPT_EXPEL_TIMEOUT, "@li expelTimeout: integer value to define the time period in " "seconds that cluster members should wait for a non-responding " "member before evicting it from the cluster."); REGISTER_HELP( CLUSTER_OPT_AUTO_REJOIN_TRIES, "@li autoRejoinTries: integer value to define the number of times an " "instance will attempt to rejoin the cluster after being expelled."); REGISTER_HELP(CLUSTER_OPT_IP_WHITELIST, "@li ipWhitelist: The list of hosts allowed to connect to the " "instance for group replication. Deprecated."); REGISTER_HELP(CLUSTER_OPT_IP_ALLOWLIST, "@li ipAllowlist: The list of hosts allowed to connect to the " "instance for group replication. Only valid if " "communicationStack=XCOM."); REGISTER_HELP(CLUSTER_OPT_COMM_STACK, "@li communicationStack: The Group Replication protocol stack to " "be used in the Cluster: XCom (legacy) or MySQL."); REGISTER_HELP(CLUSTER_OPT_LOCAL_ADDRESS, "@li localAddress: string value with the Group Replication local " "address to be used instead of the automatically generated one."); REGISTER_HELP( CLUSTER_OPT_TRANSACTION_SIZE_LIMIT, "@li transactionSizeLimit: integer value to configure the maximum " "transaction size in bytes which the Cluster accepts"); REGISTER_HELP( CLUSTER_OPT_PAXOS_SINGLE_LEADER, "@li paxosSingleLeader: boolean value used to enable/disable the Group " "Communication engine to operate with a single consensus leader."); REGISTER_HELP( OPT_INTERACTIVE, "@li interactive: boolean value used to disable/enable the wizards in the " "command execution, i.e. prompts and confirmations will be provided or not " "according to the value set. The default value is equal to MySQL Shell " "wizard mode. Deprecated."); REGISTER_HELP(OPT_APPLIERWORKERTHREADS, "@li applierWorkerThreads: Number of threads used for applying " "replicated transactions. The default value is 4."); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_MEMBER_SSL_MODE_DETAIL, R"*( The memberSslMode option controls whether TLS is to be used for connections opened by Group Replication from one server to another (both recovery and group communication, in either communication stack). It also controls what kind of verification the client end of connections perform on the SSL certificate presented by the server end. The memberSslMode option supports the following values: @li REQUIRED: if used, SSL (encryption) will be enabled for the instances to communicate with other members of the cluster @li VERIFY_CA: Like REQUIRED, but additionally verify the peer server TLS certificate against the configured Certificate Authority (CA) certificates. @li VERIFY_IDENTITY: Like VERIFY_CA, but additionally verify that the peer server certificate matches the host to which the connection is attempted. @li DISABLED: if used, SSL (encryption) will be disabled @li AUTO: if used, SSL (encryption) will be enabled if supported by the instance, otherwise disabled If memberSslMode is not specified AUTO will be used by default. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_EXIT_STATE_ACTION_DETAIL, R"*( The exitStateAction option supports the following values: @li ABORT_SERVER: if used, the instance shuts itself down if it leaves the cluster unintentionally or exhausts its auto-rejoin attempts. @li READ_ONLY: if used, the instance switches itself to super-read-only mode if it leaves the cluster unintentionally or exhausts its auto-rejoin attempts. @li OFFLINE_MODE: if used, the instance switches itself to offline mode if it leaves the cluster unintentionally or exhausts its auto-rejoin attempts. Requires MySQL 8.0.18 or newer. If exitStateAction is not specified READ_ONLY will be used by default. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_EXIT_STATE_ACTION_EXTRA, R"*( The value for exitStateAction is used to configure how Group Replication behaves when a server instance leaves the group unintentionally (for example after encountering an applier error) or exhausts its auto-rejoin attempts. When set to ABORT_SERVER, the instance shuts itself down. When set to READ_ONLY the server switches itself to super-read-only mode. When set to OFFLINE_MODE it switches itself to offline mode. In this mode, connected client users are disconnected on their next request and connections are no longer accepted, with the exception of client users that have the CONNECTION_ADMIN or SUPER privilege. The exitStateAction option accepts case-insensitive string values, being the accepted values: OFFLINE_MODE (or 2), ABORT_SERVER (or 1) and READ_ONLY (or 0). The default value is READ_ONLY. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_MEMBER_WEIGHT_DETAIL_EXTRA, R"*( The value for memberWeight is used to set the Group Replication system variable 'group_replication_member_weight'. The memberWeight option accepts integer values. Group Replication limits the value range from 0 to 100, automatically adjusting it if a lower/bigger value is provided. Group Replication uses a default value of 50 if no value is provided. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_CONSISTENCY_DETAIL, R"*( The consistency option supports the following values: @li BEFORE_ON_PRIMARY_FAILOVER: if used, new queries (read or write) to the new primary will be put on hold until after the backlog from the old primary is applied. @li EVENTUAL: if used, read queries to the new primary are allowed even if the backlog isn't applied. If consistency is not specified, EVENTUAL will be used by default. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_CONSISTENCY_EXTRA, R"*( The value for consistency is used to set the Group Replication system variable 'group_replication_consistency' and configure the transaction consistency guarantee which a cluster provides. When set to BEFORE_ON_PRIMARY_FAILOVER, whenever a primary failover happens in single-primary mode (default), new queries (read or write) to the newly elected primary that is applying backlog from the old primary, will be hold before execution until the backlog is applied. When set to EVENTUAL, read queries to the new primary are allowed even if the backlog isn't applied but writes will fail (if the backlog isn't applied) due to super-read-only mode being enabled. The client may return old values. When set to BEFORE, each transaction (RW or RO) waits until all preceding transactions are complete before starting its execution. This ensures that each transaction is executed on the most up-to-date snapshot of the data, regardless of which member it is executed on. The latency of the transaction is affected but the overhead of synchronization on RW transactions is reduced since synchronization is used only on RO transactions. When set to AFTER, each RW transaction waits until its changes have been applied on all of the other members. This ensures that once this transaction completes, all following transactions read a database state that includes its changes, regardless of which member they are executed on. This mode shall only be used on a group that is used for predominantly RO operations to ensure that subsequent reads fetch the latest data which includes the latest writes. The overhead of synchronization on every RO transaction is reduced since synchronization is used only on RW transactions. When set to BEFORE_AND_AFTER, each RW transaction waits for all preceding transactions to complete before being applied and until its changes have been applied on other members. A RO transaction waits for all preceding transactions to complete before execution takes place. This ensures the guarantees given by BEFORE and by AFTER. The overhead of synchronization is higher. The consistency option accepts case-insensitive string values, being the accepted values: EVENTUAL (or 0), BEFORE_ON_PRIMARY_FAILOVER (or 1), BEFORE (or 2), AFTER (or 3), and BEFORE_AND_AFTER (or 4). The default value is EVENTUAL. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_EXPEL_TIMEOUT_EXTRA, R"*( The value for expelTimeout is used to set the Group Replication system variable 'group_replication_member_expel_timeout' and configure how long Group Replication will wait before expelling from the group any members suspected of having failed. On slow networks, or when there are expected machine slowdowns, increase the value of this option. The expelTimeout option accepts positive integer values and, since 8.0.21, defaults to 5 seconds. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_AUTO_REJOIN_TRIES_EXTRA, R"*( The value for autoRejoinTries is used to set the Group Replication system variable 'group_replication_autorejoin_tries' and configure how many times an instance will try to rejoin a Group Replication group after being expelled. In scenarios where network glitches happen but recover quickly, setting this option prevents users from having to manually add the expelled node back to the group. The autoRejoinTries option accepts positive integer values and, since 8.0.21, defaults to 3. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_IP_ALLOWLIST_EXTRA, R"*( The ipAllowlist format is a comma separated list of IP addresses or subnet CIDR notation, for example: 192.168.1.0/24,10.0.0.1. By default the value is set to AUTOMATIC, allowing addresses from the instance private network to be automatically set for the allowlist. This option is only used and allowed when communicationStack is set to XCOM. )*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_COMM_STACK_EXTRA, R"*( The value for communicationStack is used to choose which Group Replication communication stack must be used in the Cluster. It's used to set the value of the Group Replication system variable 'group_replication_communication_stack'. When set to legacy 'XCom', all internal GCS network traffic (PAXOS and communication infrastructure) flows through a separate network address: the localAddress. When set to 'MySQL', such traffic re-uses the existing MySQL Server facilities to establish connections among Cluster members. It allows a simpler and safer setup as it obsoletes the usage of IP allowlists (ipAllowlist), removes the explicit need to have a separate network address (localAddress), and introduces user-based authentication. The default value for Clusters running 8.0.27+ is 'MySQL'.)*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_LOCAL_ADDRESS_EXTRA, R"*( The value for localAddress is used to set the Group Replication system variable 'group_replication_local_address'. The localAddress option accepts values in the format: 'host:port' or 'host:' or ':port'. If the specified value does not include a colon (:) and it is numeric, then it is assumed to be the port, otherwise it is considered to be the host. When the host is not specified, the default value is the value of the system variable 'report_host' if defined (i.e., not 'NULL'), otherwise it is the hostname value. When the port is not specified, the default value is the port of the target instance if the communication stack in use by the Cluster is 'MYSQL', otherwise, port * 10 + 1 when the communication stack is 'XCOM'. In case the automatically determined default port value is invalid (> 65535) then an error is thrown.)*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_TRANSACTION_SIZE_LIMIT_EXTRA, R"*( The value for transactionSizeLimit is used to set the Group Replication system variable 'group_replication_transaction_size_limit' and configures the maximum transaction size in bytes which the Cluster accepts. Transactions larger than this size are rolled back by the receiving member and are not broadcast to the Cluster. The transactionSizeLimit option accepts positive integer values and, if set to zero, there is no limit to the size of transactions the Cluster accepts All members added or rejoined to the Cluster will use the same value.)*"); REGISTER_HELP_DETAIL_TEXT(CLUSTER_OPT_PAXOS_SINGLE_LEADER_EXTRA, R"*( The value for paxosSingleLeader is used to enable or disable the Group Communication engine to operate with a single consensus leader when the Cluster is in single-primary more. When enabled, the Cluster uses a single leader to drive consensus which improves performance and resilience in single-primary mode, particularly when some of the Cluster's members are unreachable. The option is available on MySQL 8.0.31 or newer and the default value is 'OFF'. )*"); // TODO create a dedicated topic for InnoDB clusters and replicasets, // with a quick tutorial for both, in addition to more deep technical info // Documentation of the DBA Class REGISTER_HELP_TOPIC(AdminAPI, CATEGORY, adminapi, Contents, SCRIPTING); REGISTER_HELP_TOPIC_WITH_BRIEF_TEXT(ADMINAPI, R"*( The <b>AdminAPI</b> is an API that enables configuring and managing InnoDB Clusters, ReplicaSets, ClusterSets, among other things. The AdminAPI can be used interactively from the MySQL Shell prompt and non-interactively from JavaScript and Python scripts and directly from the command line. For more information about the <b>dba</b> object use: \\? dba In the AdminAPI, an InnoDB Cluster is represented as an instance of the <b>Cluster</b> class, while ReplicaSets are represented as an instance of the <b>ReplicaSet</b> class, and ClusterSets are represented as an instance of the <b>ClusterSet</b> class. For more information about the <b>Cluster</b> class use: \\? Cluster For more information about the <b>ClusterSet</b> class use: \\? ClusterSet For more information about the <b>ReplicaSet</b> class use: \\? ReplicaSet <b>Scripting</b> Through the JavaScript and Python bindings of the MySQL Shell, the AdminAPI can be used from scripts, which can in turn be used interactively or non-interactively. To execute a script, use the -f command line option, followed by the script file path. Options that follow the path are passed directly to the script being executed, which can access them from sys.argv @code mysqlsh root@localhost -f myscript.py arg1 arg2 @endcode If the script finishes successfully, the Shell will exit with code 0, while uncaught exceptions/errors cause it to exist with a non-0 code. By default, the AdminAPI enables interactivity, which will cause functions to prompt for missing passwords, confirmations and bits of information that cannot be obtained automatically. Prompts can be completely disabled with the --no-wizard command line option or using the "interactive" boolean option available in some of functions. If interactivity is disabled and some information is missing (e.g. a password), an error will be raised instead of being prompted. <b>Secure Password Handling</b> Passwords can be safely stored locally, using the system's native secrets storage functionality (or login-paths in Linux). Whenever the Shell needs a password, it will first query for the password in the system, before prompting for it. Passwords can be stored during interactive use, by confirming in the Store Password prompt. They can also be stored programmatically, using the shell.<<<storeCredential>>>() function. You can also use environment variables to pass information to your scripts. In JavaScript, the os.getenv() function can be used to access them. <b>Command Line Interface</b> In addition to the scripting interface, the MySQL Shell supports generic command line integration, which allows calling AdminAPI functions directly from the system shell (e.g. bash). Examples: @code $ mysqlsh -- dba configure-instance root@localhost is equivalent to: > dba.configureInstance("root@localhost") $ mysqlsh root@localhost -- cluster status --extended is equivalent to: > dba.getCluster().status({extended:true}) @endcode The mapping from AdminAPI function signatures works as follows: @li The first argument after a -- can be a shell global object, such as dba. As a special case, cluster and rs are also accepted. @li The second argument is the name of the function of the object to be called. The naming convention is automatically converted from camelCase/snake_case to lower case separated by dashes. @li The rest of the arguments are used in the same order as their JS/Python counterparts. Instances can be given as URIs. Option dictionaries can be passed as --options, where the option name is the same as in JS/Python. )*"); REGISTER_HELP_GLOBAL_OBJECT(dba, adminapi); REGISTER_HELP( DBA_BRIEF, "InnoDB Cluster, ReplicaSet, and ClusterSet management functions."); REGISTER_HELP( DBA_GLOBAL_BRIEF, "Used for InnoDB Cluster, ReplicaSet, and ClusterSet administration."); REGISTER_HELP_TOPIC_TEXT(DBA, R"*( Entry point for AdminAPI functions, including InnoDB Clusters, ReplicaSets, and ClusterSets. <b>InnoDB Clusters</b> The dba.<<<configureInstance>>>() function can be used to configure a MySQL instance with the settings required to use it in an InnoDB Cluster. InnoDB Clusters can be created with the dba.<<<createCluster>>>() function. Once created, InnoDB Cluster management objects can be obtained with the dba.<<<getCluster>>>() function. <b>InnoDB ReplicaSets</b> The dba.<<<configureReplicaSetInstance>>>() function can be used to configure a MySQL instance with the settings required to use it in a ReplicaSet. ReplicaSets can be created with the dba.<<<createReplicaSet>>>() function. Once created, ReplicaSet management objects can be obtained with the dba.<<<getReplicaSet>>>() function. <b>InnoDB ClusterSets</b> ClusterSets can be created with the <Cluster>.<<<createClusterSet>>>() function. Once created, ClusterSet management objected can be obtained with the dba.<<<getClusterSet>>>() or <Cluster>.<<<getClusterSet>>>() functions. <b>Sandboxes</b> Utility functions are provided to create sandbox MySQL instances, which can be used to create test Clusters and ReplicaSets. )*"); REGISTER_HELP(DBA_CLOSING, "<b>SEE ALSO</b>"); REGISTER_HELP( DBA_CLOSING1, "@li For general information about the AdminAPI use: \\? AdminAPI"); REGISTER_HELP( DBA_CLOSING2, "@li For help on a specific function use: \\? dba.<functionName>"); REGISTER_HELP(DBA_CLOSING3, "e.g. \\? dba.<<<deploySandboxInstance>>>"); Dba::Dba(shcore::IShell_core *owner) : _shell_core(owner) { init(); } Dba::Dba(const std::shared_ptr<ShellBaseSession> &session) : m_session(session) { init(); } Dba::~Dba() {} bool Dba::operator==(const Object_bridge &other) const { return class_name() == other.class_name() && this == &other; } void Dba::init() { add_property("verbose"); add_property("session"); // Pure functions expose("createCluster", &Dba::create_cluster, "name", "?options")->cli(); expose("getCluster", &Dba::get_cluster, "?name", "?options")->cli(false); expose("dropMetadataSchema", &Dba::drop_metadata_schema, "?options")->cli(); expose("checkInstanceConfiguration", &Dba::check_instance_configuration, "?instance", "?options") ->cli(); expose("deploySandboxInstance", &Dba::deploy_sandbox_instance, "port", "?options") ->cli(); expose("startSandboxInstance", &Dba::start_sandbox_instance, "port", "?options") ->cli(); expose("stopSandboxInstance", &Dba::stop_sandbox_instance, "port", "?options") ->cli(); expose("deleteSandboxInstance", &Dba::delete_sandbox_instance, "port", "?options") ->cli(); expose("killSandboxInstance", &Dba::kill_sandbox_instance, "port", "?options") ->cli(); expose("rebootClusterFromCompleteOutage", &Dba::reboot_cluster_from_complete_outage, "?clusterName", "?options") ->cli(); expose("upgradeMetadata", &Dba::upgrade_metadata, "?options")->cli(); expose("configureLocalInstance", &Dba::configure_local_instance, "?instance", "?options") ->cli(); expose("configureInstance", &Dba::configure_instance, "?instance", "?options") ->cli(); expose("configureReplicaSetInstance", &Dba::configure_replica_set_instance, "?instance", "?options") ->cli(); expose("createReplicaSet", &Dba::create_replica_set, "name", "?options") ->cli(); expose("getReplicaSet", &Dba::get_replica_set)->cli(false); expose("getClusterSet", &Dba::get_cluster_set)->cli(false); } void Dba::set_member(const std::string &prop, shcore::Value value) { if (prop == "verbose") { try { int verbosity = value.as_int(); _provisioning_interface.set_verbose(verbosity); } catch (const shcore::Exception &e) { throw shcore::Exception::value_error( "Invalid value for property 'verbose', use either boolean or integer " "value."); } } else { Cpp_object_bridge::set_member(prop, value); } } REGISTER_HELP_PROPERTY(verbose, dba); REGISTER_HELP_PROPERTY_TEXT(DBA_VERBOSE, R"*( Controls debug message verbosity for sandbox related <b>dba</b> operations. The assigned value can be either boolean or integer, the result depends on the assigned value: @li 0: disables mysqlprovision verbosity @li 1: enables mysqlprovision verbosity @li >1: enables mysqlprovision debug verbosity @li Boolean: equivalent to assign either 0 or 1 )*"); /** * $(DBA_VERBOSE_BRIEF) * * $(DBA_VERBOSE) */ #if DOXYGEN_JS || DOXYGEN_PY int Dba::verbose; #endif REGISTER_HELP_PROPERTY(session, dba); REGISTER_HELP_PROPERTY_TEXT(DBA_SESSION, R"*( The session the dba object will use by default. Reference to the MySQL session that will be used as the default target for AdminAPI operations such as <<<getCluster>>>() or <<<createCluster>>>(). This is a read-only property. )*"); /** * $(DBA_SESSION_BRIEF) * * $(DBA_SESSION) */ #if DOXYGEN_JS || DOXYGEN_PY Session Dba::session; #endif shcore::Value Dba::get_member(const std::string &prop) const { shcore::Value ret_val; if (prop == "verbose") { ret_val = shcore::Value(_provisioning_interface.get_verbose()); } else if (prop == "session") { if (m_session) { ret_val = shcore::Value(m_session); } else if (_shell_core && _shell_core->get_dev_session()) { ret_val = shcore::Value(_shell_core->get_dev_session()); } } else { ret_val = Cpp_object_bridge::get_member(prop); } return ret_val; } constexpr const char INNODB_SSH_NOT_SUPPORTED[] = "InnoDB cluster functionality is not available through SSH tunneling"; /* Open a classic session to the instance the shell is connected to. If the shell's session is X protocol, it will query it for the classic port and connect to it. */ std::shared_ptr<Instance> Dba::connect_to_target_member() const { DBUG_TRACE; auto active_shell_session = get_active_shell_session(); if (active_shell_session) { if (active_shell_session->get_connection_options().has_ssh_options()) { throw shcore::Exception::logic_error(INNODB_SSH_NOT_SUPPORTED); } return Instance::connect( get_classic_connection_options(active_shell_session), false); } return {}; } /* Create a metadata object and a MySQL session for managing the InnoDB cluster which the shell's active session target is part of. If a target_member_session is given, it will be reused for the metadata and group sessions if possible. Note that the target_member_session must not be the shell's global session. */ void Dba::connect_to_target_group( std::shared_ptr<Instance> target_member, std::shared_ptr<MetadataStorage> *out_metadata, std::shared_ptr<Instance> *out_group_server, bool connect_to_primary) const { bool owns_target_member_session = false; if (!target_member) { target_member = connect_to_target_member(); owns_target_member_session = true; } else if (target_member->get_connection_options().has_ssh_options()) { throw shcore::Exception::logic_error(INNODB_SSH_NOT_SUPPORTED); } if (!target_member) { throw shcore::Exception::logic_error( "The shell must be connected to a member of the InnoDB cluster being " "managed"); } if (connect_to_primary) { std::string primary_uri = find_primary_member_uri(target_member, false); if (primary_uri.empty()) { throw shcore::Exception("Unable to find a primary member in the Cluster", SHERR_DBA_GROUP_MEMBER_NOT_ONLINE); } else if (!mysqlshdk::utils::are_endpoints_equal( primary_uri, target_member->get_connection_options().uri_endpoint())) { log_info("%s is not a primary, will try to find one and reconnect", target_member->get_connection_options().as_uri().c_str()); try { mysqlshdk::db::Connection_options coptions(primary_uri); coptions.set_login_options_from( target_member->get_connection_options()); log_info("Opening session to primary of InnoDB cluster at %s...", coptions.as_uri().c_str()); auto instance = Instance::connect(coptions); mysqlshdk::gr::Member_state state = mysqlshdk::gr::get_member_state(*instance); // Check whether the member we connected to is really ONLINE. If e.g. // the PRIMARY restarted, it will not be expelled from the group until // some time has passed. if (state != mysqlshdk::gr::Member_state::ONLINE) { log_warning("PRIMARY member %s is currently in state %s", instance->descr().c_str(), mysqlshdk::gr::to_string(state).c_str()); throw shcore::Exception("Group PRIMARY is not ONLINE", SHERR_DBA_GROUP_MEMBER_NOT_ONLINE); } log_info( "Metadata and group sessions are now using the primary member"); if (owns_target_member_session) target_member->close_session(); target_member = instance; } catch (const shcore::Exception &e) { // If we reached here it means Group Replication is still reporting a // primary available but we couldn't connect to it log_warning("PRIMARY member '%s' is currently unreachable: %s", primary_uri.c_str(), e.what()); throw shcore::Exception( "Unable to connect to the primary member of the Cluster: '" + std::string(e.what()) + "'", SHERR_DBA_GROUP_MEMBER_NOT_ONLINE); } catch (const std::exception &) { throw; } } } if (out_group_server) *out_group_server = target_member; // Check if the metadataStorage doesn't have an open session and create it if // necessary if (out_metadata && (!out_metadata->get() || !out_metadata->get()->is_valid())) { *out_metadata = std::make_shared<MetadataStorage>( Instance::connect(target_member->get_connection_options())); } // Check if we need to reset the MetadataStorage to use the Primary member bool is_md_connected_to_primary = mysqlshdk::utils::are_endpoints_equal( out_metadata->get()->get_md_server()->get_connection_options().as_uri(), target_member->get_connection_options().as_uri()); if (connect_to_primary && !is_md_connected_to_primary) { *out_metadata = std::make_shared<MetadataStorage>( Instance::connect(target_member->get_connection_options())); } // Check if the target instance has MD schema mysqlshdk::utils::Version md_version; bool has_md_schema = out_metadata->get()->check_version(&md_version); // Check if the target member belongs to a ClusterSet and get the connection // to the primary instance of the primary cluster if (out_metadata && has_md_schema && md_version >= mysqlshdk::utils::Version(2, 1, 0)) { Cluster_metadata cluster_md; out_metadata->get()->get_cluster_for_server_uuid(target_member->get_uuid(), &cluster_md); // IF the cluster is part of a ClusterSet // Get the primary cluster if (!cluster_md.cluster_set_id.empty() && connect_to_primary) { std::vector<Cluster_set_member_metadata> cs_members; Cluster_metadata primary_cluster; if (!out_metadata->get()->get_cluster_set(cluster_md.cluster_set_id, false, nullptr, &cs_members)) { throw std::runtime_error("Metadata not found"); } for (const auto &m : cs_members) { if (m.primary_cluster) { primary_cluster = m.cluster; break; } } // Check if the MD server we're connected to is the PRIMARY Cluster. // If not, we need to establish a session to the primary member of the // PRIMARY cluster if (out_metadata->get()->get_md_server()->get_group_name() != primary_cluster.group_name) { Scoped_instance_pool ipool( *out_metadata, current_shell_options()->get().wizards, Instance_pool::Auth_options( target_member->get_connection_options())); try { std::shared_ptr<Instance> primary_instance( ipool->connect_group_primary(primary_cluster.group_name)); primary_instance->steal(); out_metadata->reset(new MetadataStorage(primary_instance)); } catch (const shcore::Exception &e) { auto console = current_console(); log_error( "Could not connect to any member of the PRIMARY Cluster: %s", e.what()); if (e.code() == SHERR_DBA_GROUP_HAS_NO_PRIMARY || e.code() == SHERR_DBA_GROUP_REPLICATION_NOT_RUNNING) { console->print_warning( "Could not connect to any member of PRIMARY Cluster '" + primary_cluster.cluster_name + "', topology changes will not be allowed on the InnoDB Cluster " "'" + cluster_md.cluster_name + "'"); } else if (e.code() == SHERR_DBA_GROUP_HAS_NO_QUORUM) { console->print_warning("The PRIMARY Cluster '" + primary_cluster.cluster_name + "' lost the quorum, topology changes will " "not be allowed on the InnoDB Cluster " "'" + cluster_md.cluster_name + "'"); console->print_note( "To restore the Cluster and ClusterSet operations, restore the " "quorum on the PRIMARY Cluster using " "<<<forceQuorumUsingPartitionOf>>>()"); } else { throw; } } catch (const mysqlshdk::db::Error &) { throw; } } } } } std::shared_ptr<mysqlshdk::db::ISession> Dba::get_active_shell_session() const { if (m_session) return m_session->get_core_session(); if (_shell_core && _shell_core->get_dev_session()) return _shell_core->get_dev_session()->get_core_session(); return {}; } // Documentation of the getCluster function REGISTER_HELP_FUNCTION(getCluster, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_GETCLUSTER, R"*( Returns an object representing a Cluster. @param name Optional parameter to specify the name of the Cluster to be returned. @param options Optional dictionary with additional options. @returns The Cluster object identified by the given name or the default Cluster. If name is not specified or is null, the default Cluster will be returned. If name is specified, and no Cluster with the indicated name is found, an error will be raised. The options dictionary may contain the following attributes: @li connectToPrimary: boolean value used to indicate if Shell should automatically connect to the primary member of the Cluster or not. Deprecated and ignored, Shell will attempt to connect to the primary by default and fallback to a secondary when not possible. )*"); /** * $(DBA_GETCLUSTER_BRIEF) * * $(DBA_GETCLUSTER) */ #if DOXYGEN_JS Cluster Dba::getCluster(String name, Dictionary options) {} #elif DOXYGEN_PY Cluster Dba::get_cluster(str name, dict options) {} #endif std::shared_ptr<Cluster> Dba::get_cluster( const mysqlshdk::null_string &cluster_name, const shcore::Dictionary_t &options) const { std::shared_ptr<Instance> target_member, group_server; std::shared_ptr<MetadataStorage> metadata, metadata_at_target; auto console = mysqlsh::current_console(); try { target_member = connect_to_target_member(); if (!target_member) { // An open session is required throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } metadata = std::make_shared<MetadataStorage>( Instance::connect(target_member->get_connection_options())); metadata_at_target = metadata; // Init the connection pool Scoped_instance_pool ipool(current_shell_options()->get().wizards, target_member->get_connection_options()); check_function_preconditions("Dba.getCluster", metadata, target_member); if (options && options->has_key("connectToPrimary")) { console->print_warning( "Option 'connectToPrimary' is deprecated and it will " "be removed in a future release. Shell will attempt to connect to " "the primary member of the Cluster by default and fallback to a " "secondary when not possible"); } // This will throw if not a cluster member try { // Connect to the target cluster member and // also find the primary and connect to it, unless target is already // primary or connectToPrimary:false was given connect_to_target_group(target_member, &metadata, &group_server, true); } catch (const shcore::Exception &e) { // Print warning in case a cluster error is found (e.g., no quorum). if (e.code() == SHERR_DBA_GROUP_HAS_NO_QUORUM) { console->print_warning( "Cluster has no quorum and cannot process write transactions: " + std::string(e.what())); } else { console->print_warning("Error connecting to Cluster: " + e.format()); } if ((e.code() == SHERR_DBA_GROUP_HAS_NO_QUORUM || e.code() == SHERR_DBA_GROUP_MEMBER_NOT_ONLINE)) { console->print_info("Retrying getCluster() using a secondary member"); connect_to_target_group({}, &metadata, &group_server, false); } else { throw; } } ipool->set_metadata(metadata); if (current_shell_options()->get().wizards) { auto state = get_cluster_check_info(*metadata); if (state.source_state == mysqlsh::dba::ManagedInstance::OnlineRO) { console->print_info( "WARNING: You are connected to an instance in state '" + mysqlsh::dba::ManagedInstance::describe( static_cast<mysqlsh::dba::ManagedInstance::State>( state.source_state)) + "'\n" "Write operations on the InnoDB cluster will not be allowed.\n"); } else if (state.source_state != mysqlsh::dba::ManagedInstance::OnlineRW) { console->print_info( "WARNING: You are connected to an instance in state '" + mysqlsh::dba::ManagedInstance::describe( static_cast<mysqlsh::dba::ManagedInstance::State>( state.source_state)) + "'\n" "Write operations on the InnoDB cluster will not be allowed.\n" "Output from describe() and status() may be outdated.\n"); } } auto cluster = get_cluster(!cluster_name ? nullptr : cluster_name->c_str(), metadata, group_server, false, true); // Check if the target Cluster is invalidated in a clusterset if (cluster->impl()->is_cluster_set_member()) cluster->impl()->check_and_get_cluster_set_for_cluster(); return cluster; } catch (const shcore::Exception &e) { if (e.code() == SHERR_DBA_GROUP_HAS_NO_QUORUM) { throw shcore::Exception::runtime_error( get_function_name("getCluster") + ": Unable to find a cluster PRIMARY member from the active shell " "session because the cluster has too many UNREACHABLE members and " "no quorum is possible.\n" "Use " + get_function_name("getCluster") + "(null, {connectToPrimary:false}) to get a read-only cluster " "handle."); } else if (e.code() == SHERR_DBA_METADATA_MISSING) { // The Metadata does not contain this Cluster, however, the preconditions // checker verified the existence of the Cluster in the Metadata schema of // the current Shell session meaning: // // - The Cluster is ONLINE // - The Cluster belongs to a ClusterSet, however it's not the valid // PRIMARY Cluster // - The Cluster's Metadata is not the most up-to-date Metadata copy of // the ClusterSet hence it does not exist in that Metadata copy // // This means the Cluster has been removed from the ClusterSet Metadata so // we must check if the Cluster is part of a ClusterSet to print a // warning and obtain the Cluster object from its own Metadata. // If not, we must error out right away. if (target_member) { auto cluster = get_cluster(!cluster_name ? nullptr : cluster_name->c_str(), metadata_at_target, group_server); std::string cs_domain_name; // The correct MD thinks this cluster is a member, check if it thinks // it's a member itself if (metadata_at_target->check_cluster_set(target_member.get(), nullptr, &cs_domain_name)) { console->print_warning( "The Cluster '" + cluster->impl()->get_name() + "' appears to have been removed from the ClusterSet '" + cs_domain_name + "', however its own metadata copy wasn't properly updated during " "the removal"); // Mark that the cluster was already removed from the clusterset but // doesn't know about it yet cluster->impl()->set_cluster_set_remove_pending(true); return cluster; } } } else if (e.code() == SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_REPLICASET) { // we can inform to the user with an hint (the user called getCluster on a // replicat set) console->print_info( "No InnoDB Cluster found, did you meant to call " "dba.<<<getReplicaSet>>>()?"); throw e; } throw; } } std::shared_ptr<Cluster> Dba::get_cluster( const char *name, std::shared_ptr<MetadataStorage> metadata, std::shared_ptr<Instance> group_server, bool reboot_cluster, bool allow_invalidated) const { Cluster_metadata target_cm; Cluster_metadata group_server_cm; if (!metadata->get_cluster_for_server_uuid(group_server->get_uuid(), &group_server_cm)) { throw shcore::Exception("No cluster metadata found for " + group_server->get_uuid() + " of instance " + group_server->descr(), SHERR_DBA_METADATA_MISSING); } if (!name) { target_cm = group_server_cm; } else { if (!metadata->get_cluster_for_cluster_name(name, &target_cm, allow_invalidated)) { throw shcore::Exception( shcore::str_format("The cluster with the name '%s' does not exist.", name), SHERR_DBA_METADATA_MISSING); } } // check whether the instance we're connected to is a member of the named // group, otherwise connect to one if ((target_cm.group_name != group_server->get_group_name()) && !reboot_cluster) { log_debug( "Target instance '%s' not a member of '%s', opening new connection...", group_server->descr().c_str(), target_cm.cluster_name.c_str()); auto ipool = current_ipool(); group_server = ipool->connect_group_member(target_cm.group_name); metadata = std::make_shared<MetadataStorage>( Instance::connect(group_server->get_connection_options())); group_server->steal(); } auto cluster = std::make_shared<Cluster_impl>( target_cm, group_server, metadata, Cluster_availability::ONLINE); // Verify if the current session instance group_replication_group_name // value differs from the one registered in the Metadata for that instance if (!validate_cluster_group_name(*group_server, cluster->get_group_name())) { std::string nice_error = "Unable to get an InnoDB cluster handle. " "The instance '" + group_server->descr() + "' may belong to a different cluster from the one registered in the " "Metadata since the value of 'group_replication_group_name' does " "not match the one registered in the Metadata: possible split-brain " "scenario. Please retry while connected to another member of the " "cluster."; throw shcore::Exception::runtime_error(nice_error); } return std::make_shared<mysqlsh::dba::Cluster>(cluster); } REGISTER_HELP_FUNCTION(createCluster, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CREATECLUSTER, R"*( Creates a MySQL InnoDB cluster. @param name An identifier for the cluster to be created. @param options Optional dictionary with additional parameters described below. @returns The created cluster object. Creates a MySQL InnoDB cluster taking as seed instance the server the shell is currently connected to. The options dictionary can contain the following values: @li disableClone: boolean value used to disable the clone usage on the cluster. @li gtidSetIsComplete: boolean value which indicates whether the GTID set of the seed instance corresponds to all transactions executed. Default is false. @li multiPrimary: boolean value used to define an InnoDB cluster with multiple writable instances. @li force: boolean, confirms that the multiPrimary option must be applied and/or the operation must proceed even if unmanaged replication channels were detected. ${OPT_INTERACTIVE} @li adoptFromGR: boolean value used to create the InnoDB cluster based on existing replication group. ${CLUSTER_OPT_MEMBER_SSL_MODE} ${OPT_MEMBER_AUTH_TYPE} ${OPT_CERT_ISSUER} ${OPT_CERT_SUBJECT} ${CLUSTER_OPT_IP_WHITELIST} ${CLUSTER_OPT_IP_ALLOWLIST} @li groupName: string value with the Group Replication group name UUID to be used instead of the automatically generated one. ${CLUSTER_OPT_LOCAL_ADDRESS} @li groupSeeds: string value with a comma-separated list of the Group Replication peer addresses to be used instead of the automatically generated one. Deprecated and ignored. @li manualStartOnBoot: boolean (default false). If false, Group Replication in cluster instances will automatically start and rejoin when MySQL starts, otherwise it must be started manually. @li replicationAllowedHost: string value to use as the host name part of internal replication accounts (i.e. 'mysql_innodb_cluster_###'@'hostname'). Default is %. It must be possible for any member of the Cluster to connect to any other member using accounts with this hostname value. ${CLUSTER_OPT_EXIT_STATE_ACTION} ${CLUSTER_OPT_MEMBER_WEIGHT} ${CLUSTER_OPT_CONSISTENCY} ${CLUSTER_OPT_FAILOVER_CONSISTENCY} ${CLUSTER_OPT_EXPEL_TIMEOUT} ${CLUSTER_OPT_AUTO_REJOIN_TRIES} @li clearReadOnly: boolean value used to confirm that super_read_only must be disabled. Deprecated. @li multiMaster: boolean value used to define an InnoDB cluster with multiple writable instances. Deprecated. ${CLUSTER_OPT_COMM_STACK} ${CLUSTER_OPT_TRANSACTION_SIZE_LIMIT} ${CLUSTER_OPT_PAXOS_SINGLE_LEADER} An InnoDB cluster may be setup in two ways: @li Single Primary: One member of the cluster allows write operations while the rest are read-only secondaries. @li Multi Primary: All the members in the cluster allow both read and write operations. Note that Multi-Primary mode has limitations about what can be safely executed. Make sure to read the MySQL documentation for Group Replication and be aware of what is and is not safely executable in such setups. By default this function creates a Single Primary cluster. Use the multiPrimary option set to true if a Multi Primary cluster is required. <b>Options</b> interactive controls whether prompts are shown for MySQL passwords, confirmations and handling of cases where user feedback may be required. Defaults to true, unless the Shell is started with the --no-wizards option. disableClone should be set to true if built-in clone support should be completely disabled, even in instances where that is supported. Built-in clone support is available starting with MySQL 8.0.17 and allows automatically provisioning new cluster members by copying state from an existing cluster member. Note that clone will completely delete all data in the instance being added to the cluster. gtidSetIsComplete is used to indicate that GTIDs have been always enabled at the cluster seed instance and that GTID_EXECUTED contains all transactions ever executed. It must be left as false if data was inserted or modified while GTIDs were disabled or if RESET MASTER was executed. This flag affects how cluster.<<<addInstance>>>() decides which recovery methods are safe to use. Distributed recovery based on replaying the transaction history is only assumed to be safe if the transaction history is known to be complete, otherwise cluster members could end up with incomplete data sets. adoptFromGR allows creating an InnoDB cluster from an existing unmanaged Group Replication setup, enabling use of MySQL Router and the shell AdminAPI for managing it. ${CLUSTER_OPT_MEMBER_SSL_MODE_DETAIL} ${OPT_MEMBER_AUTH_TYPE_EXTRA} When CERT_ISSUER or CERT_SUBJECT are used, the server's own certificate is used as its client certificate when authenticating replication channels with peer servers. memberSslMode must be at least REQUIRED, although VERIFY_CA or VERIFY_IDENTITY are recommended for additional security. ${CLUSTER_OPT_IP_ALLOWLIST_EXTRA} The groupName and localAddress are advanced options and their usage is discouraged since incorrect values can lead to Group Replication errors. The value for groupName is used to set the Group Replication system variable 'group_replication_group_name'. ${CLUSTER_OPT_LOCAL_ADDRESS_EXTRA} The groupSeeds option is deprecated as of MySQL Shell 8.0.28 and is ignored. 'group_replication_group_seeds' is automatically set based on the current topology. ${CLUSTER_OPT_EXIT_STATE_ACTION_DETAIL} ${CLUSTER_OPT_CONSISTENCY_DETAIL} ${CLUSTER_OPT_EXIT_STATE_ACTION_EXTRA} ${CLUSTER_OPT_MEMBER_WEIGHT_DETAIL_EXTRA} ${CLUSTER_OPT_CONSISTENCY_EXTRA} ${CLUSTER_OPT_EXPEL_TIMEOUT_EXTRA} ${CLUSTER_OPT_AUTO_REJOIN_TRIES_EXTRA} ${CLUSTER_OPT_COMM_STACK_EXTRA} ${CLUSTER_OPT_TRANSACTION_SIZE_LIMIT_EXTRA} ${CLUSTER_OPT_PAXOS_SINGLE_LEADER_EXTRA} @attention The clearReadOnly option will be removed in a future release. @attention The multiMaster option will be removed in a future release. Please use the multiPrimary option instead. @attention The failoverConsistency option will be removed in a future release. Please use the consistency option instead. @attention The ipWhitelist option will be removed in a future release. Please use the ipAllowlist option instead. @attention The groupSeeds option will be removed in a future release. @attention The interactive option will be removed in a future release. )*"); /** * $(DBA_CREATECLUSTER_BRIEF) * * $(DBA_CREATECLUSTER) */ #if DOXYGEN_JS Cluster Dba::createCluster(String name, Dictionary options) {} #elif DOXYGEN_PY Cluster Dba::create_cluster(str name, dict options) {} #endif shcore::Value Dba::create_cluster( const std::string &cluster_name, const shcore::Option_pack_ref<Create_cluster_options> &options) { std::string instance_label; std::shared_ptr<Instance> group_server; std::shared_ptr<MetadataStorage> metadata; // We're in createCluster(), so there's no metadata yet, but the metadata // object can already exist now to hold a session // connect_to_primary must be false, because we don't have a cluster yet // and this will be the seed instance anyway. If we start storing metadata // outside the managed cluster, then this will have to be updated. connect_to_target_group({}, &metadata, &group_server, false); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(group_server->get_connection_options())); // Check preconditions. Cluster_check_info state; try { state = check_function_preconditions("Dba.createCluster", metadata, group_server); ipool->set_metadata(metadata); } catch (const shcore::Exception &e) { if (e.code() == SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_CLUSTER) { /* * For V1.0 we only support one single Cluster. That one shall be the * default Cluster. We must check if there's already a Default Cluster * assigned, and if so thrown an exception. And we must check if there's * already one Cluster on the MD and if so assign it to Default */ std::string nice_error = get_function_name("createCluster") + ": Unable to create cluster. The instance '" + group_server->descr() + "' already belongs to an InnoDB cluster. Use " "dba." + get_function_name("getCluster", false) + "() to access it."; throw shcore::Exception::runtime_error(nice_error); } else if (e.code() == SHERR_DBA_BADARG_INSTANCE_NOT_ONLINE) { // BUG#29271400: // createCluster() should not be allowed in instance with metadata std::string nice_error = "dba.<<<createCluster>>>: Unable to create cluster. The instance '" + group_server->descr() + "' has a populated Metadata schema and belongs to that Metadata. Use " "either dba.<<<dropMetadataSchema>>>() to drop the schema, or " "dba.<<<rebootClusterFromCompleteOutage>>>() to reboot the cluster " "from complete outage."; throw shcore::Exception::runtime_error(shcore::str_subvars(nice_error)); } else { throw; } } catch (const shcore::Error &dberr) { throw shcore::Exception::mysql_error_with_code(dberr.what(), dberr.code()); } // put an exclusive lock on the target instance auto i_lock = group_server->get_lock_exclusive(); // Create the cluster { // Create the add_instance command and execute it. Create_cluster op_create_cluster(group_server, cluster_name, *options); // Always execute finish when leaving "try catch". shcore::on_leave_scope finally( [&op_create_cluster]() { op_create_cluster.finish(); }); // Prepare the add_instance command execution (validations). op_create_cluster.prepare(); // Execute add_instance operations. return op_create_cluster.execute(); } } namespace { void ensure_not_in_cluster_set(const MetadataStorage &metadata, const Instance &instance) { Cluster_metadata cluster_md; if (metadata.get_cluster_for_server_uuid(instance.get_uuid(), &cluster_md) && !cluster_md.cluster_set_id.empty()) { std::vector<Cluster_set_member_metadata> cs_members; if (metadata.get_cluster_set(cluster_md.cluster_set_id, false, nullptr, &cs_members) && cs_members.size() > 1) { throw shcore::Exception::runtime_error( "Cannot drop metadata for the Cluster because it is part of a " "ClusterSet with other Clusters."); } } } } // namespace REGISTER_HELP_FUNCTION(dropMetadataSchema, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_DROPMETADATASCHEMA, R"*( Drops the Metadata Schema. @param options Dictionary containing an option to confirm the drop operation. @returns Nothing. The options dictionary may contain the following options: @li force: boolean, confirms that the drop operation must be executed. @li clearReadOnly: boolean value used to confirm that super_read_only must be disabled. Deprecated and default value is true. @attention The clearReadOnly option will be removed in a future release and it's no longer needed, super_read_only is automatically cleared. )*"); /** * $(DBA_DROPMETADATASCHEMA_BRIEF) * * $(DBA_DROPMETADATASCHEMA) */ #if DOXYGEN_JS Undefined Dba::dropMetadataSchema(Dictionary options) {} #elif DOXYGEN_PY None Dba::drop_metadata_schema(dict options) {} #endif void Dba::drop_metadata_schema( const shcore::Option_pack_ref<Drop_metadata_schema_options> &options) { auto instance = connect_to_target_member(); std::shared_ptr<MetadataStorage> metadata; // An open session is required if (!instance) { throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } metadata = std::make_shared<MetadataStorage>(instance); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(instance->get_connection_options())); auto primary = check_preconditions("Dba.dropMetadataSchema", instance); if (primary) { metadata = std::make_shared<MetadataStorage>(primary); ipool->set_metadata(metadata); } bool interactive = current_shell_options()->get().wizards; instance = metadata->get_md_server(); auto console = current_console(); // Can't dissolve if in a cluster_set ensure_not_in_cluster_set(*metadata, *instance); std::optional<bool> force = options->force; if (!force.has_value() && interactive && console->confirm("Are you sure you want to remove the Metadata?", mysqlsh::Prompt_answer::NO) == mysqlsh::Prompt_answer::YES) { force = true; } if (force.value_or(false)) { // Check if super_read_only is turned off and disable it if required // NOTE: this is left for last to avoid setting super_read_only to true // and right before some execution failure of the command leaving the // instance in an incorrect state validate_super_read_only(*instance, options->clear_read_only); metadata::uninstall(instance); if (interactive) { console->print_info("Metadata Schema successfully removed."); } } else { if (interactive) { console->print_info("No changes made to the Metadata Schema."); } else { throw shcore::Exception::runtime_error( "No operation executed, use the 'force' option"); } } } REGISTER_HELP_FUNCTION(checkInstanceConfiguration, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CHECKINSTANCECONFIGURATION, R"*( Validates an instance for MySQL InnoDB Cluster usage. @param instance An instance definition. @param options Optional data for the operation. @returns A descriptive text of the operation result. This function reviews the instance configuration to identify if it is valid for usage with group replication. Use this to check for possible configuration issues on MySQL instances before creating a cluster with them or adding them to an existing cluster. The instance definition is the connection data for the instance. ${TOPIC_CONNECTION_MORE_INFO} The options dictionary may contain the following options: @li mycnfPath: Optional path to the MySQL configuration file for the instance. Alias for verifyMyCnf @li verifyMyCnf: Optional path to the MySQL configuration file for the instance. If this option is given, the configuration file will be verified for the expected option values, in addition to the global MySQL system variables. @li password: The password to get connected to the instance. ${OPT_INTERACTIVE} The connection password may be contained on the instance definition, however, it can be overwritten if it is specified on the options. The returned descriptive text of the operation result indicates whether the instance is valid for InnoDB Cluster usage or not. If not, a table containing the following information is presented: @li Variable: the invalid configuration variable. @li Current Value: the current value for the invalid configuration variable. @li Required Value: the required value for the configuration variable. @li Note: the action to be taken. The note can be one of the following: @li Update the config file and update or restart the server variable. @li Update the config file and restart the server. @li Update the config file. @li Update the server variable. @li Restart the server. @attention The interactive option will be removed in a future release. @attention The password option will be removed in a future release. )*"); /** * $(DBA_CHECKINSTANCECONFIGURATION_BRIEF) * * $(DBA_CHECKINSTANCECONFIGURATION) */ #if DOXYGEN_JS JSON Dba::checkInstanceConfiguration(InstanceDef instance, Dictionary options) { } #elif DOXYGEN_PY JSON Dba::check_instance_configuration(InstanceDef instance, dict options) {} #endif shcore::Value Dba::check_instance_configuration( const mysqlshdk::utils::nullable<Connection_options> &instance_def_, const shcore::Option_pack_ref<Check_instance_configuration_options> &options) { auto instance_def = instance_def_; const auto has_co = instance_def && instance_def->has_data(); if (has_co && !options->password.is_null()) { auto connection_options = instance_def.operator->(); connection_options->set_password(*options->password); } shcore::Value ret_val; std::shared_ptr<Instance> instance; std::shared_ptr<MetadataStorage> metadata; // Establish the session to the target instance if (has_co) { if (instance_def->has_ssh_options()) throw shcore::Exception::logic_error(INNODB_SSH_NOT_SUPPORTED); instance = Instance::connect(*instance_def, options->interactive()); } else { instance = connect_to_target_member(); } if (!instance) { throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } metadata = std::make_shared<MetadataStorage>(instance); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(instance->get_connection_options())); check_preconditions("Dba.checkInstanceConfiguration", instance); ipool->set_metadata(metadata); // Get the Connection_options Connection_options coptions = instance->get_connection_options(); // Close the session instance->close_session(); // Call the API Check_instance op_check_instance{coptions, options->mycnf_path}; op_check_instance.prepare(); ret_val = op_check_instance.execute(); op_check_instance.finish(); return ret_val; } // ----------------------------------------------------------------------------- REGISTER_HELP_FUNCTION(getReplicaSet, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_GETREPLICASET, R"*( Returns an object representing a ReplicaSet. The returned object is identical to the one returned by <<<createReplicaSet>>>() and can be used to manage the replicaset. The function will work regardless of whether the target instance is a PRIMARY or a SECONDARY, but its copy of the metadata is expected to be up-to-date. This function will also work if the PRIMARY is unreachable or unavailable, although replicaset change operations will not be possible, except for <<<forcePrimaryInstance>>>(). )*"); /** * $(DBA_GETREPLICASET_BRIEF) * * $(DBA_GETREPLICASET) */ #if DOXYGEN_JS ReplicaSet Dba::getReplicaSet() {} #elif DOXYGEN_PY ReplicaSet Dba::get_replica_set() {} #endif std::shared_ptr<ReplicaSet> Dba::get_replica_set() { const auto console = mysqlsh::current_console(); const auto target_server = connect_to_target_member(); std::shared_ptr<MetadataStorage> metadata; if (!target_server) { throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } metadata = std::make_shared<MetadataStorage>(target_server); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(target_server->get_connection_options())); check_preconditions("Dba.getReplicaSet", target_server); ipool->set_metadata(metadata); const auto rs = get_replica_set(metadata, target_server); console->print_info("You are connected to a member of replicaset '" + rs->get_name() + "'."); return std::make_shared<ReplicaSet>(rs); } std::shared_ptr<Replica_set_impl> Dba::get_replica_set( const std::shared_ptr<MetadataStorage> &metadata, const std::shared_ptr<Instance> &target_server) { Cluster_metadata target_server_cm; if (!metadata->get_cluster_for_server_uuid(target_server->get_uuid(), &target_server_cm)) { throw shcore::Exception("No metadata found for " + target_server->descr() + " (" + target_server->get_uuid() + ")", SHERR_DBA_METADATA_MISSING); } return std::make_shared<Replica_set_impl>(target_server_cm, target_server, metadata); } REGISTER_HELP_FUNCTION(createReplicaSet, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CREATEREPLICASET, R"*( Creates a MySQL InnoDB ReplicaSet. @param name An identifier for the replicaset to be created. @param options Optional dictionary with additional parameters described below. @returns The created replicaset object. This function will create a managed ReplicaSet using MySQL asynchronous replication, as opposed to Group Replication. The MySQL instance the shell is connected to will be the initial PRIMARY of the ReplicaSet. The replication channel will have TLS encryption enabled by default. The function will perform several checks to ensure the instance state and configuration are compatible with a managed ReplicaSet and if so, a metadata schema will be initialized there. New replica instances can be added through the <<<addInstance>>>() function of the returned replicaset object. Status of the instances and their replication channels can be inspected with <<<status>>>(). <b>InnoDB ReplicaSets</b> A replicaset allows managing a GTID-based MySQL replication setup, in a single PRIMARY/multiple SECONDARY topology. A replicaset has several limitations compared to a InnoDB cluster and thus, it is recommended that InnoDB clusters be preferred unless not possible. Generally, a ReplicaSet on its own does not provide High Availability. Among its limitations are: @li No automatic failover @li No protection against inconsistencies or partial data loss in a crash <b>Pre-Requisites</b> The following is a non-exhaustive list of requirements for managed ReplicaSets. The dba.<<<configureInstance>>>() command can be used to make necessary configuration changes automatically. @li MySQL 8.0 or newer required @li Statement Based Replication (SBR) is unsupported, only Row Based Replication @li GTIDs required @li Replication filters are not allowed @li All instances in the replicaset must be managed @li Unmanaged replication channels are not allowed in any instance <b>Adopting an Existing Topology</b> Existing asynchronous setups can be managed by calling this function with the adoptFromAR option. The topology will be automatically scanned and validated, starting from the instance the shell is connected to, and all instances that are part of the topology will be automatically added to the replicaset. The only changes made by this function to an adopted replicaset are the creation of the metadata schema. Existing replication channels will not be changed during adoption, although they will be changed during PRIMARY switch operations. However, it is only possible to manage setups that use supported configurations and topology. Configuration of all instances will be checked during adoption, to ensure they are compatible. All replication channels must be active and their transaction sets as verified through GTID sets must be consistent. The data set of all instances are expected to be identical, but is not verified. <b>Options</b> The options dictionary can contain the following values: @li adoptFromAR: boolean value used to create the replicaset based on an existing asynchronous replication setup. @li instanceLabel: string a name to identify the target instance. Defaults to hostname:port @li dryRun: boolean if true, all validations and steps for creating a replica set are executed, but no changes are actually made. An exception will be thrown when finished. @li gtidSetIsComplete: boolean value which indicates whether the GTID set of the seed instance corresponds to all transactions executed. Default is false. @li replicationAllowedHost: string value to use as the host name part of internal replication accounts (i.e. 'mysql_innodb_rs_###'@'hostname'). Default is %. It must be possible for any member of the ReplicaSet to connect to any other member using accounts with this hostname value. ${OPT_INTERACTIVE} ${OPT_MEMBER_AUTH_TYPE} ${OPT_CERT_ISSUER} ${OPT_CERT_SUBJECT} @li replicationSslMode: SSL mode to use to configure the asynchronous replication channels of the replicaset. The replicationSslMode option supports the following values: @li DISABLED: TLS encryption is disabled for the replication channel. @li REQUIRED: TLS encryption is enabled for the replication channel. @li VERIFY_CA: like REQUIRED, but additionally verify the peer server TLS certificate against the configured Certificate Authority (CA) certificates. @li VERIFY_IDENTITY: like VERIFY_CA, but additionally verify that the peer server certificate matches the host to which the connection is attempted. @li AUTO: TLS encryption will be enabled if supported by the instance, otherwise disabled. If replicationSslMode is not specified AUTO will be used by default. ${OPT_MEMBER_AUTH_TYPE_EXTRA} When CERT_ISSUER or CERT_SUBJECT are used, the server's own certificate is used as its client certificate when authenticating replication channels with peer servers. replicationSslMode must be at least REQUIRED, although VERIFY_CA or VERIFY_IDENTITY are recommended for additional security. @attention The interactive option will be removed in a future release. )*"); /** * $(DBA_CREATEREPLICASET_BRIEF) * * $(DBA_CREATEREPLICASET) */ #if DOXYGEN_JS ReplicaSet Dba::createReplicaSet(String name, Dictionary options) {} #elif DOXYGEN_PY ReplicaSet Dba::create_replica_set(str name, dict options) {} #endif shcore::Value Dba::create_replica_set( const std::string &full_rs_name, const shcore::Option_pack_ref<Create_replicaset_options> &options) { std::shared_ptr<Instance> target_server = connect_to_target_member(); try { std::shared_ptr<MetadataStorage> metadata; if (target_server) metadata = std::make_shared<MetadataStorage>(target_server); check_function_preconditions("Dba.createReplicaSet", metadata, target_server); } catch (const shcore::Exception &e) { if (e.code() == SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_CLUSTER) { throw shcore::Exception("Unable to create replicaset. The instance '" + target_server->descr() + "' already belongs to an InnoDB cluster. Use " "dba.<<<getCluster>>>() to access it.", e.code()); } else if (e.code() == SHERR_DBA_BADARG_INSTANCE_MANAGED_IN_REPLICASET) { throw shcore::Exception("Unable to create replicaset. The instance '" + target_server->descr() + "' already belongs to a replicaset. Use " "dba.<<<getReplicaSet>>>() to access it.", e.code()); } else { throw; } } catch (const shcore::Error &dberr) { throw shcore::Exception::mysql_error_with_code(dberr.what(), dberr.code()); } // Init the connection pool Scoped_instance_pool ipool(current_shell_options()->get().wizards, target_server->get_connection_options()); auto topology_type = Global_topology_type::SINGLE_PRIMARY_TREE; auto cluster = Replica_set_impl::create(full_rs_name, topology_type, target_server, *options); auto console = mysqlsh::current_console(); console->print_info("ReplicaSet object successfully created for " + target_server->descr() + ".\nUse rs.<<<addInstance>>>() to add more " "asynchronously replicated instances to this replicaset " "and rs.<<<status>>>() to check its status."); console->print_info(); return shcore::Value(std::make_shared<ReplicaSet>(cluster)); } REGISTER_HELP_FUNCTION(getClusterSet, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_GETCLUSTERSET, R"*( Returns an object representing a ClusterSet. @returns The ClusterSet object to which the current session belongs to. The returned object is identical to the one returned by <<<createClusterSet>>>() and can be used to manage the ClusterSet. The function will work regardless of whether the active session is established to an instance that belongs to a PRIMARY or a REPLICA Cluster, but its copy of the metadata is expected to be up-to-date. This function will also work if the PRIMARY Cluster is unreachable or unavailable, although ClusterSet change operations will not be possible, except for failover with <<<forcePrimaryCluster>>>(). )*"); /** * $(DBA_GETCLUSTERSET_BRIEF) * * $(DBA_GETCLUSTERSET) */ #if DOXYGEN_JS ClusterSet Dba::getClusterSet() {} #elif DOXYGEN_PY ClusterSet Dba::get_cluster_set() {} #endif std::shared_ptr<ClusterSet> Dba::get_cluster_set() { const auto target_server = connect_to_target_member(); std::shared_ptr<MetadataStorage> metadata; if (target_server) { metadata = std::make_shared<MetadataStorage>( Instance::connect(target_server->get_connection_options())); } check_function_preconditions("Dba.getClusterSet", metadata, target_server); // Init the connection pool Scoped_instance_pool ipool(metadata, current_shell_options()->get().wizards, target_server->get_connection_options()); // Validate if the target instance is a member of an InnoDB Cluster Cluster_metadata cluster_md; if (!metadata->get_cluster_for_server_uuid(target_server->get_uuid(), &cluster_md)) { throw shcore::Exception("No metadata found for " + target_server->descr() + " (" + target_server->get_uuid() + ")", SHERR_DBA_METADATA_MISSING); } auto cluster = get_cluster(cluster_md.cluster_name.c_str(), metadata, target_server, false, true); auto cs = cluster->impl()->check_and_get_cluster_set_for_cluster(); cluster->impl()->invalidate_cluster_set_metadata_cache(); if (!cs || !cluster->impl()->is_cluster_set_member() || cluster->impl()->is_cluster_set_remove_pending()) { throw shcore::Exception("Cluster is not part of a ClusterSet", SHERR_DBA_CLUSTER_DOES_NOT_BELONG_TO_CLUSTERSET); } return std::make_shared<mysqlsh::dba::ClusterSet>(cs); } // ----- void Dba::exec_instance_op(const std::string &function, int port, const std::string &sandbox_dir, const std::string &password) { validate_port(port, "port"); bool interactive = current_shell_options()->get().wizards; // Prints the operation introductory message auto console = mysqlsh::current_console(); shcore::Value::Array_type_ref errors; if (interactive) { console->print_info(); console->print_info(sandbox::Operations_text[function].progressive + " MySQL instance..."); } int rc = 0; if (function == "delete") { rc = _provisioning_interface.delete_sandbox(port, sandbox_dir, false, &errors); } else if (function == "kill") { rc = _provisioning_interface.kill_sandbox(port, sandbox_dir, &errors); } else if (function == "stop") { rc = _provisioning_interface.stop_sandbox(port, sandbox_dir, password, &errors); } else if (function == "start") { rc = _provisioning_interface.start_sandbox(port, sandbox_dir, &errors); } if (rc != 0) { throw_instance_op_error(errors); } else if (interactive) { console->print_info(); console->print_info("Instance localhost:" + std::to_string(port) + " successfully " + sandbox::Operations_text[function].past + "."); console->print_info(); } } REGISTER_HELP_FUNCTION(deploySandboxInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_DEPLOYSANDBOXINSTANCE, R"*( Creates a new MySQL Server instance on localhost. @param port The port where the new instance will listen for connections. @param options Optional dictionary with options affecting the new deployed instance. @returns Nothing. This function will deploy a new MySQL Server instance, the result may be affected by the provided options: @li portx: port where the new instance will listen for X Protocol connections. @li sandboxDir: path where the new instance will be deployed. @li password: password for the MySQL root user on the new instance. @li allowRootFrom: create remote root account, restricted to the given address pattern (default: %). @li ignoreSslError: ignore errors when adding SSL support for the new instance, by default: true. @li mysqldOptions: list of MySQL configuration options to write to the my.cnf file, as option=value strings. If the portx option is not specified, it will be automatically calculated as 10 times the value of the provided MySQL port. The password option specifies the MySQL root password on the new instance. The sandboxDir must be an existing folder where the new instance will be deployed. If not specified the new instance will be deployed at: ~/mysql-sandboxes on Unix-like systems or @%userprofile@%\\MySQL\\mysql-sandboxes on Windows systems. SSL support is added by default if not already available for the new instance, but if it fails to be added then the error is ignored. Set the ignoreSslError option to false to ensure the new instance is deployed with SSL support. )*"); /** * $(DBA_DEPLOYSANDBOXINSTANCE_BRIEF) * * $(DBA_DEPLOYSANDBOXINSTANCE) */ #if DOXYGEN_JS Instance Dba::deploySandboxInstance(Integer port, Dictionary options) {} #elif DOXYGEN_PY Instance Dba::deploy_sandbox_instance(int port, dict options) {} #endif void Dba::deploy_sandbox_instance( int port, const shcore::Option_pack_ref<Deploy_sandbox_options> &options) { validate_port(port, "port"); const Deploy_sandbox_options &opts = *options; if (!opts.xport.is_null()) { validate_port(*opts.xport, "portx"); } mysqlshdk::null_string password = opts.password; bool interactive = current_shell_options()->get().wizards; auto console = mysqlsh::current_console(); std::string path = shcore::path::join_path(options->sandbox_dir, std::to_string(port)); if (interactive) { console->print_info( "A new MySQL sandbox instance will be created on this host in \n" + path + "\n\nWarning: Sandbox instances are only suitable for deploying and " "\nrunning on your local machine for testing purposes and are not " "\naccessible from external networks.\n"); if (password.is_null()) { std::string answer; if (console->prompt_password( "Please enter a MySQL root password for the new instance: ", &answer) == shcore::Prompt_result::Ok) { password = answer; } else { return; } } } if (password.is_null()) { throw shcore::Exception::argument_error( "Missing root password for the deployed instance"); } if (interactive) { console->print_info(); console->print_info("Deploying new MySQL instance..."); } shcore::Array_t errors; int rc = _provisioning_interface.create_sandbox( port, opts.xport.get_safe(0), options->sandbox_dir, *password, shcore::Value(opts.mysqld_options), true, opts.ignore_ssl_error, 0, "", &errors); if (rc != 0) throw_instance_op_error(errors); // Create root@<allowRootFrom> account // Default: // allowRootFrom: % // Valid values: // allowRootFrom: address // allowRootFrom: % if (!opts.allow_root_from.empty()) { std::string uri = "root@localhost:" + std::to_string(port); mysqlshdk::db::Connection_options instance_def(uri); instance_def.set_password(*password); std::shared_ptr<Instance> instance; try { instance = Instance::connect(instance_def); } catch (const shcore::Error &e) { if (e.code() != CR_SSL_CONNECTION_ERROR) throw; // we can get SSL connect errors when connecting to older servers that // don't support TLSv1.2 auto ssl_opts = instance_def.get_ssl_options(); ssl_opts.set_mode(mysqlshdk::db::Ssl_mode::Disabled); instance_def.set_ssl_options(ssl_opts); instance = Instance::connect(instance_def); } log_info("Creating root@%s account for sandbox %i", opts.allow_root_from.c_str(), port); instance->execute("SET sql_log_bin = 0"); shcore::Scoped_callback enableLogBin( [&instance]() { instance->execute("SET sql_log_bin = 1"); }); { std::string pwd; if (instance_def.has_password()) pwd = instance_def.get_password(); shcore::sqlstring create_user( "CREATE USER root@? IDENTIFIED BY /*((*/ ? /*))*/", 0); create_user << opts.allow_root_from << pwd; create_user.done(); instance->execute(create_user); } { shcore::sqlstring grant("GRANT ALL ON *.* TO root@? WITH GRANT OPTION", 0); grant << opts.allow_root_from; grant.done(); instance->execute(grant); } } log_warning( "Sandbox instances are only suitable for deploying and running on " "your local machine for testing purposes and are not accessible " "from external networks."); if (current_shell_options()->get().wizards) { console->print_info(); console->print_info("Instance localhost:" + std::to_string(port) + " successfully deployed and started."); console->print_info( "Use shell.connect('root@localhost:" + std::to_string(port) + "') to connect to the instance."); console->print_info(); } } REGISTER_HELP_FUNCTION(deleteSandboxInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_DELETESANDBOXINSTANCE, R"*( Deletes an existing MySQL Server instance on localhost. @param port The port of the instance to be deleted. @param options Optional dictionary with options that modify the way this function is executed. @returns Nothing. This function will delete an existing MySQL Server instance on the local host. The following options affect the result: @li sandboxDir: path where the instance is located. The sandboxDir must be the one where the MySQL instance was deployed. If not specified it will use: ~/mysql-sandboxes on Unix-like systems or @%userprofile@%\\MySQL\\mysql-sandboxes on Windows systems. If the instance is not located on the used path an error will occur. )*"); /** * $(DBA_DELETESANDBOXINSTANCE_BRIEF) * * $(DBA_DELETESANDBOXINSTANCE) */ #if DOXYGEN_JS Undefined Dba::deleteSandboxInstance(Integer port, Dictionary options) {} #elif DOXYGEN_PY None Dba::delete_sandbox_instance(int port, dict options) {} #endif void Dba::delete_sandbox_instance( int port, const shcore::Option_pack_ref<Common_sandbox_options> &options) { exec_instance_op("delete", port, options->sandbox_dir); } REGISTER_HELP_FUNCTION(killSandboxInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_KILLSANDBOXINSTANCE, R"*( Kills a running MySQL Server instance on localhost. @param port The port of the instance to be killed. @param options Optional dictionary with options affecting the result. @returns Nothing. This function will kill the process of a running MySQL Server instance on the local host. The following options affect the result: @li sandboxDir: path where the instance is located. The sandboxDir must be the one where the MySQL instance was deployed. If not specified it will use: ~/mysql-sandboxes on Unix-like systems or @%userprofile@%\\MySQL\\mysql-sandboxes on Windows systems. If the instance is not located on the used path an error will occur. )*"); /** * $(DBA_KILLSANDBOXINSTANCE_BRIEF) * * $(DBA_KILLSANDBOXINSTANCE) */ #if DOXYGEN_JS Undefined Dba::killSandboxInstance(Integer port, Dictionary options) {} #elif DOXYGEN_PY None Dba::kill_sandbox_instance(int port, dict options) {} #endif void Dba::kill_sandbox_instance( int port, const shcore::Option_pack_ref<Common_sandbox_options> &options) { exec_instance_op("kill", port, options->sandbox_dir); } REGISTER_HELP_FUNCTION(stopSandboxInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_STOPSANDBOXINSTANCE, R"*( Stops a running MySQL Server instance on localhost. @param port The port of the instance to be stopped. @param options Optional dictionary with options affecting the result. @returns Nothing. This function will gracefully stop a running MySQL Server instance on the local host. The following options affect the result: @li sandboxDir: path where the instance is located. @li password: password for the MySQL root user on the instance. The sandboxDir must be the one where the MySQL instance was deployed. If not specified it will use: ~/mysql-sandboxes on Unix-like systems or @%userprofile@%\\MySQL\\mysql-sandboxes on Windows systems. If the instance is not located on the used path an error will occur. )*"); /** * $(DBA_STOPSANDBOXINSTANCE_BRIEF) * * $(DBA_STOPSANDBOXINSTANCE) */ #if DOXYGEN_JS Undefined Dba::stopSandboxInstance(Integer port, Dictionary options) {} #elif DOXYGEN_PY None Dba::stop_sandbox_instance(int port, dict options) {} #endif void Dba::stop_sandbox_instance( int port, const shcore::Option_pack_ref<Stop_sandbox_options> &options) { std::string sandbox_dir; validate_port(port, "port"); mysqlshdk::null_string password = options->password; bool interactive = current_shell_options()->get().wizards; auto console = mysqlsh::current_console(); std::string path = shcore::path::join_path(sandbox_dir, std::to_string(port)); if (interactive) { console->print_info("The MySQL sandbox instance on this host in \n" + path + " will be " + sandbox::Operations_text["stop"].past + "\n"); if (password.is_null()) { std::string answer; if (console->prompt_password( "Please enter the MySQL root password for the instance " "'localhost:" + std::to_string(port) + "': ", &answer) == shcore::Prompt_result::Ok) { password = answer; } else { return; } } } if (password.is_null()) { throw shcore::Exception::argument_error( "Missing root password for the instance"); } auto instance = std::make_shared<Instance>(get_active_shell_session()); if (instance && instance->get_session()) { auto connection_opts = instance->get_connection_options(); if ((connection_opts.get_host() == "localhost" || connection_opts.get_host() == "127.0.0.1") && connection_opts.get_port() == port) { if (interactive) { console->print_info( "The active session is established to the sandbox being stopped " "so it's going to be closed."); } instance->close_session(); } } exec_instance_op("stop", port, options->sandbox_dir, *password); } REGISTER_HELP_FUNCTION(startSandboxInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_STARTSANDBOXINSTANCE, R"*( Starts an existing MySQL Server instance on localhost. @param port The port where the instance listens for MySQL connections. @param options Optional dictionary with options affecting the result. @returns Nothing. This function will start an existing MySQL Server instance on the local host. The following options affect the result: @li sandboxDir: path where the instance is located. The sandboxDir must be the one where the MySQL instance was deployed. If not specified it will use: ~/mysql-sandboxes on Unix-like systems or @%userprofile@%\\MySQL\\mysql-sandboxes on Windows systems. If the instance is not located on the used path an error will occur. )*"); /** * $(DBA_STARTSANDBOXINSTANCE_BRIEF) * * $(DBA_STARTSANDBOXINSTANCE) */ #if DOXYGEN_JS Undefined Dba::startSandboxInstance(Integer port, Dictionary options) {} #elif DOXYGEN_PY None Dba::start_sandbox_instance(int port, dict options) {} #endif void Dba::start_sandbox_instance( int port, const shcore::Option_pack_ref<Common_sandbox_options> &options) { exec_instance_op("start", port, options->sandbox_dir); } void Dba::do_configure_instance( const mysqlshdk::db::Connection_options &instance_def_, const Configure_instance_options &options, Cluster_type purpose) { shcore::Value ret_val; auto instance_def = instance_def_; if (instance_def.has_data() && !options.password.is_null()) { instance_def.set_password(*options.password); } // Establish the session to the target instance std::shared_ptr<Instance> target_instance; if (instance_def.has_data()) { if (instance_def.has_ssh_options()) throw shcore::Exception::logic_error(INNODB_SSH_NOT_SUPPORTED); target_instance = Instance::connect(instance_def, options.interactive()); } else { target_instance = connect_to_target_member(); } if (!target_instance) { throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } auto metadata = std::make_shared<MetadataStorage>(target_instance); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(target_instance->get_connection_options())); // Check the function preconditions Cluster_check_info state; if (options.cluster_type == Cluster_type::ASYNC_REPLICATION) { state = check_function_preconditions("Dba.configureReplicaSetInstance", metadata, target_instance); } else { state = check_function_preconditions( options.local ? "Dba.configureLocalInstance" : "Dba.configureInstance", metadata, target_instance); } ipool->set_metadata(metadata); auto warnings_callback = [instance_descr = target_instance->descr()]( const std::string &sql, int code, const std::string &level, const std::string &msg) { log_debug("%s: '%s' on instance '%s'. (Code %d) When executing query: '%s'", level.c_str(), msg.c_str(), instance_descr.c_str(), code, sql.c_str()); if (level != "WARNING") return; auto console = current_console(); console->print_info(); console->print_warning( shcore::str_format("%s (Code %d).", msg.c_str(), code)); }; // Add the warnings callback target_instance->register_warnings_callback(warnings_callback); { // Call the API std::unique_ptr<Configure_instance> op_configure_instance; if (options.local) { op_configure_instance.reset( new Configure_local_instance(target_instance, options, purpose)); } else { op_configure_instance.reset(new Configure_instance( target_instance, options, state.source_type, purpose)); } op_configure_instance->prepare(); op_configure_instance->execute(); } } REGISTER_HELP_FUNCTION(configureLocalInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CONFIGURELOCALINSTANCE, R"*( Validates and configures a local instance for MySQL InnoDB Cluster usage. @param instance Optional An instance definition. @param options Optional Additional options for the operation. @returns Nothing @attention This function is deprecated and will be removed in a future release of MySQL Shell, use dba.configureInstance() instead. This function reviews the instance configuration to identify if it is valid for usage in an InnoDB cluster, making configuration changes if necessary. The instance definition is the connection data for the instance. ${TOPIC_CONNECTION_MORE_INFO} ${CONFIGURE_INSTANCE_COMMON_OPTIONS} ${CONFIGURE_INSTANCE_COMMON_DETAILS_1} The returned descriptive text of the operation result indicates whether the instance was successfully configured for InnoDB Cluster usage or if it was already valid for InnoDB Cluster usage. ${CONFIGURE_INSTANCE_COMMON_DETAILS_2} @attention The clearReadOnly option will be removed in a future release and it's no longer needed, super_read_only is automatically cleared. @attention The interactive option will be removed in a future release. @attention The password option will be removed in a future release. )*"); /** * $(DBA_CONFIGURELOCALINSTANCE_BRIEF) * * $(DBA_CONFIGURELOCALINSTANCE) */ #if DOXYGEN_JS Undefined Dba::configureLocalInstance(InstanceDef instance, Dictionary options) {} #elif DOXYGEN_PY None Dba::configure_local_instance(InstanceDef instance, dict options) {} #endif void Dba::configure_local_instance( const mysqlshdk::utils::nullable<Connection_options> &instance_def, const shcore::Option_pack_ref<Configure_cluster_local_instance_options> &options) { mysqlsh::current_console()->print_warning( "This function is deprecated and will be removed in a future release of " "MySQL Shell, use dba.<<<configureInstance()>>> instead."); return do_configure_instance( instance_def ? *instance_def : Connection_options{}, *options, Cluster_type::GROUP_REPLICATION); } REGISTER_HELP_FUNCTION(configureInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CONFIGUREINSTANCE, R"*( Validates and configures an instance for MySQL InnoDB Cluster usage. @param instance Optional An instance definition. @param options Optional Additional options for the operation. @returns A descriptive text of the operation result. This function auto-configures the instance for InnoDB Cluster usage. If the target instance already belongs to an InnoDB Cluster it errors out. The instance definition is the connection data for the instance. ${TOPIC_CONNECTION_MORE_INFO} ${CONFIGURE_INSTANCE_COMMON_OPTIONS} ${OPT_APPLIERWORKERTHREADS} ${CONFIGURE_INSTANCE_COMMON_DETAILS_1} This function reviews the instance configuration to identify if it is valid for usage in group replication and cluster. An exception is thrown if not. ${CONFIGURE_INSTANCE_COMMON_DETAILS_2} @attention The clearReadOnly option will be removed in a future release and it's no longer needed, super_read_only is automatically cleared. @attention The interactive option will be removed in a future release. @attention The password option will be removed in a future release. )*"); REGISTER_HELP_TOPIC_TEXT(CONFIGURE_INSTANCE_COMMON_OPTIONS, R"*( The options dictionary may contain the following options: @li mycnfPath: The path to the MySQL configuration file of the instance. @li outputMycnfPath: Alternative output path to write the MySQL configuration file of the instance. @li password: The password to be used on the connection. Deprecated. @li clusterAdmin: The name of the "cluster administrator" account. @li clusterAdminPassword: The password for the "cluster administrator" account. @li clusterAdminPasswordExpiration: Password expiration setting for the account. May be set to the number of days for expiration, 'NEVER' to disable expiration and 'DEFAULT' to use the system default. @li clusterAdminCertIssuer: Optional SSL certificate issuer for the account. @li clusterAdminCertSubject: Optional SSL certificate subject for the account. @li clearReadOnly: boolean value used to confirm that super_read_only must be disabled. Deprecated and default value is true. @li restart: boolean value used to indicate that a remote restart of the target instance should be performed to finalize the operation. ${OPT_INTERACTIVE} )*"); REGISTER_HELP_TOPIC_TEXT(CONFIGURE_INSTANCE_COMMON_DETAILS_1, R"*( If the outputMycnfPath option is used, only that file is updated and mycnfPath is treated as read-only. The connection password may be contained on the instance definition, however, it can be overwritten if it is specified on the options. The clusterAdmin must be a standard MySQL account name. It could be either an existing account or an account to be created. The clusterAdminPassword must be specified only if the clusterAdmin account will be created. )*"); REGISTER_HELP_TOPIC_TEXT(CONFIGURE_INSTANCE_COMMON_DETAILS_2, R"*( If the instance was not valid for InnoDB Cluster and interaction is enabled, before configuring the instance a prompt to confirm the changes is presented and a table with the following information: @li Variable: the invalid configuration variable. @li Current Value: the current value for the invalid configuration variable. @li Required Value: the required value for the configuration variable. @attention The interactive option will be removed in a future release. )*"); /** * $(DBA_CONFIGUREINSTANCE_BRIEF) * * $(DBA_CONFIGUREINSTANCE) */ #if DOXYGEN_JS Undefined Dba::configureInstance(InstanceDef instance, Dictionary options) {} #elif DOXYGEN_PY None Dba::configure_instance(InstanceDef instance, dict options) {} #endif void Dba::configure_instance( const mysqlshdk::utils::nullable<Connection_options> &instance_def, const shcore::Option_pack_ref<Configure_cluster_instance_options> &options) { do_configure_instance(instance_def ? *instance_def : Connection_options{}, *options, Cluster_type::GROUP_REPLICATION); } REGISTER_HELP_FUNCTION(configureReplicaSetInstance, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_CONFIGUREREPLICASETINSTANCE, R"*( Validates and configures an instance for use in an InnoDB ReplicaSet. @param instance Optional An instance definition. By default, the active shell session is used. @param options Optional Additional options for the operation. @returns Nothing This function will verify and automatically configure the target instance for use in an InnoDB ReplicaSet. The function can optionally create a "cluster administrator" account, if the "clusterAdmin" and "clusterAdminPassword" options are given. The account is created with the minimal set of privileges required to manage InnoDB clusters or ReplicaSets. The "cluster administrator" account must have matching username and password across all instances of the same cluster or replicaset. <b>Options</b> The instance definition is the connection data for the instance. ${TOPIC_CONNECTION_MORE_INFO} The options dictionary may contain the following options: @li password: The password to be used on the connection. Deprecated. @li clusterAdmin: The name of a "cluster administrator" user to be created. The supported format is the standard MySQL account name format. @li clusterAdminPassword: The password for the "cluster administrator" account. @li clusterAdminPasswordExpiration: Password expiration setting for the account. May be set to the number of days for expiration, 'NEVER' to disable expiration and 'DEFAULT' to use the system default. @li clusterAdminCertIssuer: Optional SSL certificate issuer for the account. @li clusterAdminCertSubject: Optional SSL certificate subject for the account. ${OPT_INTERACTIVE} @li restart: boolean value used to indicate that a remote restart of the target instance should be performed to finalize the operation. ${OPT_APPLIERWORKERTHREADS} The connection password may be contained on the instance definition, however, it can be overwritten if it is specified on the options. This function reviews the instance configuration to identify if it is valid for usage in replicasets. An exception is thrown if not. If the instance was not valid for InnoDB ReplicaSet and interaction is enabled, before configuring the instance a prompt to confirm the changes is presented and a table with the following information: @li Variable: the invalid configuration variable. @li Current Value: the current value for the invalid configuration variable. @li Required Value: the required value for the configuration variable. @attention The interactive option will be removed in a future release. @attention The password option will be removed in a future release. )*"); /** * $(DBA_CONFIGUREREPLICASETINSTANCE_BRIEF) * * $(DBA_CONFIGUREREPLICASETINSTANCE) */ #if DOXYGEN_JS Undefined Dba::configureReplicaSetInstance(InstanceDef instance, Dictionary options) {} #elif DOXYGEN_PY None Dba::configure_replica_set_instance(InstanceDef instance, dict options) {} #endif void Dba::configure_replica_set_instance( const mysqlshdk::utils::nullable<Connection_options> &instance_def, const shcore::Option_pack_ref<Configure_replicaset_instance_options> &options) { return do_configure_instance( instance_def ? *instance_def : Connection_options{}, *options, Cluster_type::ASYNC_REPLICATION); } REGISTER_HELP_FUNCTION(rebootClusterFromCompleteOutage, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_REBOOTCLUSTERFROMCOMPLETEOUTAGE, R"*( Brings a cluster back ONLINE when all members are OFFLINE. @param clusterName Optional The name of the cluster to be rebooted. @param options Optional dictionary with options that modify the behavior of this function. @returns The rebooted cluster object. The options dictionary can contain the next values: @li user: The user used for the instances sessions required operations. @li password: The password used for the instances sessions required operations. @li clearReadOnly: boolean value used to confirm that super_read_only must be disabled. @li force: boolean value to indicate that the operation must be executed even if some members of the Cluster cannot be reached, or the primary instance selected has a diverging or lower GTID_SET. @li dryRun: boolean value that if true, all validations and steps of the command are executed, but no changes are actually made. An exception will be thrown when finished. @li primary: Instance definition representing the instance that must be selected as the primary. @li switchCommunicationStack: The Group Replication protocol stack to be used by the Cluster after the reboot. @li ipAllowList: The list of hosts allowed to connect to the instance for Group Replication traffic when using the 'XCOM' protocol stack. @li localAddress: string value with the Group Replication local address to be used instead of the automatically generated one when using the 'XCOM' protocol stack. @li timeout: integer value with the maximum number of seconds to wait for pending transactions to be applied in each instance of the cluster (default value is retrieved from the 'dba.gtidWaitTimeout' shell option). ${CLUSTER_OPT_PAXOS_SINGLE_LEADER} The value for switchCommunicationStack is used to choose which Group Replication communication stack must be used in the Cluster after the reboot is complete. It's used to set the value of the Group Replication system variable 'group_replication_communication_stack'. When set to legacy 'XCom', all internal GCS network traffic (PAXOS and communication infrastructure) flows through a separate network address: the localAddress. When set to 'MySQL', such traffic re-uses the existing MySQL Server facilities to establish connections among Cluster members. It allows a simpler and safer setup as it obsoletes the usage of IP allowlists (ipAllowlist), removes the explicit need to have a separate network address (localAddress), and introduces user-based authentication. The option is used to switch the communication stack previously in use by the Cluster, to another one. Setting the same communication stack results in no changes. The ipAllowlist format is a comma separated list of IP addresses or subnet CIDR notation, for example: 192.168.1.0/24,10.0.0.1. By default the value is set to AUTOMATIC, allowing addresses from the instance private network to be automatically set for the allowlist. ${CLUSTER_OPT_LOCAL_ADDRESS_EXTRA} ${CLUSTER_OPT_PAXOS_SINGLE_LEADER_EXTRA} The option is used to switch the value of paxosSingleLeader previously in use by the Cluster, to either enable or disable it. @attention The clearReadOnly option will be removed in a future release. @attention The user option will be removed in a future release. @attention The password option will be removed in a future release. This function reboots a cluster from complete outage. It obtains the Cluster information from the instance MySQL Shell is connected to and uses the most up-to-date instance in regards to transactions as new seed instance to recover the cluster. The remaining Cluster members are automatically rejoined. On success, the restored cluster object is returned by the function. The current session must be connected to a former instance of the cluster. If name is not specified, the default cluster will be returned. @note The user and password options are no longer used, the connection data is taken from the active shell session. @note The clearReadOnly option is no longer used, super_read_only is automatically cleared. )*"); /** * $(DBA_REBOOTCLUSTERFROMCOMPLETEOUTAGE_BRIEF) * * $(DBA_REBOOTCLUSTERFROMCOMPLETEOUTAGE) */ #if DOXYGEN_JS Cluster Dba::rebootClusterFromCompleteOutage(String clusterName, Dictionary options) {} #elif DOXYGEN_PY Cluster Dba::reboot_cluster_from_complete_outage(str clusterName, dict options) {} #endif std::shared_ptr<Cluster> Dba::reboot_cluster_from_complete_outage( const mysqlshdk::null_string &cluster_name, const shcore::Option_pack_ref<Reboot_cluster_options> &options) { std::shared_ptr<MetadataStorage> metadata; std::shared_ptr<Instance> target_instance; // The cluster is completely dead, so we can't find the primary anyway... // thus this instance will be used as the primary connect_to_target_group({}, &metadata, &target_instance, false); check_function_preconditions("Dba.rebootClusterFromCompleteOutage", metadata, target_instance); // Validate and handle command options { // Get the communication in stack that was being used by the Cluster auto target_instance_version = target_instance->get_version(); std::string comm_stack = kCommunicationStackXCom; if (supports_mysql_communication_stack(target_instance_version)) { comm_stack = target_instance ->get_persisted_value("group_replication_communication_stack") .value_or(""); } // Validate the options: // - switchCommunicationStack // - localAddress static_cast<Reboot_cluster_options>(*options).check_option_values( target_instance_version, target_instance->get_canonical_port(), comm_stack); } Scoped_instance_pool ipool( metadata, false, Instance_pool::Auth_options{target_instance->get_connection_options()}); const auto console = current_console(); if (options->get_dry_run()) { console->print_note( "dryRun option was specified. Validations will be executed, but no " "changes will be applied."); } // Getting the cluster from the metadata already complies with: // - ensure that a Metadata Schema exists on the current session instance // - ensure that the provided cluster identifier exists on the Metadata // Schema std::shared_ptr<mysqlsh::dba::Cluster> cluster; try { cluster = get_cluster(!cluster_name ? nullptr : cluster_name->c_str(), metadata, target_instance, true, true); } catch (const shcore::Error &e) { // If the GR plugin is not installed, we can get this error. // In that case, we install the GR plugin and retry. if (e.code() != ER_UNKNOWN_SYSTEM_VARIABLE) throw; if (!options->get_dry_run()) { log_info("%s: installing GR plugin (%s)", target_instance->descr().c_str(), e.format().c_str()); mysqlshdk::gr::install_group_replication_plugin(*target_instance, nullptr); cluster = get_cluster(!cluster_name ? nullptr : cluster_name->c_str(), metadata, target_instance, true, true); } } if (!cluster) { if (!cluster_name) { throw shcore::Exception::logic_error("No default Cluster is configured."); } throw shcore::Exception::logic_error(shcore::str_format( "The Cluster '%s' is not configured.", cluster_name->c_str())); } return Cluster_topology_executor<Reboot_cluster_from_complete_outage>{ this, cluster, target_instance, static_cast<Reboot_cluster_options>(*options)} .run(); } REGISTER_HELP_FUNCTION(upgradeMetadata, dba); REGISTER_HELP_FUNCTION_TEXT(DBA_UPGRADEMETADATA, R"*( Upgrades (or restores) the metadata to the version supported by the Shell. @param options Optional dictionary with option for the operation. This function will compare the version of the installed metadata schema with the version of the metadata schema supported by this Shell. If the installed metadata version is lower, an upgrade process will be started. The options dictionary accepts the following attributes: @li dryRun: boolean value used to enable a dry run of the upgrade process. ${OPT_INTERACTIVE} If dryRun is used, the function will determine whether a metadata upgrade or restore is required and inform the user without actually executing the operation. The interactive option can be used to explicitly enable or disable the interactive prompts that help the user through te upgrade process. The default value is equal to MySQL Shell wizard mode. <b>The Upgrade Process</b> When upgrading the metadata schema of clusters deployed by Shell versions before 8.0.19, a rolling upgrade of existing MySQL Router instances is required. This process allows minimizing disruption to applications during the upgrade. The rolling upgrade process must be performed in the following order: -# Execute dba.<<<upgradeMetadata>>>() using the latest Shell version. The upgrade function will stop if outdated Router instances are detected, at which point you can stop the upgrade to resume later. -# Upgrade MySQL Router to the latest version (same version number as the Shell) -# Continue or re-execute dba.<<<upgradeMetadata>>>() <b>Failed Upgrades</b> If the installed metadata is not available because a previous call to this function ended unexpectedly, this function will restore the metadata to the state it was before the failed upgrade operation. @attention The interactive option will be removed in a future release. )*"); /** * $(DBA_UPGRADEMETADATA_BRIEF) * * $(DBA_UPGRADEMETADATA) */ #if DOXYGEN_JS Undefined Dba::upgradeMetadata(Dictionary options) {} #elif DOXYGEN_PY None Dba::upgrade_metadata(dict options) {} #endif void Dba::upgrade_metadata( const shcore::Option_pack_ref<Upgrade_metadata_options> &options) { auto instance = connect_to_target_member(); if (!instance) { // An open session is required throw shcore::Exception::runtime_error( "An open session is required to perform this operation."); } auto metadata = std::make_shared<MetadataStorage>(instance); // Init the connection pool Scoped_instance_pool ipool( current_shell_options()->get().wizards, Instance_pool::Auth_options(instance->get_connection_options())); auto primary = check_preconditions("Dba.upgradeMetadata", instance); if (primary) { metadata = std::make_shared<MetadataStorage>(primary); } ipool->set_metadata(metadata); // retrieve the cluster and clusterset (if available) in order to put an // exclusive lock on them std::shared_ptr<Cluster_set_impl> clusterset; std::shared_ptr<mysqlsh::dba::Cluster> cluster; mysqlshdk::mysql::Lock_scoped cs_lock; mysqlshdk::mysql::Lock_scoped c_lock; if (!options->dry_run) { try { cluster = get_cluster(nullptr, metadata, metadata->get_md_server(), false, false); } catch (const shcore::Error &e) { log_info("Unable to retrieve cluster: %s", e.what()); } try { if (cluster) clusterset = cluster->impl()->get_cluster_set_object(); } catch (const shcore::Error &e) { log_info("Unable to retrieve clusterset: %s", e.what()); } // get exclusive locks (note: the order is important) if (clusterset) cs_lock = clusterset->get_lock_exclusive(); if (cluster) c_lock = cluster->impl()->get_lock_exclusive(); } // acquire required lock on target instance. auto i_lock = metadata->get_md_server()->get_lock_exclusive(); Upgrade_metadata op_upgrade(metadata, options->interactive(), options->dry_run); shcore::on_leave_scope finally([&op_upgrade]() { op_upgrade.finish(); }); op_upgrade.prepare(); op_upgrade.execute(); } std::shared_ptr<Instance> Dba::check_preconditions( const std::string &function_name, const std::shared_ptr<Instance> &group_server, const Function_availability *custom_func_avail) { bool primary_available = true; std::shared_ptr<Instance> primary; auto metadata = std::make_shared<MetadataStorage>(group_server); auto state = get_cluster_check_info(*metadata, group_server.get(), false); try { current_ipool()->set_metadata(metadata); if (state.source_type == TargetType::InnoDBCluster || state.source_type == TargetType::InnoDBClusterSet) { auto cluster = get_cluster({}, metadata, group_server); auto primary_member = cluster->impl()->acquire_primary(); if (primary_member) { primary = std::make_shared<Instance>(*primary_member); } metadata = cluster->impl()->get_metadata_storage(); } else if (state.source_type == TargetType::AsyncReplicaSet) { auto rs = get_replica_set(metadata, group_server); primary = std::make_shared<Instance>(*rs->acquire_primary()); metadata = rs->get_metadata_storage(); } } catch (const shcore::Exception &e) { if (e.code() == SHERR_DBA_ASYNC_PRIMARY_UNAVAILABLE || e.code() == SHERR_DBA_GROUP_HAS_NO_PRIMARY) { primary_available = false; } else if (shcore::str_beginswith( e.what(), "Failed to execute query on Metadata server")) { log_debug("Unable to query Metadata schema: %s", e.what()); primary_available = false; } } check_function_preconditions(function_name, metadata, group_server, primary_available, custom_func_avail); return primary; } } // namespace dba } // namespace mysqlsh