Merge branch '2.2' into develop

This commit is contained in:
Markus Mäkelä 2018-06-20 10:26:29 +03:00
commit 6278f27ab6
No known key found for this signature in database
GPG Key ID: 72D48FCE664F7B19
12 changed files with 212 additions and 147 deletions

View File

@ -59,8 +59,9 @@ HTTPS/TLS Options:
[boolean] [default: true]
Options:
--version Show version number [boolean]
--help Show help [boolean]
--version Show version number [boolean]
--tls-passphrase Password for the TLS private key [string]
--help Show help [boolean]
```
## list
@ -680,6 +681,11 @@ Usage: api <command>
Commands:
get <resource> [path] Get raw JSON
API options:
--sum Calculate sum of API result. Only works for arrays of numbers e.g. `api
get --sum servers data[].attributes.statistics.connections`.
[boolean] [default: false]
```
### api get

View File

@ -486,8 +486,6 @@ int mysql_send_custom_error(DCB *dcb, int sequence, int affected_rows, const cha
int mysql_send_standard_error(DCB *dcb, int sequence, int errnum, const char *msg);
int mysql_send_auth_error(DCB *dcb, int sequence, int affected_rows, const char* msg);
GWBUF* gw_MySQL_get_next_packet(GWBUF** p_readbuf);
GWBUF* gw_MySQL_get_packets(GWBUF** p_readbuf, int* npackets);
void protocol_add_srv_command(MySQLProtocol* p, mxs_mysql_cmd_t cmd);
void protocol_remove_srv_command(MySQLProtocol* p);
bool protocol_waits_response(MySQLProtocol* p);

View File

@ -74,6 +74,10 @@ program
describe: 'Path to TLS public certificate',
type: 'string'
})
.option('tls-passphrase', {
describe: 'Password for the TLS private key',
type: 'string'
})
.option('tls-ca-cert', {
describe: 'Path to TLS CA certificate',
type: 'string'

View File

@ -494,6 +494,10 @@ add_test_executable(mxs822_maxpasswd.cpp mxs822_maxpasswd maxpasswd LABELS maxsc
# Do only SELECTS during time > wait_timeout and then do INSERT
add_test_executable(mxs827_write_timeout.cpp mxs827_write_timeout mxs827_write_timeout LABELS readwritesplit REPL_BACKEND)
# MXS-872: MaxScale doesn't understand roles
# https://jira.mariadb.org/browse/MXS-872
add_test_executable(mxs872_roles.cpp mxs872_roles replication LABELS REPL_BACKEND)
# Block and unblock first and second slaves and check that they are recovered
add_test_executable(mxs874_slave_recovery.cpp mxs874_slave_recovery mxs874 LABELS readwritesplit REPL_BACKEND)

View File

@ -75,8 +75,7 @@ int main(int argc, char* argv[])
{
if (test.maxscales->start() == 0)
{
// Give the monitor a few seconds to monitor the servers
sleep(5);
test.maxscales->wait_for_monitor();
if (test.maxscales->connect_rwsplit() == 0)
{

View File

@ -86,7 +86,7 @@ int main(int argc, char *argv[])
Test->repl->sync_slaves();
Test->tprintf("Creating user 'user' \n");
execute_query(Test->maxscales->conn_rwsplit[0], (char *) "DROP USER user@'%%'");
execute_query(Test->maxscales->conn_rwsplit[0], (char *) "DROP USER IF EXISTS user@'%%'");
execute_query(Test->maxscales->conn_rwsplit[0], (char *) "CREATE USER user@'%%' IDENTIFIED BY 'pass2'");
execute_query(Test->maxscales->conn_rwsplit[0], (char *) "GRANT SELECT ON test.* TO user@'%%'");
execute_query(Test->maxscales->conn_rwsplit[0], (char *) "DROP TABLE IF EXISTS test.t1");

View File

@ -0,0 +1,60 @@
/**
* MXS-872: MaxScale doesn't understand roles
*
* https://jira.mariadb.org/browse/MXS-872
*/
#include "testconnections.h"
#include <vector>
using namespace std;
int main(int argc, char** argv)
{
TestConnections test(argc, argv);
test.repl->connect();
for (auto a : vector<string>({"DROP DATABASE IF EXISTS my_db",
"CREATE DATABASE my_db",
"DROP ROLE IF EXISTS dba",
"CREATE ROLE dba",
"GRANT SELECT ON my_db.* TO dba",
"DROP USER IF EXISTS 'test'@'%'",
"DROP USER IF EXISTS 'test2'@'%'",
"CREATE USER 'test'@'%' IDENTIFIED BY 'test'",
"CREATE USER 'test2'@'%' IDENTIFIED BY 'test2'",
"GRANT dba TO 'test'@'%'",
"GRANT dba TO 'test2'@'%'",
"SET DEFAULT ROLE dba FOR 'test'@'%'"}))
{
test.try_query(test.repl->nodes[0], "%s", a.c_str());
}
// Wait for the users to replicate
test.repl->sync_slaves();
test.tprintf("Connect with a user that has a default role");
MYSQL* conn = open_conn_db(test.maxscales->rwsplit_port[0], test.maxscales->IP[0], "my_db", "test", "test");
test.assert(mysql_errno(conn) == 0, "Connection failed: %s", mysql_error(conn));
char value[100] {};
find_field(conn, "SELECT CURRENT_ROLE() AS role", "role", value);
test.assert(strcmp(value, "dba") == 0, "Current role should be 'dba' but is: %s", value);
mysql_close(conn);
test.tprintf("Connect with a user that doesn't have a default role, expect failure");
conn = open_conn_db(test.maxscales->rwsplit_port[0], test.maxscales->IP[0], "my_db", "test2", "test2");
test.assert(mysql_errno(conn) != 0, "Connection should fail");
mysql_close(conn);
// Cleanup
for (auto a : vector<string>({"DROP DATABASE IF EXISTS my_db",
"DROP ROLE IF EXISTS dba",
"DROP USER 'test'@'%'",
"DROP USER 'test2'@'%'"}))
{
execute_query_silent(test.repl->nodes[0], "%s", a.c_str());
}
test.repl->disconnect();
return test.global_result;
}

View File

@ -643,7 +643,10 @@ void TestConnections::init_maxscale(int m)
{
const char * template_name = get_template_name(test_name);
stop_maxscale(m);
if (maxscale::start)
{
stop_maxscale(m);
}
process_template(m, template_name, maxscales->access_homedir[m]);
maxscales->ssh_node_f(m, true,

View File

@ -50,14 +50,77 @@
FROM mysql.user AS u LEFT JOIN mysql.tables_priv AS t \
ON (u.user = t.user AND u.host = t.host) WHERE u.plugin IN ('', 'mysql_native_password') %s"
// Query used with MariaDB 10.1 and newer, supports roles
const char* mariadb_users_query =
// First, select all users
"SELECT t.user, t.host, t.db, t.select_priv, t.password FROM "
"( "
" SELECT u.user, u.host, d.db, u.select_priv, u.password AS password, u.is_role "
" FROM mysql.user AS u LEFT JOIN mysql.db AS d "
" ON (u.user = d.user AND u.host = d.host) "
" UNION "
" SELECT u.user, u.host, t.db, u.select_priv, u.password AS password, u.is_role "
" FROM mysql.user AS u LEFT JOIN mysql.tables_priv AS t "
" ON (u.user = t.user AND u.host = t.host) "
") AS t "
// Discard any users that are roles
"WHERE t.is_role <> 'Y' %s "
"UNION "
// Then select all users again
"SELECT r.user, r.host, u.db, u.select_priv, t.password FROM "
"( "
" SELECT u.user, u.host, d.db, u.select_priv, u.password AS password, u.default_role "
" FROM mysql.user AS u LEFT JOIN mysql.db AS d "
" ON (u.user = d.user AND u.host = d.host) "
" UNION "
" SELECT u.user, u.host, t.db, u.select_priv, u.password AS password, u.default_role "
" FROM mysql.user AS u LEFT JOIN mysql.tables_priv AS t "
" ON (u.user = t.user AND u.host = t.host) "
") AS t "
// Join it to the roles_mapping table to only have users with roles
"JOIN mysql.roles_mapping AS r "
"ON (r.user = t.user AND r.host = t.host) "
// Then join it into itself to get the privileges of the role with the name of the user
"JOIN "
"( "
" SELECT u.user, u.host, d.db, u.select_priv, u.password AS password, u.is_role "
" FROM mysql.user AS u LEFT JOIN mysql.db AS d "
" ON (u.user = d.user AND u.host = d.host) "
" UNION "
" SELECT u.user, u.host, t.db, u.select_priv, u.password AS password, u.is_role "
" FROM mysql.user AS u LEFT JOIN mysql.tables_priv AS t "
" ON (u.user = t.user AND u.host = t.host) "
") AS u "
"ON (u.user = r.role AND u.is_role = 'Y') "
// We only care about users that have a default role assigned
"WHERE t.default_role = u.user %s;";
static int get_users(SERV_LISTENER *listener, bool skip_local);
static MYSQL *gw_mysql_init(void);
static int gw_mysql_set_timeouts(MYSQL* handle);
static char *mysql_format_user_entry(void *data);
static bool get_hostname(DCB *dcb, char *client_hostname, size_t size);
static char* get_new_users_query(const char *server_version, bool include_root)
static char* get_mariadb_users_query(bool include_root)
{
const char *root = include_root ? "" : " AND t.user NOT IN ('root')";
size_t n_bytes = snprintf(NULL, 0, mariadb_users_query, root, root);
char *rval = static_cast<char*>(MXS_MALLOC(n_bytes + 1));
MXS_ABORT_IF_NULL(rval);
snprintf(rval, n_bytes + 1, mariadb_users_query, root, root);
return rval;
}
static char* get_users_query(const char *server_version, uint64_t version, bool include_root)
{
if (version >= 100101) // 10.1.1 or newer, supports default roles
{
return get_mariadb_users_query(include_root);
}
// Either an older MariaDB version or a MySQL variant, use the legacy query
const char* password = strstr(server_version, "5.7.") || strstr(server_version, "8.0.")
? MYSQL57_PASSWORD : MYSQL_PASSWORD;
const char *with_root = include_root ? "" : " AND u.user NOT IN ('root')";
@ -738,7 +801,9 @@ int get_users_from_server(MYSQL *con, SERVER_REF *server_ref, SERVICE *service,
mxs_mysql_set_server_version(con, server_ref->server);
}
char *query = get_new_users_query(server_ref->server->version_string, service->enable_root);
char *query = get_users_query(server_ref->server->version_string,
server_ref->server->version,
service->enable_root);
MYSQL_AUTH *instance = (MYSQL_AUTH*)listener->auth_instance;
sqlite3* handle = get_handle(instance);
bool anon_user = false;

View File

@ -1056,9 +1056,17 @@ gw_read_normal_data(DCB *dcb, GWBUF *read_buffer, int nbytes_read)
return 0;
}
// Update the current command, required by KILL command processing
/**
* Update the current command, required by KILL command processing.
* If a COM_CHANGE_USER is in progress, this must not be done as the client
* is sending authentication data that does not have the command byte.
*/
MySQLProtocol *proto = (MySQLProtocol*)dcb->protocol;
proto->current_command = (mxs_mysql_cmd_t)mxs_mysql_get_command(read_buffer);
if (!proto->changing_user)
{
proto->current_command = (mxs_mysql_cmd_t)mxs_mysql_get_command(read_buffer);
}
char* message = handle_variables(session, &read_buffer);
@ -1624,6 +1632,17 @@ static bool reauthenticate_client(MXS_SESSION* session, GWBUF* packetbuf)
return rval;
}
// Helper function for debug assertions
static bool only_one_packet(GWBUF* buffer)
{
ss_dassert(buffer);
uint8_t header[4] = {};
gwbuf_copy_data(buffer, 0, MYSQL_HEADER_LEN, header);
size_t packet_len = gw_mysql_get_byte3(header);
size_t buffer_len = gwbuf_length(buffer);
return packet_len + MYSQL_HEADER_LEN == buffer_len;
}
/**
* Detect if buffer includes partial mysql packet or multiple packets.
* Store partial packet to dcb_readqueue. Send complete packets one by one
@ -1645,15 +1664,15 @@ static int route_by_statement(MXS_SESSION* session, uint64_t capabilities, GWBUF
GWBUF* packetbuf;
do
{
/**
* Collect incoming bytes to a buffer until complete packet has
* arrived and then return the buffer.
*/
// TODO: This should be replaced with modutil_get_next_MySQL_packet.
packetbuf = gw_MySQL_get_next_packet(p_readbuf);
// Process client request one packet at a time
packetbuf = modutil_get_next_MySQL_packet(p_readbuf);
// TODO: Do this only when RCAP_TYPE_CONTIGUOUS_INPUT is requested
packetbuf = gwbuf_make_contiguous(packetbuf);
if (packetbuf != NULL)
{
ss_dassert(only_one_packet(packetbuf));
CHK_GWBUF(packetbuf);
MySQLProtocol* proto = (MySQLProtocol*)session->client_dcb->protocol;
@ -1667,25 +1686,9 @@ static int route_by_statement(MXS_SESSION* session, uint64_t capabilities, GWBUF
if (rcap_type_required(capabilities, RCAP_TYPE_CONTIGUOUS_INPUT))
{
if (!GWBUF_IS_CONTIGUOUS(packetbuf))
{
// TODO: As long as gw_MySQL_get_next_packet is used above, the buffer
// TODO: will be contiguous. That function should be replaced with
// TODO: modutil_get_next_MySQL_packet.
GWBUF* tmp = gwbuf_make_contiguous(packetbuf);
if (tmp)
{
packetbuf = tmp;
}
else
{
// TODO: A memory allocation failure. We should close the dcb
// TODO: and terminate the session.
rc = 0;
goto return_rc;
}
}
ss_dassert(GWBUF_IS_CONTIGUOUS(packetbuf));
SERVICE *service = session->client_dcb->service;
if (rcap_type_required(capabilities, RCAP_TYPE_TRANSACTION_TRACKING) && !service->session_track_trx_state)
{
if (session_trx_is_ending(session))
@ -1752,6 +1755,7 @@ static int route_by_statement(MXS_SESSION* session, uint64_t capabilities, GWBUF
}
else if (proto->changing_user)
{
ss_dassert(proto->current_command == MXS_COM_CHANGE_USER);
proto->changing_user = false;
bool ok = reauthenticate_client(session, packetbuf);
gwbuf_free(packetbuf);

View File

@ -475,100 +475,6 @@ int mysql_send_auth_error(DCB *dcb,
return sizeof(mysql_packet_header) + mysql_payload_size;
}
/**
* Buffer contains at least one of the following:
* complete [complete] [partial] mysql packet
*
* @param p_readbuf Address of read buffer pointer
*
* @return pointer to gwbuf containing a complete packet or
* NULL if no complete packet was found.
*/
GWBUF* gw_MySQL_get_next_packet(GWBUF** p_readbuf)
{
GWBUF* packetbuf;
GWBUF* readbuf;
size_t buflen;
size_t packetlen;
size_t totalbuflen;
uint8_t* data;
size_t nbytes_copied = 0;
uint8_t* target;
readbuf = *p_readbuf;
if (readbuf == NULL)
{
packetbuf = NULL;
goto return_packetbuf;
}
CHK_GWBUF(readbuf);
if (GWBUF_EMPTY(readbuf))
{
packetbuf = NULL;
goto return_packetbuf;
}
totalbuflen = gwbuf_length(readbuf);
data = (uint8_t *)GWBUF_DATA((readbuf));
packetlen = MYSQL_GET_PAYLOAD_LEN(data) + 4;
/** packet is incomplete */
if (packetlen > totalbuflen)
{
packetbuf = NULL;
goto return_packetbuf;
}
packetbuf = gwbuf_alloc(packetlen);
target = GWBUF_DATA(packetbuf);
packetbuf->gwbuf_type = readbuf->gwbuf_type; /*< Copy the type too */
/**
* Copy first MySQL packet to packetbuf and leave posible other
* packets to read buffer.
*/
while (nbytes_copied < packetlen && totalbuflen > 0)
{
uint8_t* src = GWBUF_DATA((*p_readbuf));
size_t bytestocopy;
buflen = GWBUF_LENGTH((*p_readbuf));
bytestocopy = buflen < (packetlen - nbytes_copied) ? buflen : packetlen - nbytes_copied;
memcpy(target + nbytes_copied, src, bytestocopy);
*p_readbuf = gwbuf_consume((*p_readbuf), bytestocopy);
totalbuflen = gwbuf_length((*p_readbuf));
nbytes_copied += bytestocopy;
}
ss_dassert(buflen == 0 || nbytes_copied == packetlen);
return_packetbuf:
return packetbuf;
}
/**
* Move <npackets> from buffer pointed to by <*p_readbuf>.
* Appears to be unused 11 May 2016 (Martin)
*/
GWBUF* gw_MySQL_get_packets(GWBUF** p_srcbuf,
int* npackets)
{
GWBUF* packetbuf;
GWBUF* targetbuf = NULL;
while (*npackets > 0 && (packetbuf = gw_MySQL_get_next_packet(p_srcbuf)) != NULL)
{
targetbuf = gwbuf_append(targetbuf, packetbuf);
*npackets -= 1;
}
ss_dassert(*npackets < 128);
ss_dassert(*npackets >= 0);
return targetbuf;
}
static server_command_t* server_command_init(server_command_t* srvcmd,
mxs_mysql_cmd_t cmd)
{

View File

@ -321,6 +321,22 @@ void RWSplitSession::correct_packet_sequence(GWBUF *buffer)
}
}
static bool connection_was_killed(GWBUF* buffer)
{
bool rval = false;
if (mxs_mysql_is_err_packet(buffer))
{
uint8_t buf[2];
// First two bytes after the 0xff byte are the error code
gwbuf_copy_data(buffer, MYSQL_HEADER_LEN + 1, 2, buf);
uint16_t errcode = gw_mysql_get_byte2(buf);
rval = errcode == ER_CONNECTION_KILLED;
}
return rval;
}
static void log_unexpected_response(SRWBackend& backend, GWBUF* buffer, GWBUF* current_query)
{
if (mxs_mysql_is_err_packet(buffer))
@ -333,18 +349,9 @@ static void log_unexpected_response(SRWBackend& backend, GWBUF* buffer, GWBUF* c
uint16_t errcode = MYSQL_GET_ERRCODE(data);
std::string errstr((char*)data + 7, (char*)data + 7 + len - 3);
if (errcode == ER_CONNECTION_KILLED)
{
MXS_INFO("Connection from '%s'@'%s' to '%s' was killed",
backend->dcb()->session->client_dcb->user,
backend->dcb()->session->client_dcb->remote,
backend->name());
}
else
{
MXS_WARNING("Server '%s' sent an unexpected error: %hu, %s",
backend->name(), errcode, errstr.c_str());
}
ss_dassert(errcode != ER_CONNECTION_KILLED);
MXS_WARNING("Server '%s' sent an unexpected error: %hu, %s",
backend->name(), errcode, errstr.c_str());
}
else
{
@ -449,11 +456,20 @@ void RWSplitSession::clientReply(GWBUF *writebuf, DCB *backend_dcb)
if (backend->get_reply_state() == REPLY_STATE_DONE)
{
/** If we receive an unexpected response from the server, the internal
* logic cannot handle this situation. Routing the reply straight to
* the client should be the safest thing to do at this point. */
log_unexpected_response(backend, writebuf, m_current_query.get());
MXS_SESSION_ROUTE_REPLY(backend_dcb->session, writebuf);
if (connection_was_killed(writebuf))
{
// The connection was killed, we can safely ignore it. When the TCP connection is
// closed, the router's error handling will sort it out.
gwbuf_free(writebuf);
}
else
{
/** If we receive an unexpected response from the server, the internal
* logic cannot handle this situation. Routing the reply straight to
* the client should be the safest thing to do at this point. */
log_unexpected_response(backend, writebuf, m_current_query.get());
MXS_SESSION_ROUTE_REPLY(backend_dcb->session, writebuf);
}
return;
}