mirror of
				https://git.proxmox.com/git/mirror_frr
				synced 2025-11-03 23:47:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			382 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
						|
/*
 | 
						|
 * Copyright (C) 2021  Vmware, Inc.
 | 
						|
 *		       Pushpasis Sarkar <spushpasis@vmware.com>
 | 
						|
 * Copyright (c) 2023, LabN Consulting, L.L.C.
 | 
						|
 */
 | 
						|
 | 
						|
#include <zebra.h>
 | 
						|
#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);
 | 
						|
}
 |