/*
   CTDB NFSv3 rpc.statd HA callout

   Copyright 2023, DataDirect Networks, Inc. All rights reserved.
   Author: Martin Schwenke <mschwenke@ddn.com>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   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 for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, see <http://www.gnu.org/licenses/>.
*/

#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "replace.h"

/*
 * A configuration file, created by statd_callout_helper, containing
 * at least 1 line of text.
 *
 * The first line is the mode.  Currently supported modes are:
 *
 *   persistent_db
 *   shared_dir
 *   none
 *
 * In persistent_db and shared_dir modes, the file contains 2
 * subsequent lines of text:
 *
 *   path: directory where files should be created
 *   ips_file: file containing node's currently assigned public IP addresses
 *
 * In none mode, there are no subsequent lines.
 */
#define CONFIG_FILE CTDB_VARDIR "/scripts/statd_callout.conf"

static const char *progname;

struct {
	enum {
		CTDB_SC_MODE_PERSISTENT_DB,
		CTDB_SC_MODE_SHARED_DIR,
		CTDB_SC_MODE_NONE,
	} mode;
	union {
		struct {
			char *path;
			char *ips_file;
		};
	};
} config;

static bool getline_strip(char **lineptr,
			  size_t *n,
			  FILE *stream)
{
	bool was_null;
	int ret;

	was_null = *lineptr == NULL;

	ret = getline(lineptr, n, stream);
	if (ret == -1 || ret <= 2) {
		if (was_null) {
			free(*lineptr);
			*lineptr = NULL;
			*n = 0;
		}
		return false;
	}

	if ((*lineptr)[ret - 1] == '\n') {
		(*lineptr)[ret - 1] = '\0';
	}

	return true;
}

static void free_config(void)
{
	switch (config.mode) {
	case CTDB_SC_MODE_PERSISTENT_DB:
	case CTDB_SC_MODE_SHARED_DIR:
		free(config.path);
		config.path = NULL;
		free(config.ips_file);
		config.ips_file = NULL;
		break;
	case CTDB_SC_MODE_NONE:
		break;
	}
}

static void read_config(void)
{
	const char *config_file = NULL;
	FILE *f = NULL;
	char *mode = NULL;
	size_t n = 0;
	bool status;

	/* For testing only */
	config_file = getenv("CTDB_STATD_CALLOUT_CONFIG_FILE");
	if (config_file == NULL || strlen(config_file) == 0) {
		config_file = CONFIG_FILE;
	}

	f = fopen(config_file, "r");
	if (f == NULL) {
		fprintf(stderr,
			"%s: unable to open config file (%s)\n",
			progname,
			config_file);
		exit(1);
	}

	status = getline_strip(&mode, &n, f);
	if (!status) {
		fprintf(stderr,
			"%s: error parsing mode in %s\n",
			progname,
			config_file);
		exit(1);
	}
	if (strcmp(mode, "persistent_db") == 0) {
		config.mode = CTDB_SC_MODE_PERSISTENT_DB;
	} else if (strcmp(mode, "shared_dir") == 0) {
		config.mode = CTDB_SC_MODE_SHARED_DIR;
	} else if (strcmp(mode, "none") == 0) {
		config.mode = CTDB_SC_MODE_NONE;
	} else {
		fprintf(stderr,
			"%s: unknown mode=%s in %s\n",
			progname,
			mode,
			config_file);
		free(mode);
		exit(1);
	}
	free(mode);

	switch (config.mode) {
	case CTDB_SC_MODE_PERSISTENT_DB:
	case CTDB_SC_MODE_SHARED_DIR:
		status = getline_strip(&config.path, &n, f);
		if (!status) {
			goto parse_error;
		}

		status = getline_strip(&config.ips_file, &n, f);
		if (!status) {
			goto parse_error;
		}

		break;
	case CTDB_SC_MODE_NONE:
		break;
	}

	fclose(f);
	return;

parse_error:
	fprintf(stderr,
		"%s: error parsing contents of %s\n",
		progname,
		config_file);
	free_config();
	exit(1);
}

static void for_each_sip(void (*line_func)(const char *sip, const char *cip),
			 const char *cip)
{
	FILE *f = NULL;
	char *line = NULL;
	size_t n = 0;

	f = fopen(config.ips_file, "r");
	if (f == NULL) {
		fprintf(stderr,
			"%s: unable to open IPs file (%s)\n",
			progname,
			config.ips_file);
		exit(1);
	}

	for (;;) {
		bool status;

		status = getline_strip(&line, &n, f);
		if (!status) {
			if (!feof(f)) {
				fprintf(stderr,
					"%s: error parsing contents of %s\n",
					progname,
					config.ips_file);
				free(line);
				exit(1);
			}
			break;
		}

		line_func(line, cip);
	}

	free(line);
	fclose(f);
	return;
}

static void make_path(char *buf,
		      size_t num,
		      const char *sip,
		      const char *cip)
{
	int ret = snprintf(buf,
			   num,
			   "%s/statd-state@%s@%s",
			   config.path,
			   sip,
			   cip);
	if (ret < 0) {
		/* Not possible for snprintf(3)? */
		fprintf(stderr,
			"%s: error constructing path %s/statd-state@%s@%s\n",
			progname,
			config.path,
			sip,
			cip);
		exit(1);
	}
	if ((size_t)ret >= num) {
		fprintf(stderr,
			"%s: path too long %s/statd-state@%s@%s\n",
			progname,
			config.path,
			sip,
			cip);
		exit(1);
	}
}

static void add_client_persistent_db_line(const char *sip, const char *cip)
{
	char path[PATH_MAX];
	FILE *f;
	long long datetime;

	make_path(path, sizeof(path), sip, cip);

	datetime = (long long)time(NULL);

	f = fopen(path, "w");
	if (f == NULL) {
		fprintf(stderr,
			"%s: unable to open for writing %s\n",
			progname,
			path);
		exit(1);
	}
	fprintf(f, "\"statd-state@%s@%s\" \"%lld\"\n", sip, cip, datetime);
	fclose(f);
}

static void add_client_persistent_db(const char *cip)
{
	for_each_sip(add_client_persistent_db_line, cip);
}

static void del_client_persistent_db_line(const char *sip, const char *cip)
{
	char path[PATH_MAX];
	FILE *f;

	make_path(path, sizeof(path), sip, cip);

	f = fopen(path, "w");
	if (f == NULL) {
		fprintf(stderr,
			"%s: unable to open for writing %s\n",
			progname,
			path);
		exit(1);
	}
	fprintf(f, "\"statd-state@%s@%s\" \"\"\n", sip, cip);
	fclose(f);
}

static void del_client_persistent_db(const char *cip)
{
	for_each_sip(del_client_persistent_db_line, cip);
}

static void add_client_shared_dir_line(const char *sip, const char *cip)
{
	char path[PATH_MAX];
	FILE *f;

	make_path(path, sizeof(path), sip, cip);

	f = fopen(path, "w");
	if (f == NULL) {
		fprintf(stderr,
			"%s: unable to open for writing %s\n",
			progname,
			path);
		exit(1);
	}
	fclose(f);
}

static void add_client_shared_dir(const char *cip)
{
	for_each_sip(add_client_shared_dir_line, cip);
}

static void del_client_shared_dir_line(const char *sip, const char *cip)
{
	char path[PATH_MAX];
	int ret;

	make_path(path, sizeof(path), sip, cip);

	ret = unlink(path);
	if (ret != 0) {
		fprintf(stderr,
			"%s: unable to remove %s\n",
			progname,
			path);
		exit(1);
	}
}

static void del_client_shared_dir(const char *cip)
{
	for_each_sip(del_client_shared_dir_line, cip);
}

static void usage(void)
{
	printf("usage: %s { add-client | del-client } <client-ip>\n", progname);
	printf("       %s sm-notify mon_name ip_addr state\n", progname);
	exit(1);
}

/**
 * @brief To be used as the statd ha-callout program
 *
 * Examples
 *         progname add-client 192.168.10.94 nsds2
 *         progname del-client 192.168.10.94 nsds2
 *         progname sm-notify sitar1 192.168.10.94 127
 *
 * @param[in] event     One of add-client, del-client, sm-notify
 * @param[in] mon_name  The client being monitored.  For add-client, del-client
 *                      this will be the IP address. For sm-notify it will
 *                      be the hostname
 * @param[in] other     We don't actually use this.  For add-client and
 *                      del-client it will be NFS server hostname.
 *			For sm-notify, it will be the client's state number.
 */
int main(int argc, const char *argv[])
{
	const char *event = NULL;
	const char *mon_name = NULL;

	progname = argv[0];
	if (argc < 3) {
		usage();
	}

	read_config();

	event = argv[1];
	if (strcmp(event, "add-client") == 0) {
		mon_name = argv[2];
		switch (config.mode) {
		case CTDB_SC_MODE_PERSISTENT_DB:
			add_client_persistent_db(mon_name);
			break;
		case CTDB_SC_MODE_SHARED_DIR:
			add_client_shared_dir(mon_name);
			break;
		case CTDB_SC_MODE_NONE:
			break;
		}
	} else if (strcmp(event, "del-client") == 0) {
		mon_name = argv[2];
		switch (config.mode) {
		case CTDB_SC_MODE_PERSISTENT_DB:
			del_client_persistent_db(mon_name);
			break;
		case CTDB_SC_MODE_SHARED_DIR:
			del_client_shared_dir(mon_name);
			break;
		case CTDB_SC_MODE_NONE:
			break;
		}
	} else if (strcmp(event, "sm-notify") == 0) {
		exit(0);
	} else {
		usage();
	}

	free_config();
}
