/*
 * Copyright (c) 2010 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Adam Hamsik.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/param.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <prop/proplib.h>

#include <dm.h>

#ifdef RUMP_ACTION
#include <rump/rump.h>
#include <rump/rumpclient.h>
#include <rump/rump_syscalls.h>
#endif

/* dmctl command is used to communicate with device-mapper driver in NetBSD
 * it uses libdm library to create and send required data to kernel.
 *
 * Main purpose of dmctl is to add possibility to use device-mapper driver
 * from outside of LVM scope.
 */

#define DMCTL_CMD_REQ_NODEVNAME 0	/* Command do not require device name */
#define DMCTL_CMD_REQ_DEVNAME	1	/* Command require device name to work */
#define DMCTL_CMD_REQ_NEWNAME	2	/* Command require device name and
					   newname to work */
struct command {
	const char *cmd_name;
	const char *cmd_help;
	const char *ioctl_cmd_name;
	int min_args;
	int (*cmd_func)(int, char *[], libdm_task_t);
};

static const   char *dvname;            /* device name */
static const   char *cmdname;           /* command user issued */

static char * parse_stdin(char *);

static int dmctl_get_version(int, char *[], libdm_task_t);
static int dmctl_get_targets(int, char *[], libdm_task_t);
static int dmctl_get_device_info(int, char *[], libdm_task_t);
static int dmctl_create_dev(int, char *[], libdm_task_t);
static int dmctl_dev_rename(int, char *[], libdm_task_t);
static int dmctl_dev_remove(int, char *[], libdm_task_t);
static int dmctl_dev_resume(int, char *[], libdm_task_t);
static int dmctl_dev_suspend(int, char *[], libdm_task_t);
static int dmctl_dev_deps(int, char *[], libdm_task_t);
static int dmctl_list_devices(int, char *[], libdm_task_t);
static int dmctl_table_reload(int, char *[], libdm_task_t);
static int dmctl_table_status(int, char *[], libdm_task_t);
__dead static void usage(void);

static struct command commands[] = {
	{ "version",
	  "Print driver and lib version.",
	  NULL, DMCTL_CMD_REQ_NODEVNAME,
	  dmctl_get_version },
	{ "targets",
	  "List available kernel targets.",
	  NULL, DMCTL_CMD_REQ_NODEVNAME,
	  dmctl_get_targets },
	{ "create",
	  "Create device with [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_create_dev },
	{ "ls",
	  "List existing dm devices.",
	  "names", DMCTL_CMD_REQ_NODEVNAME,
	  dmctl_list_devices },
	{ "info",
	  "Get info about device with [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_get_device_info },
	{ "rename",
	  "Rename device with [dm device name] to  [dm device new name].",
	  NULL, DMCTL_CMD_REQ_NEWNAME,
	  dmctl_dev_rename },
	{ "remove",
	  "Remove device with [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_dev_remove },
	{ "resume",
	  "Resume IO on dm device [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_dev_resume },
	{ "suspend",
	  "Suspend IO on dm device [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_dev_suspend },
	{ "deps",
	  "Print physical dependencies for dm device [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_dev_deps },
	{ "reload",
	  "Switch active and passive tables for device with [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_table_reload },
	{ "status",
	  "Print status for device with [dm device name].",
	  "table", DMCTL_CMD_REQ_DEVNAME,
	  dmctl_table_status },
	{ "table",
	  "Print active table for device with [dm device name].",
	  NULL, DMCTL_CMD_REQ_DEVNAME,
	  dmctl_table_status },
	{ NULL,
	  NULL,
	  NULL, 0,
	  NULL },
};

int
main(int argc, char *argv[])
{
	int i;
	int oargc;
	libdm_task_t task;

	oargc = 0;

#ifdef RUMP_ACTION
	if (rumpclient_init() == -1)
	  err(EXIT_FAILURE, "rump client init failed");
#endif

	/* Must have at least: device command */
	if (argc < 2)
		usage();

	/* Skip program name, get and skip device name and command. */
	cmdname = argv[1];
	if (argc > 2) {
		oargc = 1;
		dvname = argv[2];
	}

	if (argc > 3) {
		argv += 3;
		argc -= 3;
		oargc = 2;
	} else {
		argv = 0;
		argc = 0;
	}

	for (i = 0; commands[i].cmd_name != NULL; i++)
		if (strcmp(cmdname, commands[i].cmd_name) == 0)
			break;

	if (commands[i].cmd_name == NULL)
		errx(EXIT_FAILURE, "unknown command: %s", cmdname);

	if (commands[i].ioctl_cmd_name != NULL)
		cmdname = commands[i].ioctl_cmd_name;

	if (oargc != commands[i].min_args) {
		(void)fprintf(stderr, "Insufficient number of arguments for "
		    "command: %s specified\n", commands[i].cmd_name);
		usage();
	}

	/*
	 * Create libdm task, and pass it to command handler later.
	 * Don't release it here because it will be replaced by different
	 * dictionary received from kernel after libdm_task_run.
	 */
	task = libdm_task_create(cmdname);

	(*commands[i].cmd_func)(argc, argv, task);

	return 0;
}

/*
 * Print library and kernel driver versions if command can be used only when
 * major, minor number of library version is <= kernel.
 */
static int
dmctl_get_version(int argc __unused, char *argv[] __unused, libdm_task_t task)
{
	uint32_t ver[3];

	(void)libdm_task_get_cmd_version(task, ver, sizeof(ver));

	printf("Library protocol version %d:%d:%d\n", ver[0], ver[1], ver[2]);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_get_version: libdm_task_run failed.");

	(void)libdm_task_get_cmd_version(task, ver, 3);
	printf("Kernel protocol version %d:%d:%d\n",ver[0], ver[1], ver[2]);

	libdm_task_destroy(task);
	return 0;
}

/*
 * Get list of available targets from kernel and print them.
 */
static int
dmctl_get_targets(int argc __unused, char *argv[] __unused, libdm_task_t task)
{
	libdm_cmd_t cmd;
	libdm_iter_t iter;
	libdm_target_t target;
	uint32_t ver[3];

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_get_targets: libdm_task_run failed.");

	if ((cmd = libdm_task_get_cmd(task)) == NULL)
		return ENOENT;

	iter = libdm_cmd_iter_create(cmd);

	while((target = libdm_cmd_get_target(iter)) != NULL){
		printf("Target name: %s\n", libdm_target_get_name(target));

		libdm_target_get_version(target, ver, sizeof(ver));
		printf("Target version %d.%d.%d\n\n", ver[0], ver[1], ver[2]);

		libdm_target_destroy(target);
	}

	libdm_iter_destroy(iter);
	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);

	return 0;
}

/*
 * Create device with name used as second parameter.
 * TODO: Support for UUIDs here.
 */
static int
dmctl_create_dev(int argc __unused, char *argv[] __unused, libdm_task_t task)
{

	libdm_task_set_name(dvname, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_create_dev: libdm_task_run failed.");

	libdm_task_destroy(task);
	return 0;
}

/*
 * Get basic device info from device-mapper driver.
 */
static int
dmctl_get_device_info(int argc __unused, char *argv[] __unused, libdm_task_t task)
{

	libdm_task_set_name(dvname, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "%s: libdm_task_run failed", __func__);

	printf("Printing Device info for:\n");
	printf("Device name: \t\t%s\n", libdm_task_get_name(task));
	printf("Device uuid: \t\t%s\n", libdm_task_get_uuid(task));
	printf("Device minor: \t\t%d\n", libdm_task_get_minor(task));
	printf("Device target number: \t%d\n", libdm_task_get_target_num(task));
	printf("Device flags: \t\t%d\n", libdm_task_get_flags(task));

	libdm_task_destroy(task);
	return 0;
}

/*
 * List all device in device-mapper driver.
 */
static int
dmctl_list_devices(int argc __unused, char *argv[] __unused, libdm_task_t task)
{
	libdm_cmd_t cmd;
	libdm_iter_t iter;
	libdm_dev_t dev;

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_list_devices: libdm_task_run failed.");

	if ((cmd = libdm_task_get_cmd(task)) == NULL)
		return ENOENT;

	iter = libdm_cmd_iter_create(cmd);

	while((dev = libdm_cmd_get_dev(iter)) != NULL){
		printf("Device name: %s, device minor: %d \n",
		    libdm_dev_get_name(dev), libdm_dev_get_minor(dev));
		libdm_dev_destroy(dev);
	}

	libdm_iter_destroy(iter);
	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);

	return 0;
}

/*
 * Rename device to new name
 */
static int
dmctl_dev_rename(int argc __unused, char *argv[], libdm_task_t task)
{
	libdm_cmd_t cmd;

	libdm_task_set_name(dvname, task);

	cmd = libdm_cmd_create();
	libdm_dev_set_newname(argv[0], cmd);
	libdm_task_set_cmd(cmd, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_dev_rename: libdm_task_run failed.");

	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);

	return 0;
}

/*
 * Remove device from dm device list.
 */
static int
dmctl_dev_remove(int argc __unused, char *argv[] __unused, libdm_task_t task)
{

	if (dvname == NULL)
		return (ENOENT);

	libdm_task_set_name(dvname, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_dev_remove: libdm_task_run failed.");

	libdm_task_destroy(task);
	return 0;
}

/*
 * Resume device which was suspended or created right now.
 * Replace table in "active slot" with table in "inactive slot".
 */
static int
dmctl_dev_resume(int argc __unused, char *argv[] __unused, libdm_task_t task)
{

	libdm_task_set_name(dvname, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_dev_resume: libdm_task_run failed.");

	libdm_task_destroy(task);
	return 0;
}

/*
 * Resume device which was suspended or created right now.
 * Replace table in "active slot" with table in "inactive slot".
 */
static int
dmctl_dev_suspend(int argc __unused, char *argv[] __unused, libdm_task_t task)
{

	libdm_task_set_name(dvname, task);
	libdm_task_set_suspend_flag(task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_dev_suspend: libdm_task_run failed.");

	libdm_task_destroy(task);
	return 0;
}

/*
 * Get device dependencies from device-mapper. Device dependency is physical
 * device on which dm device depends.
 */
static int
dmctl_dev_deps(int argc __unused, char *argv[] __unused, libdm_task_t task)
{
	libdm_cmd_t cmd;
	libdm_iter_t iter;
	dev_t dev_deps;

	libdm_task_set_name(dvname, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "dmctl_dev_deps: libdm_task_run failed.");

	if ((cmd = libdm_task_get_cmd(task)) == NULL)
		return ENOENT;

	iter = libdm_cmd_iter_create(cmd);

	printf("Device %s dependencies \n", dvname);

	while((dev_deps = libdm_cmd_get_deps(iter)) != 0)
		printf("major: %d minor: %d\n", major(dev_deps), minor(dev_deps));

	libdm_iter_destroy(iter);
	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);

	return 0;
}

/*
 * Reload device table to get new one to use.
 */
static int
dmctl_table_reload(int argc, char *argv[], libdm_task_t task)
{
	libdm_cmd_t cmd;
	libdm_table_t table;

	char *params;
	char *file_path;
	char target[128];
	int len;
	uint64_t start, length;

	file_path = NULL;
	params = NULL;

	cmd = libdm_cmd_create();

	libdm_task_set_name(dvname, task);

	if (argc != 0)
		file_path = argv[0];

	while ((params = parse_stdin(file_path)) != NULL) {
		table = libdm_table_create();

		sscanf(params, "%"PRIu64" %"PRIu64" %s %n", &start, &length, target, &len);

		libdm_table_set_start(start, table);
		libdm_table_set_length(length, table);
		libdm_table_set_target(target, table);
		libdm_table_set_params(params + len, table);
		libdm_cmd_set_table(table, cmd);

		libdm_table_destroy(table);

		free(params);
	}

	libdm_task_set_cmd(cmd, task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "libdm_task_run: from dmctl_table_reload failed.");

	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);
	free(params);

	return 0;
}

/*
 * Get table status from device.
 */
static int
dmctl_table_status(int argc __unused, char *argv[] __unused, libdm_task_t task)
{
	libdm_cmd_t cmd;
	libdm_table_t table;
	libdm_iter_t iter;

	libdm_task_set_name(dvname, task);
	libdm_task_set_status_flag(task);

	if (libdm_task_run(task) != 0)
		err(EXIT_FAILURE, "libdm_task_run");

	if ((cmd = libdm_task_get_cmd(task)) == NULL)
		return ENOENT;

	iter = libdm_cmd_iter_create(cmd);

	printf("Getting device table for device %s\n", dvname);

	while ((table = libdm_cmd_get_table(iter)) != NULL) {
		printf("%10"PRIu64" %10"PRIu64" %s\n",
			libdm_table_get_start(table),
			libdm_table_get_length(table),
			libdm_table_get_target(table));
		libdm_table_destroy(table);
	}

	libdm_iter_destroy(iter);
	libdm_cmd_destroy(cmd);
	libdm_task_destroy(task);

	return 0;
}

static void
usage(void)
{
	int i;

	(void)fprintf(stderr, "usage: %s command [dm device name]\n"
	    "Available commands are:\n ", getprogname());
	for (i = 0; commands[i].cmd_name != NULL; i++)
		(void)fprintf(stderr, "\t%s\t%s\n", commands[i].cmd_name, commands[i].cmd_help);
	exit(EXIT_FAILURE);
}

static char *
parse_stdin(char *file_path)
{
	char *buf, *lbuf;
	size_t len;
	FILE *fp;

	lbuf = NULL;

	if (file_path) {
		if ((fp = fopen(file_path, "r")) == NULL)
			err(ENOENT, "Cannot open table file");
	} else
		fp = stdin;

	if ((buf = fgetln(fp, &len)) == NULL)
		return NULL;

	if (buf[len - 1] != '\n')
		len++;

	if ((lbuf = malloc(len)) == NULL)
		err(EXIT_FAILURE, "malloc");

	memcpy(lbuf, buf, len);
	lbuf[len - 1] = '\0';

	return lbuf;
}
