/*
 * ProjectAcrn
 * Acrnctl
 *
 * Copyright (C) 2018-2022 Intel Corporation.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *
 *
 * Author: Tao Yuhong <yuhong.tao@intel.com>
 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdbool.h>
#include "acrn_mngr.h"
#include "acrnctl.h"
#include "ioc.h"

#define ACMD(CMD,FUNC,DESC, VALID_ARGS) \
{.cmd = CMD, .func = FUNC, .desc = DESC, .valid_args = VALID_ARGS}

/* vm life cycle cmd description */
#define LIST_DESC      "List all the virtual machines added"
#define START_DESC     "Start virtual machine VM_NAME"
#define STOP_DESC      "Stop virtual machine VM_NAME, [--force/-f, force to stop VM]"
#define DEL_DESC       "Delete virtual machine VM_NAME"
#define ADD_DESC       "Add one virtual machine with SCRIPTS and OPTIONS"
#define RESET_DESC     "Stop and then start virtual machine VM_NAME"
#define BLKRESCAN_DESC  "Rescan virtio-blk device attached to a virtual machine"

#define VM_NAME (1)
#define CMD_ARGS (2)

#define STOP_TIMEOUT	30U

struct acrnctl_cmd {
	const char *cmd;
	const char desc[128];	/* Description of the cmd */
	int (*func) (int argc, char *argv[]);
	/* Callback function to check whether args is valid */
	int (*valid_args) (struct acrnctl_cmd * cmd, int argc, char *argv[]);
};

/* There are acrnctl cmds */
/* command: list */
static int acrnctl_do_list(int argc, char *argv[])
{
	return list_vm();
}

static int check_name(const char *name)
{
	int i = 0, j = 0;
	char illegal[] = "!@#$%^&*, ";

	/* Name should start with a letter */
	if ((name[0] < 'a' || name[0] > 'z')
	    && (name[0] < 'A' || name[0] > 'Z')) {
		printf("name not started with letter!\n");
		return -1;
	}

	/* no illegal charactoer */
	while (name[i]) {
		j = 0;
		while (illegal[j]) {
			if (name[i] == illegal[j]) {
				printf("vmname[%d] is '%c'!\n", i, name[i]);
				return -1;
			}
			j++;
		}
		i++;
	}

	if (!strcmp(name, "help"))
		return -1;
	if (!strcmp(name, "nothing"))
		return -1;

	if (strnlen(name, MAX_VM_NAME_LEN) >= MAX_VM_NAME_LEN) {
		printf("(%s) size exceed MAX_VM_NAME_LEN:%u\n", name, MAX_VM_NAME_LEN);
		return -1;
	}

	return 0;
}

static const char *acrnctl_bin_path;
static int find_acrn_dm;
#define MAX_WORD   64

static int write_tmp_file(int fd, int n, char *word[])
{
	int len, ret, i = 0;
	char buf[PATH_LEN];

	if (!n)
		return 0;

	len = strnlen(word[0], MAX_WORD);
	if (len >= strlen("acrn-dm")) {
		if (!strcmp(word[0] + len - strlen("acrn-dm"), "acrn-dm")) {
			find_acrn_dm++;
			memset(buf, 0, sizeof(buf));
			if (snprintf(buf, sizeof(buf), "%s gentmpfile",
						acrnctl_bin_path) >= sizeof(buf)) {
				printf("ERROR: acrnctl bin path is truncated\n");
				return -1;
			}
			ret = write(fd, buf, strnlen(buf, sizeof(buf)));
			if (ret < 0)
				return -1;
			i++;
		}
	}

	while (i < n) {
		memset(buf, 0, sizeof(buf));
		if (snprintf(buf, sizeof(buf), " %s", word[i]) >= sizeof(buf))
			printf("WARN: buf is truncated\n");
		i++;
		ret = write(fd, buf, strnlen(buf, sizeof(buf)));
		if (ret < 0)
			return -1;
	}
	ret = write(fd, "\n", 1);
	if (ret < 0)
		return -1;
	return 0;
}

/*
 * get vmname from the string src, and src
 * format is "acrnctl: [vmname]"
 */
static inline int _get_vmname(const char *src, char *vmname, int max_len_vmname)
{
	const char *vmname_p = NULL;

	if (!strncmp("acrnctl: ", src, strlen("acrnctl: "))) {
		vmname_p = src + strlen("acrnctl: ");

		memset(vmname, 0, max_len_vmname);
		strncpy(vmname, vmname_p, max_len_vmname);
		if(vmname[max_len_vmname - 1]) {
			/* vmname is truncated */
			printf("get vmname failed, vmname is truncated\n");
			return -1;
		}
	} else {
		/* the prefix of the string "src" isn't "acrnctl: " */
		printf("can't found prefix 'acrnctl: '\n");
		return -1;
	}

	return 0;
}

#define MAX_FILE_SIZE   (4096 * 5)

#define TMP_FILE_SUFFIX		".acrnctl"

static int acrnctl_do_add(int argc, char *argv[])
{
	struct vmmngr_struct *s;
	int fd, fd_tmp, ret = 0;
	char *buf;
	char *word[MAX_WORD], *line;
	char *word_p = NULL, *line_p = NULL;
	int n_word;
	char fname[PATH_LEN + sizeof(TMP_FILE_SUFFIX)];
	char cmd[PATH_LEN];
	char args[PATH_LEN];
	int p, i, len_cmd_out = 0, c_flag = 0;
	char cmd_out[PATH_LEN * 2];
	char vmname[PATH_LEN];
	size_t len = sizeof(cmd_out);

	if (strnlen(argv[1], PATH_LEN) == PATH_LEN) {
		printf("File name too long (maximum len %d)\n", PATH_LEN);
		return -1;
	}

	memset(args, 0, sizeof(args));
	p = 0;
	for (i = 2; i < argc; i++) {
		if (p >= sizeof(args) - 1) {
			args[sizeof(args) - 1] = 0;
			printf("Too many input options\n");
			return -1;
		}

		/*
		 * If there's "-C" parameter in acrnctl add command
		 * check if the SoS support runC container at first, then
		 * strip "-C" and set the flag.
		*/
		if (strncmp(argv[i], "-C", 2) == 0) {
			if (access("/sbin/runc", F_OK) != 0) {
				printf("runC command not supproted\n");
				return -1;
			}
                        c_flag = 1;
                        continue;
                }

		p += snprintf(&args[p], sizeof(args) - p, " %s", argv[i]);
	}
	args[p] = ' ';

	fd = open(argv[1], O_RDONLY);
	if (fd < 0) {
		perror(argv[1]);
		ret = -1;
		goto open_read_file;
	}

	buf = calloc(1, MAX_FILE_SIZE);
	if (!buf) {
		perror("calloc for add vm");
		ret = -1;
		goto calloc_err;
	}

	ret = read(fd, buf, MAX_FILE_SIZE);
	if (ret >= MAX_FILE_SIZE) {
		printf("%s exceed MAX_FILE_SIZE:%d", argv[1], MAX_FILE_SIZE);
		ret = -1;
		goto file_exceed;
	}

	/* open tmp file for write */
	memset(fname, 0, sizeof(fname));
	if (snprintf(fname, sizeof(fname), "%s%s", argv[1], TMP_FILE_SUFFIX)
			>= sizeof(fname)) {
		printf("ERROR: file name is truncated\n");
		ret = -1;
		goto file_exceed;
	}
	fd_tmp = open(fname, O_RDWR | O_CREAT | O_TRUNC, 0666);
	if (fd_tmp < 0) {
		perror(fname);
		ret = -1;
		goto open_tmp_file;
	}

	find_acrn_dm = 0;

	/* Properly null-terminate buf */
	buf[MAX_FILE_SIZE - 1] = '\0';

	line = strtok_r(buf, "\n", &line_p);
	while (line) {
		word_p = NULL;
		n_word = 0;
		word[n_word] = strtok_r(line, " ", &word_p);
		while ((n_word < (MAX_WORD-1)) && word[n_word]) {
			n_word++;
			word[n_word] = strtok_r(NULL, " ", &word_p);
		}
		if (write_tmp_file(fd_tmp, n_word, word)) {
			ret = -1;
			perror(fname);
			goto write_tmpfile;
		}
		line = strtok_r(NULL, "\n", &line_p);
	}

	if (!find_acrn_dm) {
		printf("No 'acrn-dm' found in %s, expecting 'acrn-dm' command to get vmname.\n", argv[1]);
		ret = -1;
		goto no_acrn_dm;
	}

	if (snprintf(cmd, sizeof(cmd), "mv %s %s.back", argv[1], argv[1])
			>= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
		goto get_vmname;
	}
	system(cmd);

	if (snprintf(cmd, sizeof(cmd), "mv %s %s", fname, argv[1]) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
		goto get_vmname;
	}
	system(cmd);

	if (snprintf(cmd, sizeof(cmd), "bash %s%s > %s.result", argv[1],
			args, argv[1]) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1 ;
		goto get_vmname;
	}
	ret = shell_cmd(cmd, cmd_out, sizeof(cmd_out));
	if (ret < 0)
		goto get_vmname;

	if (snprintf(cmd, sizeof(cmd), "grep -a \"acrnctl: \" %s.result",
			argv[1]) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
		goto get_vmname;
	}
	len_cmd_out = shell_cmd(cmd, cmd_out, sizeof(cmd_out));
	if (len_cmd_out < 0) {
		ret = len_cmd_out;
		goto get_vmname;
	}

	if(cmd_out[len_cmd_out - 1] == '\n')
		cmd_out[len_cmd_out - 1] = '\0';

	ret = _get_vmname(cmd_out, vmname, sizeof(vmname));
	if (ret < 0) {
		/* failed to get vmname */
		if (snprintf(cmd, sizeof(cmd), "cat %s.result", argv[1]) >= sizeof(cmd)) {
			printf("ERROR: cmd is truncated\n");
			goto get_vmname;
		}
		shell_cmd(cmd, cmd_out, sizeof(cmd_out));

		/* Properly null-terminate cmd_out */
		cmd_out[len - 1] = '\0';

		printf("No executable acrn-dm found: %s, ,please make sure it can launch an User VM\n"
		       "result:\n%s\n", argv[1], cmd_out);
		goto get_vmname;
	}

	ret = check_name(vmname);
	if (ret) {
		printf("\"%s\" is a bad name, please select another name\n",
		       vmname);
		goto get_vmname;
	}

	s = vmmngr_find(vmname);
	if (s) {
		printf("%s(%s) already exist, can't add %s%s\n",
		       vmname, state_str[s->state], argv[1], args);
		ret = -1;
		goto vm_exist;
	}

	if (snprintf(cmd, sizeof(cmd), "cp %s.back %s/%s.sh", argv[1],
		 ACRN_CONF_PATH_ADD, vmname) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
		goto vm_exist;
	}
	system(cmd);

	/* If c_flag have been seted, add stripped "-C" to args file */
	if (c_flag)
		strncpy(args + p + 1, "-C", 2);
	if (snprintf(cmd, sizeof(cmd), "echo %s >%s/%s.args", args,
		 ACRN_CONF_PATH_ADD, vmname) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
		goto vm_exist;
	}
	system(cmd);
	printf("%s added\n", vmname);

 vm_exist:
 get_vmname:
	if (snprintf(cmd, sizeof(cmd), "rm -f %s.result", argv[1]) >= sizeof(cmd)) {
		printf("WARN: cmd is truncated\n");
	} else
		system(cmd);

	if (snprintf(cmd, sizeof(cmd), "mv %s %s", argv[1], fname) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
	} else
		system(cmd);

	if (snprintf(cmd, sizeof(cmd), "mv %s.back %s", argv[1], argv[1]) >= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		ret = -1;
	} else
		system(cmd);

 no_acrn_dm:
	if (snprintf(cmd, sizeof(cmd), "rm -f %s", fname) >= sizeof(cmd)) {
		 printf("WARN: cmd is truncated\n");
	} else
		system(cmd);
 write_tmpfile:
	close(fd_tmp);
 open_tmp_file:
 file_exceed:
	free(buf);
 calloc_err:
	close(fd);
 open_read_file:
	return ret;
}

static int acrnctl_do_blkrescan(int argc, char *argv[])
{
	struct vmmngr_struct *s;

	s = vmmngr_find(argv[VM_NAME]);
	if (!s) {
		printf("can't find %s\n", argv[VM_NAME]);
		return -1;
	}
	if (s->state != VM_STARTED) {
		printf("%s is in %s state but should be in %s state for blockrescan\n",
			argv[VM_NAME], state_str[s->state], state_str[VM_STARTED]);
		return -1;
	}

	blkrescan_vm(argv[VM_NAME], argv[CMD_ARGS]);

	return 0;
}

static int acrnctl_do_stop(int argc, char *argv[])
{
	struct vmmngr_struct *s;
	int i, force = 0;
	const char *vmname = NULL;

	for (i = 1; i < argc; i++) {
		if (strcmp(argv[i], "--force") && strcmp(argv[i], "-f")) {
			if (vmname == NULL)
				vmname = argv[i];
		} else {
			force = 1;
		}
	}

	if (!vmname) {
		printf("Please give a VM name\n");
		return -1;
	}

	s = vmmngr_find(vmname);
	if (!s) {
		printf("can't find %s\n", vmname);
		return -1;
	}
	if (s->state == VM_CREATED) {
		printf("%s is already (%s)\n", vmname, state_str[s->state]);
		return -1;
	}

	return stop_vm(vmname, force);

}

/* Function: Delete runC configuration */
static inline int del_runC(char *argv)
{
	char cmd[PATH_LEN];
	char cmd_out[PATH_LEN * 2];
	char runc_path[PATH_LEN];

	/* The configuration added by launch_uos script */
	if (snprintf(runc_path, sizeof(runc_path), "%s/runc/%s",
			ACRN_CONF_PATH_ADD, argv) >= sizeof(runc_path)) {
		printf("ERROR: runC path %s is truncated\n", runc_path);
		return -1;
	}
	if (access(runc_path, F_OK) != 0)
		return 0;

	/* Check if there is a container */
	if (snprintf(cmd, sizeof(cmd), "runc list | grep %s",
				argv) >= sizeof(cmd)) {
		printf("ERROR: runc cmd: %s is truncated\n", cmd);
		return -1;
	}
	shell_cmd(cmd, cmd_out, sizeof(cmd_out));
	cmd_out[PATH_LEN * 2 - 1] = '\0';
	if (strstr(cmd_out, argv) != NULL) {
		/* If the container is still running stop it by runc pause */
		if (strstr(cmd_out, "stopped") == NULL) {
			printf("Pls stop the runC before del it !!\n");
			return -1;
		}
		if (snprintf(cmd, sizeof(cmd), "runc delete %s",
				argv) >= sizeof(cmd)) {
			printf("ERROR: del container: %s trancated!", cmd);
			return -1;
		}
		system(cmd);
		if (snprintf(cmd, sizeof(cmd), "rm -f %s/runc/%s -rf",
				ACRN_CONF_PATH_ADD, argv) >= sizeof(cmd)) {
			printf("ERROR: delete cmd: %s trancated!\n", cmd);
			return -1;
		}
		system(cmd);
	}

	return 0;
}

/* command: delete */
static int acrnctl_do_del(int argc, char *argv[])
{
	struct vmmngr_struct *s;
	char cmd[PATH_LEN];

	s = vmmngr_find(argv[1]);
	if (!s) {
		printf("can't find %s\n", argv[1]);
		return -1;
	}
	if (s->state != VM_CREATED) {
		printf("can't delete %s(%s)\n", argv[1], state_str[s->state]);
		return -1;
	}
	if (snprintf(cmd, sizeof(cmd), "rm -f %s/%s.sh", ACRN_CONF_PATH_ADD, argv[1]) >= sizeof(cmd)) {
		printf("WARN: cmd is truncated\n");
		return -1;
	}
	system(cmd);
	if (snprintf(cmd, sizeof(cmd), "rm -f %s/%s.args", ACRN_CONF_PATH_ADD, argv[1]) >= sizeof(cmd)) {
		printf("WARN: cmd is truncated\n");
		return -1;
	}
	system(cmd);
	if (del_runC(argv[1]) < 0) {
		printf("ERROR: del runC failed!\n");
		return -1;
	}

	return 0;
}

static int acrnctl_do_start(int argc, char *argv[])
{
	struct vmmngr_struct *s;

	s = vmmngr_find(argv[1]);
	if (!s) {
		printf("can't find %s\n", argv[1]);
		return -1;
	}

	if (s->state != VM_CREATED) {
		printf("can't start %s(%s)\n", argv[1], state_str[s->state]);
		return -1;
	}

	return start_vm(argv[1]);

}

static int wait_vm_stop(const char * vmname, unsigned int timeout)
{
	unsigned long t = timeout;
	struct vmmngr_struct *s;

	do {
		/* list and update the vm status */
		vmmngr_update();

		s =  vmmngr_find(vmname);
		if (s == NULL) {
			printf("%s: vm %s not found\n", __func__, vmname);
			return -1;
		} else {
			if (s->state == VM_CREATED) {
				sleep(2);
				return 0;
			}
		}

		sleep(1);
	} while (t--);

	return -1;
}

static int acrnctl_do_reset(int argc, char *argv[])
{
	struct vmmngr_struct *s;
	int ret = -1;

	s = vmmngr_find(argv[1]);
	if (!s) {
		printf("Can't find vm %s\n", argv[1]);
		return ret;
	}

	switch(s->state) {
		case VM_STARTED:
		case VM_SUSPENDED:
			ret = stop_vm(argv[1], 0);
			if (ret != 0) {
				break;
			}
			if (wait_vm_stop(argv[1], STOP_TIMEOUT)) {
				printf("Failed to stop %s in %u sec, reset failed\n", argv[1], STOP_TIMEOUT);
				break;
			}
			ret = start_vm(argv[1]);
			break;
		default:
			printf("%s current state: %s, can't reset\n", argv[1], state_str[s->state]);
	}
	return ret;
}

/* Default args validation function */
int df_valid_args(struct acrnctl_cmd *cmd, int argc, char *argv[])
{
	char df_opt[32] = "VM_NAME VM_NAME ...";

	if (argc < 2 || !strcmp(argv[1], "help")) {
		printf("acrnctl %s %s\n", cmd->cmd, df_opt);
		return -1;
	}

	return 0;
}

static int valid_blkrescan_args(struct acrnctl_cmd *cmd, int argc, char *argv[])
{
	char df_opt[] = "VM_NAME slot,newpath";

	if (argc != 3 || !strcmp(argv[1], "help")) {
		printf("acrnctl %s %s\n", cmd->cmd, df_opt);
		return -1;
	}

	return 0;
}

static int valid_add_args(struct acrnctl_cmd *cmd, int argc, char *argv[])
{
	char df_opt[32] = "launch_scripts options";

	if (argc < 2 || !strcmp(argv[1], "help")) {
		printf("acrnctl %s %s\n", cmd->cmd, df_opt);
		return -1;
	}

	return 0;
}

static int valid_start_args(struct acrnctl_cmd *cmd, int argc, char *argv[])
{
	char df_opt[16] = "VM_NAME";

	if (argc != 2 || !strcmp(argv[1], "help")) {
		printf("acrnctl %s %s\n", cmd->cmd, df_opt);
		return -1;
	}

	return 0;
}

static int valid_list_args(struct acrnctl_cmd *cmd, int argc, char *argv[])
{
	if (argc != 1) {
		printf("acrnctl %s\n", cmd->cmd);
		return -1;
	}

	return 0;
}

struct acrnctl_cmd acmds[] = {
	ACMD("list", acrnctl_do_list, LIST_DESC, valid_list_args),
	ACMD("start", acrnctl_do_start, START_DESC, valid_start_args),
	ACMD("stop", acrnctl_do_stop, STOP_DESC, df_valid_args),
	ACMD("del", acrnctl_do_del, DEL_DESC, df_valid_args),
	ACMD("add", acrnctl_do_add, ADD_DESC, valid_add_args),
	ACMD("reset", acrnctl_do_reset, RESET_DESC, df_valid_args),
	ACMD("blkrescan", acrnctl_do_blkrescan, BLKRESCAN_DESC, valid_blkrescan_args),
};

#define NCMD	(sizeof(acmds)/sizeof(struct acrnctl_cmd))

static void usage(void)
{
	int i;

	printf("\nUsage: acrnctl SUB-CMD "
		"{ VM_NAME | SCRIPTS OPTIONS | help }\n\n");
	for (i = 0; i < NCMD; i++)
		printf("\t%-12s%s\n", acmds[i].cmd, acmds[i].desc);
}

int main(int argc, char *argv[])
{
	int i, err;
	char cmd[PATH_LEN];

	if (argc == 1 || !strcmp(argv[1], "help")) {
		usage();
		return 0;
	}

	if (getuid() != 0) {
		printf("Please run acrnctl with root privileges. Exiting.\n");
		return -1;
	}

	acrnctl_bin_path = argv[0];

	/* Create the acrnctl conf folder if it does not exist yet */
	if (snprintf(cmd, sizeof(cmd), "mkdir -p %s", ACRN_CONF_PATH_ADD)
			>= sizeof(cmd)) {
		printf("ERROR: cmd is truncated\n");
		return -1;
	}
	system(cmd);

	/* first check acrnctl reserved operations */
	if (!strcmp(argv[1], "gentmpfile")) {
		printf("\nacrnctl: %s\n", argv[argc - 1]);
		return 0;
	}

	for (i = 0; i < NCMD; i++)
		if (!strcmp(argv[1], acmds[i].cmd)) {
			if (acmds[i].valid_args(&acmds[i], argc - 1, &argv[1])) {
				return -1;
			} else {
				vmmngr_update();
				err = acmds[i].func(argc - 1, &argv[1]);
				return err;
			}
		}

	/* Reach here means unsupported command */
	printf("Unknown command: %s\n", argv[1]);
	usage();

	return -1;
}
