// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) 2021 Vmware, Inc. * Pushpasis Sarkar * Copyright (c) 2023, LabN Consulting, L.L.C. */ #include #include "md5.h" #include "frrevent.h" #include "xref.h" #include "mgmt_fe_client.h" #include "mgmtd/mgmt.h" #include "mgmtd/mgmt_ds.h" #include "mgmtd/mgmt_history.h" struct mgmt_cmt_info_t { struct mgmt_cmt_infos_item cmts; char cmtid_str[MGMT_SHORT_TIME_MAX_LEN]; char time_str[MGMT_LONG_TIME_MAX_LEN]; char cmt_json_file[PATH_MAX]; }; DECLARE_DLIST(mgmt_cmt_infos, struct mgmt_cmt_info_t, cmts); #define FOREACH_CMT_REC(mm, cmt_info) \ frr_each_safe (mgmt_cmt_infos, &mm->cmts, cmt_info) /* * The only instance of VTY session that has triggered an ongoing * config rollback operation. */ static struct vty *rollback_vty; static bool file_exists(const char *path) { return !access(path, F_OK); } static void remove_file(const char *path) { if (!file_exists(path)) return; if (unlink(path)) zlog_err("Failed to remove commit history file %s: %s", path, safe_strerror(errno)); } static struct mgmt_cmt_info_t *mgmt_history_new_cmt_info(void) { struct mgmt_cmt_info_t *new; struct timespec tv; struct tm tm; new = XCALLOC(MTYPE_MGMTD_CMT_INFO, sizeof(struct mgmt_cmt_info_t)); clock_gettime(CLOCK_REALTIME, &tv); localtime_r(&tv.tv_sec, &tm); mgmt_time_to_string(&tv, true, new->time_str, sizeof(new->time_str)); mgmt_time_to_string(&tv, false, new->cmtid_str, sizeof(new->cmtid_str)); snprintf(new->cmt_json_file, sizeof(new->cmt_json_file), MGMTD_COMMIT_FILE_PATH, new->cmtid_str); return new; } static struct mgmt_cmt_info_t *mgmt_history_create_cmt_rec(void) { struct mgmt_cmt_info_t *new = mgmt_history_new_cmt_info(); struct mgmt_cmt_info_t *cmt_info; struct mgmt_cmt_info_t *last_cmt_info = NULL; if (mgmt_cmt_infos_count(&mm->cmts) == MGMTD_MAX_COMMIT_LIST) { FOREACH_CMT_REC (mm, cmt_info) last_cmt_info = cmt_info; if (last_cmt_info) { remove_file(last_cmt_info->cmt_json_file); mgmt_cmt_infos_del(&mm->cmts, last_cmt_info); XFREE(MTYPE_MGMTD_CMT_INFO, last_cmt_info); } } mgmt_cmt_infos_add_head(&mm->cmts, new); return new; } static struct mgmt_cmt_info_t * mgmt_history_find_cmt_record(const char *cmtid_str) { struct mgmt_cmt_info_t *cmt_info; FOREACH_CMT_REC (mm, cmt_info) { if (strcmp(cmt_info->cmtid_str, cmtid_str) == 0) return cmt_info; } return NULL; } static bool mgmt_history_read_cmt_record_index(void) { FILE *fp; struct mgmt_cmt_info_t cmt_info; struct mgmt_cmt_info_t *new; int cnt = 0; if (!file_exists(MGMTD_COMMIT_FILE_PATH)) return false; fp = fopen(MGMTD_COMMIT_INDEX_FILE_NAME, "rb"); if (!fp) { zlog_err("Failed to open commit history %s for reading: %s", MGMTD_COMMIT_INDEX_FILE_NAME, safe_strerror(errno)); return false; } while ((fread(&cmt_info, sizeof(cmt_info), 1, fp)) > 0) { if (cnt < MGMTD_MAX_COMMIT_LIST) { if (!file_exists(cmt_info.cmt_json_file)) { zlog_err("Commit in index, but file %s missing", cmt_info.cmt_json_file); continue; } new = XCALLOC(MTYPE_MGMTD_CMT_INFO, sizeof(struct mgmt_cmt_info_t)); memcpy(new, &cmt_info, sizeof(struct mgmt_cmt_info_t)); mgmt_cmt_infos_add_tail(&mm->cmts, new); } else { zlog_warn( "More records found in commit history file %s than expected", MGMTD_COMMIT_INDEX_FILE_NAME); fclose(fp); return false; } cnt++; } fclose(fp); return true; } static bool mgmt_history_dump_cmt_record_index(void) { FILE *fp; int ret = 0; struct mgmt_cmt_info_t *cmt_info; struct mgmt_cmt_info_t cmt_info_set[10]; int cnt = 0; fp = fopen(MGMTD_COMMIT_INDEX_FILE_NAME, "wb"); if (!fp) { zlog_err("Failed to open commit history %s for writing: %s", MGMTD_COMMIT_INDEX_FILE_NAME, safe_strerror(errno)); return false; } FOREACH_CMT_REC (mm, cmt_info) { memcpy(&cmt_info_set[cnt], cmt_info, sizeof(struct mgmt_cmt_info_t)); cnt++; } if (!cnt) { fclose(fp); return false; } ret = fwrite(&cmt_info_set, sizeof(struct mgmt_cmt_info_t), cnt, fp); fclose(fp); if (ret != cnt) { zlog_err("Failed to write full commit history, removing file"); remove_file(MGMTD_COMMIT_INDEX_FILE_NAME); return false; } return true; } static int mgmt_history_rollback_to_cmt(struct vty *vty, struct mgmt_cmt_info_t *cmt_info, bool skip_file_load) { struct mgmt_ds_ctx *src_ds_ctx; struct mgmt_ds_ctx *dst_ds_ctx; int ret = 0; if (rollback_vty) { vty_out(vty, "ERROR: Rollback already in progress!\n"); return -1; } src_ds_ctx = mgmt_ds_get_ctx_by_id(mm, MGMTD_DS_CANDIDATE); dst_ds_ctx = mgmt_ds_get_ctx_by_id(mm, MGMTD_DS_RUNNING); assert(src_ds_ctx); assert(dst_ds_ctx); ret = mgmt_ds_lock(src_ds_ctx, vty->mgmt_session_id); if (ret != 0) { vty_out(vty, "Failed to lock the DS %u for rollback Reason: %s!\n", MGMTD_DS_RUNNING, strerror(ret)); return -1; } ret = mgmt_ds_lock(dst_ds_ctx, vty->mgmt_session_id); if (ret != 0) { mgmt_ds_unlock(src_ds_ctx); vty_out(vty, "Failed to lock the DS %u for rollback Reason: %s!\n", MGMTD_DS_RUNNING, strerror(ret)); return -1; } if (!skip_file_load) { ret = mgmt_ds_load_config_from_file( src_ds_ctx, cmt_info->cmt_json_file, false); if (ret != 0) { vty_out(vty, "Error with parsing the file with error code %d\n", ret); goto failed_unlock; } } /* Internally trigger a commit-request. */ ret = mgmt_txn_rollback_trigger_cfg_apply(src_ds_ctx, dst_ds_ctx); if (ret != 0) { vty_out(vty, "Error with creating commit apply txn with error code %d\n", ret); goto failed_unlock; } mgmt_history_dump_cmt_record_index(); /* * TODO: Cleanup: the generic TXN code currently checks for rollback * and does the unlock when it completes. */ /* * Block the rollback command from returning till the rollback * is completed. On rollback completion mgmt_history_rollback_complete() * shall be called to resume the rollback command return to VTYSH. */ vty->mgmt_req_pending_cmd = "ROLLBACK"; rollback_vty = vty; return 0; failed_unlock: mgmt_ds_unlock(src_ds_ctx); mgmt_ds_unlock(dst_ds_ctx); return ret; } void mgmt_history_rollback_complete(bool success) { vty_mgmt_resume_response(rollback_vty, success ? CMD_SUCCESS : CMD_WARNING_CONFIG_FAILED); rollback_vty = NULL; } int mgmt_history_rollback_by_id(struct vty *vty, const char *cmtid_str) { int ret = 0; struct mgmt_cmt_info_t *cmt_info; if (!mgmt_cmt_infos_count(&mm->cmts) || !mgmt_history_find_cmt_record(cmtid_str)) { vty_out(vty, "Invalid commit Id\n"); return -1; } FOREACH_CMT_REC (mm, cmt_info) { if (strcmp(cmt_info->cmtid_str, cmtid_str) == 0) { ret = mgmt_history_rollback_to_cmt(vty, cmt_info, false); return ret; } remove_file(cmt_info->cmt_json_file); mgmt_cmt_infos_del(&mm->cmts, cmt_info); XFREE(MTYPE_MGMTD_CMT_INFO, cmt_info); } return 0; } int mgmt_history_rollback_n(struct vty *vty, int num_cmts) { int ret = 0; int cnt = 0; struct mgmt_cmt_info_t *cmt_info; size_t cmts; if (!num_cmts) num_cmts = 1; cmts = mgmt_cmt_infos_count(&mm->cmts); if ((int)cmts < num_cmts) { vty_out(vty, "Number of commits found (%d) less than required to rollback\n", (int)cmts); return -1; } if ((int)cmts == 1 || (int)cmts == num_cmts) { vty_out(vty, "Number of commits found (%d), Rollback of last commit is not supported\n", (int)cmts); return -1; } FOREACH_CMT_REC (mm, cmt_info) { if (cnt == num_cmts) { ret = mgmt_history_rollback_to_cmt(vty, cmt_info, false); return ret; } cnt++; remove_file(cmt_info->cmt_json_file); mgmt_cmt_infos_del(&mm->cmts, cmt_info); XFREE(MTYPE_MGMTD_CMT_INFO, cmt_info); } if (!mgmt_cmt_infos_count(&mm->cmts)) { mgmt_ds_reset_candidate(); ret = mgmt_history_rollback_to_cmt(vty, cmt_info, true); } return ret; } void show_mgmt_cmt_history(struct vty *vty) { struct mgmt_cmt_info_t *cmt_info; int slno = 0; vty_out(vty, "Last 10 commit history:\n"); vty_out(vty, "Slot Commit-ID Commit-Record-Time\n"); FOREACH_CMT_REC (mm, cmt_info) { vty_out(vty, "%4d %23s %s\n", slno, cmt_info->cmtid_str, cmt_info->time_str); slno++; } } void mgmt_history_new_record(struct mgmt_ds_ctx *ds_ctx) { struct mgmt_cmt_info_t *cmt_info = mgmt_history_create_cmt_rec(); mgmt_ds_dump_ds_to_file(cmt_info->cmt_json_file, ds_ctx); mgmt_history_dump_cmt_record_index(); } void mgmt_history_init(void) { /* Create commit record for previously stored commit-apply */ mgmt_cmt_infos_init(&mm->cmts); mgmt_history_read_cmt_record_index(); } void mgmt_history_destroy(void) { struct mgmt_cmt_info_t *cmt_info; FOREACH_CMT_REC(mm, cmt_info) { mgmt_cmt_infos_del(&mm->cmts, cmt_info); XFREE(MTYPE_MGMTD_CMT_INFO, cmt_info); } mgmt_cmt_infos_fini(&mm->cmts); }