/* * Copyright (c) 2016 MariaDB Corporation Ab * * Use of this software is governed by the Business Source License included * in the LICENSE.TXT file and at www.mariadb.com/bsl11. * * Change Date: 2020-01-01 * * On the date above, in accordance with the Business Source License, use * of this software will be governed by version 2 or later of the General * Public License. */ #include "../mysqlmon.h" #include #include #include #include #include /** * Crash-safe storage of server states * * This file contains functions to store and load journals of the server states. */ /** Schema version, journals must have a matching version */ #define MMB_SCHEMA_VERSION 1 /** Constants for byte lengths of the values */ #define MMB_LEN_BYTES 4 #define MMB_LEN_SCHEMA_VERSION 1 #define MMB_LEN_CRC32 4 #define MMB_LEN_VALUE_TYPE 1 #define MMB_LEN_SERVER_STATUS 4 /** Type of the stored value */ enum stored_value_type { SVT_SERVER = 1, // Generic server state information SVT_MASTER = 2, // The master server name }; /** * @brief Remove .tmp suffix and rename file * * @param src File to rename * @return True if file was successfully renamed */ static bool rename_tmp_file(const char *src) { char dest[strlen(src) + 1]; strcpy(dest, src); char *tail = strrchr(dest, '.'); ss_dassert(tail && strcmp(tail, ".tmp") == 0); *tail = '\0'; bool rval = true; if (rename(src, dest) == -1) { rval = false; MXS_ERROR("Failed to rename journal file '%s' to '%s': %d, %s", src, dest, errno, mxs_strerror(errno)); } return rval; } /** * @brief Open temporary file * * @param monitor Monitor * @param path Output where the path is stored * @return Opened file or NULL on error */ static FILE* open_tmp_file(MXS_MONITOR *monitor, char *path) { const char *name_template = "%s/%s/"; const char filename[] = "mysqlmon.dat.tmp"; int nbytes = snprintf(path, PATH_MAX, name_template, get_datadir(), monitor->name); FILE *rval = NULL; if (nbytes < PATH_MAX - sizeof(filename) && mxs_mkdir_all(path, 0744)) { strcat(path, filename); if ((rval = fopen(path, "wb")) == NULL) { MXS_ERROR("Failed to open file '%s': %d, %s", path, errno, mxs_strerror(errno)); } } return rval; } /** * @brief Store server data to in-memory buffer * * @param monitor Monitor * @param data Pointer to in-memory buffer used for storage, should be at least * PATH_MAX bytes long * @param size Size of @c data */ static void store_data(MXS_MONITOR *monitor, char *data, uint32_t size) { MYSQL_MONITOR* handle = (MYSQL_MONITOR*) monitor->handle; char *ptr = data; /** Store the data length */ ss_dassert(sizeof(size) == MMB_LEN_BYTES); *ptr++ = size; *ptr++ = (size >> 8); *ptr++ = (size >> 16); *ptr++ = (size >> 24); /** Then the schema version */ *ptr++ = MMB_SCHEMA_VERSION; /** Store the states of all servers */ for (MXS_MONITOR_SERVERS* db = monitor->databases; db; db = db->next) { *ptr++ = (char)SVT_SERVER; // Value type strcpy(ptr, db->server->unique_name); // Name of the server ptr += strlen(db->server->unique_name) + 1; uint32_t status = db->server->status; // Server status as 4 byte integer ss_dassert(sizeof(status) == MMB_LEN_SERVER_STATUS); *ptr++ = status; *ptr++ = (status >> 8); *ptr++ = (status >> 16); *ptr++ = (status >> 24); } /** Store the current root master if we have one */ if (handle->master) { *ptr++ = (char)SVT_MASTER; strcpy(ptr, handle->master->server->unique_name); ptr += strlen(handle->master->server->unique_name) + 1; } /** Calculate the CRC32 for the complete payload minus the CRC32 bytes */ uint32_t crc = crc32(0L, NULL, 0); crc = crc32(crc, (uint8_t*)data + MMB_LEN_BYTES, size - MMB_LEN_CRC32); ss_dassert(sizeof(crc) == MMB_LEN_CRC32); *ptr++ = crc; *ptr++ = (crc >> 8); *ptr++ = (crc >> 16); *ptr++ = (crc >> 24); ss_dassert(ptr - data == size + MMB_LEN_BYTES); } static int get_data_file_path(MXS_MONITOR *monitor, char *path) { const char *name_template = "%s/%s/mysqlmon.dat"; return snprintf(path, PATH_MAX, name_template, get_datadir(), monitor->name); } /** * @brief Open stored journal file * * @param monitor Monitor to reload * @param path Output where path is stored * @return Opened file or NULL on error */ static FILE* open_data_file(MXS_MONITOR *monitor, char *path) { FILE *rval = NULL; int nbytes = get_data_file_path(monitor, path); if (nbytes < PATH_MAX) { if ((rval = fopen(path, "rb")) == NULL && errno != ENOENT) { MXS_ERROR("Failed to open journal file: %d, %s", errno, mxs_strerror(errno)); } } return rval; } /** * Check that memory area contains a null terminator */ static bool has_null_terminator(const char *data, const char *end) { while (data < end) { if (*data == '\0') { return true; } data++; } return false; } /** * Process a generic server */ static const char* process_server(MXS_MONITOR *monitor, const char *data, const char *end) { for (MXS_MONITOR_SERVERS* db = monitor->databases; db; db = db->next) { if (strcmp(db->server->unique_name, data) == 0) { const unsigned char *sptr = (unsigned char*)strchr(data, '\0'); ss_dassert(sptr); sptr++; uint32_t state = sptr[0] | (sptr[1] << 8) | (sptr[2] << 16) | (sptr[3] << 24); server_set_status_nolock(db->server, state); monitor_set_pending_status(db, state); break; } } data += strlen(data) + 1 + MMB_LEN_SERVER_STATUS; return data; } /** * Process a master */ static const char* process_master(MXS_MONITOR *monitor, const char *data, const char *end) { for (MXS_MONITOR_SERVERS* db = monitor->databases; db; db = db->next) { if (strcmp(db->server->unique_name, data) == 0) { MYSQL_MONITOR* handle = (MYSQL_MONITOR*)monitor->handle; handle->master = db; break; } } data += strlen(data) + 1; return data; } /** * Check that the calculated CRC32 matches the one stored on disk */ static bool check_crc32(const uint8_t *data, uint32_t size, const uint8_t *crc_ptr) { uint32_t crc = crc_ptr[0] | (crc_ptr[1] << 8) | (crc_ptr[2] << 16) | (crc_ptr[3] << 24); uint32_t calculated_crc = crc32(0L, NULL, 0); calculated_crc = crc32(calculated_crc, data, size); return calculated_crc == crc; } /** * Process the stored journal data */ static bool process_data_file(MXS_MONITOR *monitor, const char *data, const char *crc_ptr) { const char *ptr = data; ss_debug(const char *prevptr = ptr); while (ptr < crc_ptr) { /** All values contain a null terminated string */ if (!has_null_terminator(ptr, crc_ptr)) { MXS_ERROR("Possible corrupted journal file (no null terminator found). Ignoring."); return false; } enum stored_value_type type = *ptr; ptr += MMB_LEN_VALUE_TYPE; switch (type) { case SVT_SERVER: ptr = process_server(monitor, ptr, crc_ptr); break; case SVT_MASTER: ptr = process_master(monitor, ptr, crc_ptr); break; default: MXS_ERROR("Possible corrupted journal file (unknown stored value). Ignoring."); return false; } ss_dassert(prevptr != ptr); ss_debug(prevptr = ptr); } ss_dassert(ptr == crc_ptr); return true; } void store_server_journal(MXS_MONITOR *monitor) { /** Calculate how much memory we need to allocate */ uint32_t size = MMB_LEN_SCHEMA_VERSION + MMB_LEN_CRC32; for (MXS_MONITOR_SERVERS* db = monitor->databases; db; db = db->next) { /** Each server is stored as a type byte and a null-terminated string * followed by eight byte server status. */ size += MMB_LEN_VALUE_TYPE + strlen(db->server->unique_name) + 1 + MMB_LEN_SERVER_STATUS; } MYSQL_MONITOR* handle = (MYSQL_MONITOR*) monitor->handle; if (handle->master) { /** The master server name is stored as a null terminated string */ size += MMB_LEN_VALUE_TYPE + strlen(handle->master->server->unique_name) + 1; } /** 4 bytes for file length, 1 byte for schema version and 4 bytes for CRC32 */ uint32_t buffer_size = size + MMB_LEN_BYTES; char *data = (char*)MXS_MALLOC(buffer_size); char path[PATH_MAX + 1]; if (data) { /** Store the data in memory first */ store_data(monitor, data, size); FILE *file = open_tmp_file(monitor, path); if (file) { /** Write the data to a temp file and rename it to the final name */ if (fwrite(data, 1, buffer_size, file) == buffer_size && fflush(file) == 0) { if (!rename_tmp_file(path)) { unlink(path); } } else { MXS_ERROR("Failed to write journal data to disk: %d, %s", errno, mxs_strerror(errno)); } fclose(file); } } MXS_FREE(data); } void load_server_journal(MXS_MONITOR *monitor) { char path[PATH_MAX]; FILE *file = open_data_file(monitor, path); if (file) { uint32_t size = 0; size_t bytes = fread(&size, 1, MMB_LEN_BYTES, file); ss_dassert(sizeof(size) == MMB_LEN_BYTES); if (bytes == MMB_LEN_BYTES) { /** Payload contents: * * - One byte of schema version * - `size - 5` bytes of data * - Trailing 4 bytes of CRC32 */ char *data = (char*)MXS_MALLOC(size); if (data && (bytes = fread(data, 1, size, file)) == size) { if (*data == MMB_SCHEMA_VERSION) { if (check_crc32((uint8_t*)data, size - MMB_LEN_CRC32, (uint8_t*)data + size - MMB_LEN_CRC32)) { if (process_data_file(monitor, data + MMB_LEN_SCHEMA_VERSION, data + size - MMB_LEN_CRC32)) { MXS_NOTICE("Loaded server states from journal file: %s", path); } } else { MXS_ERROR("CRC32 mismatch in journal file. Ignoring."); } } else { MXS_ERROR("Unknown journal schema version: %d", (int)*data); } } else if (data) { if (ferror(file)) { MXS_ERROR("Failed to read journal file: %d, %s", errno, mxs_strerror(errno)); } else { MXS_ERROR("Failed to read journal file: Expected %u bytes, " "read %lu bytes.", size, bytes); } } MXS_FREE(data); } else { if (ferror(file)) { MXS_ERROR("Failed to read journal file length: %d, %s", errno, mxs_strerror(errno)); } else { MXS_ERROR("Failed to read journal file length: Expected %d bytes, " "read %lu bytes.", MMB_LEN_BYTES, bytes); } } fclose(file); } } void remove_server_journal(MXS_MONITOR *monitor) { char path[PATH_MAX]; if (get_data_file_path(monitor, path) < PATH_MAX) { unlink(path); } else { MXS_ERROR("Path to monitor journal directory is too long."); } } bool journal_is_stale(MXS_MONITOR *monitor, time_t max_age) { bool is_stale = true; char path[PATH_MAX]; if (get_data_file_path(monitor, path) < PATH_MAX) { struct stat st; if (stat(path, &st) == 0) { time_t tdiff = time(NULL) - st.st_mtim.tv_sec; if (tdiff >= max_age) { MXS_WARNING("Journal file was created %ld seconds ago. Maximum journal " "age is %ld seconds.", tdiff, max_age); } else { is_stale = false; } } else if (errno != ENOENT) { MXS_ERROR("Failed to inspect journal file: %d, %s", errno, mxs_strerror(errno)); } } else { MXS_ERROR("Path to monitor journal directory is too long."); } return is_stale; }