/*
 * Copyright (c) 2014,2016 Intel Corporation. All Rights Reserved
 *
 * This software is available to you under a choice of one of two
 * licenses.  You may choose to be licensed under the terms of the GNU
 * General Public License (GPL) Version 2, available from the file
 * COPYING in the main directory of this source tree, or the
 * OpenIB.org BSD license below:
 *
 *     Redistribution and use in source and binary forms, with or
 *     without modification, are permitted provided that the following
 *     conditions are met:
 *
 *      - Redistributions of source code must retain the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer.
 *
 *      - Redistributions in binary form must reproduce the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

#include <config.h>

#include <poll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <string.h>
#include <limits.h>
#include <stdio.h>
#include <syslog.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <getopt.h>
#include <stdlib.h>
#include <stdbool.h>

#include <systemd/sd-daemon.h>
#include <libudev.h>

static struct udev *g_udev;
static struct udev_monitor *g_mon;

#define SYS_HOSTNAME "/proc/sys/kernel/hostname"
#define SYS_INFINIBAND "/sys/class/infiniband"
#define DEFAULT_ND_FORMAT "%h %d"

static char *g_nd_format = NULL;
static bool debugging;

static void newline_to_null(char *str)
{
	char *term = index(str, '\n');
	if (term)
		*term = '\0';
}

static void strip_domain(char *str)
{
	char *term = index(str, '.');
	if (term)
		*term = '\0';
}

static __attribute__((format(printf, 1, 2))) void dbg_log(const char *fmt, ...)
{
	va_list ap;

	if (!debugging)
		return;

	va_start(ap, fmt);
	vsyslog(LOG_DEBUG, fmt, ap);
	va_end(ap);
}

static void build_node_desc(char *dest, size_t len,
		     const char *device, const char *hostname)
{
	char *end = dest + len-1;
	const char *field;
	char *src = g_nd_format;

	while (*src && (dest < end)) {
		if (*src != '%') {
			*dest++ = *src++;
		} else {
			src++;
			switch (*src) {
			case 'h':
				field = hostname;
				while (*field && (*field != '.') && (dest < end))
					*dest++ = *field++;
				break;
			case 'd':
				field = device;
				while (*field && (dest < end))
					*dest++ = *field++;
				break;
			}
			src++;
		}
	}
	*dest = 0;
}

static int update_node_desc(const char *device, const char *hostname, int force)
{
	int rc;
	char nd[128];
	char new_nd[64];
	char nd_file[PATH_MAX];
	FILE *f;

	snprintf(nd_file, sizeof(nd_file), SYS_INFINIBAND "/%s/node_desc",
			device);
	nd_file[sizeof(nd_file)-1] = '\0';

	f = fopen(nd_file, "r+");
	if (!f)
		return -EIO;

	if (!fgets(nd, sizeof(nd), f)) {
		syslog(LOG_ERR, "Failed to read %s\n", nd_file);
		rc = -EIO;
		goto error;
	}
	newline_to_null(nd);

	build_node_desc(new_nd, sizeof(new_nd), device, hostname);

	if (!force && strncmp(new_nd, nd, sizeof(new_nd)) == 0) {
		dbg_log("%s: no change (%s)\n", device, new_nd);
	} else {
		dbg_log("%s: change (%s) -> (%s)\n", device, nd, new_nd);
		rewind(f);
		fprintf(f, "%s", new_nd);
	}

	rc = 0;
error:
	fclose(f);
	return rc;
}

static void set_rdma_node_desc(const char *hostname, int force)
{
	DIR *class_dir;
	struct dirent *dent;

	class_dir = opendir(SYS_INFINIBAND);
	if (!class_dir) {
		syslog(LOG_ERR, "Failed to open " SYS_INFINIBAND);
		return;
	}

	while ((dent = readdir(class_dir))) {
		if (dent->d_name[0] == '.')
			continue;

		if (update_node_desc(dent->d_name, hostname, force))
			syslog(LOG_DEBUG, "set Node Description failed on %s\n",
			       dent->d_name);
	}

	closedir(class_dir);
}

static void read_hostname(int fd, char *name, size_t len)
{
	memset(name, 0, len);
	if (read(fd, name, len-1) >= 0) {
		newline_to_null(name);
		strip_domain(name);
	} else {
		syslog(LOG_ERR, "Read %s Failed\n", SYS_HOSTNAME);
	}
	lseek(fd, 0, SEEK_SET);
}

static void setup_udev(void)
{
	g_udev = udev_new();
	if (!g_udev) {
		syslog(LOG_ERR, "udev_new failed\n");
		return;
	}
}

static int get_udev_fd(void)
{
	g_mon = udev_monitor_new_from_netlink(g_udev, "udev");
	if (!g_mon) {
		syslog(LOG_ERR, "udev monitoring failed\n");
		return -1;
	}

	udev_monitor_filter_add_match_subsystem_devtype(g_mon, "infiniband", NULL);
	udev_monitor_enable_receiving(g_mon);
	return udev_monitor_get_fd(g_mon);
}

static void process_udev_event(int ud_fd, const char *hostname)
{
	struct udev_device *dev;

	dev = udev_monitor_receive_device(g_mon);
	if (dev) {
		const char *device = udev_device_get_sysname(dev);
		const char *action = udev_device_get_action(dev);

		dbg_log("Device event: %s, %s, %s\n",
			udev_device_get_subsystem(dev), device, action);

		if (device && action &&
		    (!strncmp(action, "add", sizeof("add")) ||
		     !strncmp(action, "move", sizeof("add"))))
			if (update_node_desc(device, hostname, 1))
				syslog(LOG_DEBUG, "set Node Description failed on %s\n",
				       device);

		udev_device_unref(dev);
	}
}

static void monitor(bool systemd)
{
	char hostname[128];
	int hn_fd;
	struct pollfd fds[2];
	int numfds = 1;
	int ud_fd;

	hn_fd = open(SYS_HOSTNAME, O_RDONLY);
	if (hn_fd < 0) {
		syslog(LOG_ERR, "Open %s Failed exiting\n",
			SYS_HOSTNAME);
		exit(EXIT_FAILURE);
	}

	read_hostname(hn_fd, hostname, sizeof(hostname));

	fds[0].fd = hn_fd;
	fds[0].events = 0;

	ud_fd = get_udev_fd();
	if (ud_fd >= 0)
		numfds = 2;

	fds[1].fd = ud_fd;
	fds[1].events = POLLIN;

	if (systemd)
		sd_notify(0, "READY=1");

	set_rdma_node_desc((const char *)hostname, 1);

	while (1) {
		if (poll(fds, numfds, -1) <= 0) {
			syslog(LOG_ERR, "Poll %s failed; exiting\n", SYS_HOSTNAME);
			exit(EXIT_FAILURE);
		}

		if (fds[0].revents != 0) {
			read_hostname(hn_fd, hostname, sizeof(hostname));
			dbg_log("Hostname event: %s\n", hostname);
			set_rdma_node_desc((const char *)hostname, 0);
		}

		if (fds[1].revents != 0)
			process_udev_event(ud_fd, hostname);
	}
}

int main(int argc, char *argv[])
{
	bool foreground = false;
	bool systemd = false;

	openlog(NULL, LOG_NDELAY | LOG_CONS | LOG_PID, LOG_DAEMON);

	while (1) {
		static const struct option long_opts[] = {
			{ "foreground",   0, NULL, 'f' },
			{ "systemd",      0, NULL, 's' },
			{ "help",         0, NULL, 'h' },
			{ "debug",        0, NULL, 'd' },
			{ }
		};

		int c = getopt_long(argc, argv, "fh", long_opts, NULL);
		if (c == -1)
			break;

		switch (c) {
		case 'f':
			foreground = true;
			break;
		case 's':
			systemd = true;
			break;
		case 'd':
			debugging = true;
			break;
		case 'h':
			printf("rdma-ndd [options]\n");
			printf("    See 'man rdma-ndd' for details\n");
			return 0;
		default:
			break;

		}
	}

	if (!foreground && !systemd) {
		if (daemon(0, 0) != 0) {
			syslog(LOG_ERR, "Failed to daemonize\n");
			return EXIT_FAILURE;
		}
	}

	setup_udev();

	g_nd_format = getenv("RDMA_NDD_ND_FORMAT");
	if (g_nd_format && strncmp("", g_nd_format, strlen(g_nd_format)) != 0)
		g_nd_format = strdup(g_nd_format);
	else
		g_nd_format = strdup(DEFAULT_ND_FORMAT);

	dbg_log("Node Descriptor format (%s)\n", g_nd_format);

	monitor(systemd);

	return 0;
}
