/*
 * cpuplugd - Linux for System z Hotplug Daemon
 *
 * Config file parsing
 *
 * Copyright IBM Corp. 2007, 2018
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include "cpuplugd.h"

/*
 * Return the value of a variable which parse_config() found within the
 * configuration file. Use only for values valid >= 0, because -1 is returned
 * in error case.
 */
static long parse_positive_value(char *ptr)
{
	long value = 0;
	unsigned int i;

	if (ptr == NULL)
		return -1;
	for (i = 0; i < strlen(ptr); i++) {
		if (isdigit(ptr[i]) == 0)
			return -1;
	}
	sscanf(ptr, "%ld", &value);
	return value;
}

char *get_var_rvalue(char *var_name)
{
	char tmp_name[MAX_VARNAME + 3]; /* +3 for '\0', '=' and '\n' */
	unsigned int tmp_length;
	char *rvalue;

	tmp_name[0] = '\n';
	strncpy(&tmp_name[1], var_name, MAX_VARNAME + 1); /* +1 for '\0' */
	tmp_length = strlen(tmp_name);
	tmp_name[tmp_length] = '=';
	tmp_name[tmp_length + 1] = '\0';
	rvalue = strstr(varinfo, tmp_name);
	if (rvalue == NULL)
		return NULL;
	rvalue += strlen(tmp_name);
	return rvalue;
}

static void add_var(char *name, char *rvalue)
{
	size_t offset, size;
	unsigned int i;

	if (get_var_rvalue(name))
		cpuplugd_exit("Variable defined twice: %s\n", name);
	for (i = 0; i < sym_names_count; i++) {
		if (strncmp(name, sym_names[i].name,
			    MAX(strlen(sym_names[i].name), strlen(name))) != 0)
			continue;
		cpuplugd_exit("Cannot use (pre-defined) variable name: %s\n",
			      name);
	}

	offset = strlen(varinfo);
	/* +3 because of extra '=', '\n' and '\0' */
	size = offset + strlen(name) + strlen(rvalue) + 3;
	if (size > varinfo_size)
		//TODO realloc?
		cpuplugd_exit("buffer for variables too small: need %ld, "
			      "have %ld (bytes)\n", size, varinfo_size);
	size -= offset;
	snprintf(&varinfo[offset], size, "%s=%s\n", name, rvalue);
	return;
}

static int check_term(char *symbol, char *name, char *rvalue, struct term **term)
{
	if (!strncasecmp(name, symbol, strlen(symbol))) {
		cpuplugd_debug("found the following rule: %s = %s\n",
			       name, rvalue);
		*term = parse_term(&rvalue, OP_PRIO_NONE);
		if (rvalue[0] == '\0')
			return 1;
		cpuplugd_exit("parsing error at %s, position: %s\n", symbol,
			      rvalue);
	}
	return 0;
}

static int check_value(char *symbol, char *name, char *rvalue, long *value)
{
	if (!strncasecmp(name, symbol, strlen(symbol))) {
		*value = parse_positive_value(rvalue);
		cpuplugd_debug("found %s value: %ld\n", symbol, *value);
		if (*value >= 0)
			return 1;
		cpuplugd_exit("parsing error at update\n");
	}
	return 0;
}

/*
 * Parse a single line of the configuration file
 */
static void parse_configline(char *line)
{
	char *match, *name, *rvalue, *start, *stop;
	int i, j;
	size_t len;
	char temp[strlen(line) + 1];

	if (line[0] == '#')
		return;
	for (i = j = 0; line[i] != 0; i++) /* Remove whitespace. */
		if (!isblank(line[i]) && !isspace(line[i]))
			temp[j++] = line[i];
	temp[j] = '\0';
	match = strchr(temp, '=');
	if (match == NULL)
		return;
	*match = '\0';		/* Separate name and right hand value */
	name = temp;		/* left side of = */
	rvalue = match + 1;	/* right side of = */
	/*
	 * remove the double quotes
	 * example:  CPU_MIN="2"
	 */
	start = strchr(rvalue, '"');	/* points to first " */
	stop = strrchr(rvalue, '"');	/* points to last " */
	len = stop - start;
	if (start != NULL && stop != NULL && len > 0) {
		rvalue[len] = '\0';
		rvalue = rvalue + 1;
	} else
		cpuplugd_exit("the configuration file has syntax "
			      "errors at %s, position: %s\n", name, rvalue);

	if (check_term("hotplug", name, rvalue, &cfg.hotplug))
		return;
	if (check_term("hotunplug", name, rvalue, &cfg.hotunplug))
		return;
	if (check_term("memplug", name, rvalue, &cfg.memplug))
		return;
	if (check_term("memunplug", name, rvalue, &cfg.memunplug))
		return;
	if (check_term("cmm_inc", name, rvalue, &cfg.cmm_inc))
		return;
	if (check_term("cmm_dec", name, rvalue, &cfg.cmm_dec))
		return;

	if (check_value("update", name, rvalue, &cfg.update)) {
		if (cfg.update > 0)
			return;
		cpuplugd_exit("update must be > 0\n");
	}
	if (check_value("cpu_min", name, rvalue, &cfg.cpu_min)) {
		if (cfg.cpu_min > 0)
			return;
		cpuplugd_exit("cpu_min must be > 0\n");
	}
	if (check_value("cpu_max", name, rvalue, &cfg.cpu_max)) {
		if (cfg.cpu_max == 0)
			/* if cpu_max is 0, we use the overall number of cpus */
			cfg.cpu_max = get_numcpus();
		return;
	}
	if (check_value("cmm_min", name, rvalue, &cfg.cmm_min))
		return;
	if (check_value("cmm_max", name, rvalue, &cfg.cmm_max))
		return;

	cpuplugd_debug("found the following variable: %s = %s\n",
		       name, rvalue);
	if (strlen(name) > MAX_VARNAME)
		cpuplugd_exit("Variable name too long (max. length is "
			      "%i chars): %s\n", MAX_VARNAME, name);
	add_var(name, rvalue);
}

/*
 * Function used to parse the min and max values at the beginning of the
 * configuration file as well as the hotplug and hotunplug rules.
 */
void parse_configfile(char *file)
{
	char linebuffer[MAX_LINESIZE + 2]; /* current line incl. \n and \0 */
	char *linep_offset;
	FILE *filp;

	filp = fopen(file, "r");
	if (!filp)
		cpuplugd_exit("Opening configuration file failed: %s\n",
			      strerror(errno));
	while (fgets(linebuffer, sizeof(linebuffer), filp) != NULL) {
		if (!(linep_offset = strchr(linebuffer, '\n')))
			cpuplugd_exit("Line is too long (max. length is %i "
				      "characters): %s\n", MAX_LINESIZE,
				      linebuffer);
		parse_configline(linebuffer);
	}
	fclose(filp);
}

void apply_cpu_config(void)
{
	/*
	 * Check that the initial number of cpus is not below the
	 * minimum
	 */
	if (num_cpu_start < cfg.cpu_min &&
	    get_numcpus() >= cfg.cpu_min) {
		cpuplugd_debug("The number of online cpus is below ");
		cpuplugd_debug("the minimum and will be increased.\n");
		while (get_num_online_cpus() < cfg.cpu_min) {
			if (hotplug_one_cpu())
				break;
		}
	}
	if (get_num_online_cpus() > cfg.cpu_max) {
		cpuplugd_debug("The number of online cpus is above the maximum");
		cpuplugd_debug(" and will be decreased.\n");
		while (get_num_online_cpus() > cfg.cpu_max) {
			if (hotunplug_one_cpu())
				break;
		}
	}
	if (cfg.cpu_min > get_numcpus()) {
		/*
		 * This check only works if nobody used the
		 * additional_cpus in the boot parameter section
		 */
		cpuplugd_debug("The minimum amount of cpus is above the ");
		cpuplugd_debug("number of available cpus.\n");
		cpuplugd_exit("Detected %d available cpus\n", get_numcpus());
	}
	if (get_num_online_cpus() < cfg.cpu_min) {
		cpuplugd_debug("Failed to set the number of online cpus to ");
		cpuplugd_debug("the minimum. ");
		cpuplugd_exit("Aborting.\n");
	}
}

/*
 * Check if the required settings are found in the configuration file.
 * "Autodetect" if cpu and/or memory hotplug configuration entries
 * where specified
 */
void check_config()
{
	int lpar_status;

	lpar_status = check_lpar();
	if (cfg.update < 0)
		cpuplugd_exit("No valid update interval specified.\n");
	if (cfg.cpu_max < cfg.cpu_min && cfg.cpu_max != 0)
		cpuplugd_exit("cpu_max below cpu_min, aborting.\n");
	if (cfg.cpu_max < 0 || cfg.cpu_min < 0 || cfg.hotplug == NULL ||
	    cfg.hotunplug == NULL) {
		cpuplugd_error("No valid CPU hotplug configuration "
			       "detected.\n");
		cpu = 0;
	} else {
		cpu = 1;
		cpuplugd_debug("Valid CPU hotplug configuration detected.\n");
	}
	if (cfg.cmm_max < 0 || cfg.cmm_min < 0 || cfg.memplug == NULL ||
	    cfg.memunplug == NULL || cfg.cmm_inc == NULL ||
	    cfg.cmm_max < cfg.cmm_min) {
		cpuplugd_error("No valid memory hotplug configuration "
			       "detected.\n");
		memory = 0;
	} else {
		memory = 1;
		/*
		 * check if all the necessary files exit below /proc
		 */
		if (check_cmmfiles() != 0 && lpar_status == 0) {
			cpuplugd_info("Can not open /proc/sys/vm/cmm_pages. "
				      "The memory hotplug function will be "
				      "disabled.\n");
			memory = 0;
		}
		if (memory == 1  && lpar_status == 0)
			cpuplugd_debug("Valid memory hotplug configuration "
				       "detected.\n");
		if (memory == 1  && lpar_status == 1) {
			cpuplugd_debug("Valid memory hotplug configuration "
				       "detected inside LPAR. "
				       "The memory hotplug function will be "
				       "disabled. \n");
			memory = 0;
		}
	}
	if (memory == 0 && cpu == 0)
		cpuplugd_exit("Exiting, because neither a valid cpu nor a val"
			      "id memory hotplug configuration was found.\n");
	/*
	* Save the number of online cpus and the cmm_pagesize at startup,
	* so that we can enable exactly the same amount when the daemon ends
	*
	* Don't adjust cpus if system is on vertical polarization
	*/
	saved_polarization = get_polarization();
	num_cpu_start = get_num_online_cpus();
	cpuplugd_debug("Daemon started with %d active cpus.\n",
		       num_cpu_start);
	if (saved_polarization < 0) {
		cpuplugd_debug("Daemon couldn't determine system polarization\n");
		cpuplugd_debug("Starting without evaluating cpu rules\n");
	} else if (saved_polarization == PLR_VERTICAL) {
		cpuplugd_debug("Daemon started with vertical polarization.\n");
		cpuplugd_debug("Cpu adjustments won't be made until system ");
		cpuplugd_debug("is in horizontal polarization\n");
	} else if (cpu == 1) {
		apply_cpu_config();
	}
	if (memory == 1) {
		/*
		 * Check that the initial value of cmm_pages is not below
		 * cmm_min or above cmm_max
		 */
		cmm_pagesize_start = get_cmmpages_size();
		if (cmm_pagesize_start < cfg.cmm_min) {
			cpuplugd_debug("cmm_pages is below minimum and will "
				       "be increased.\n");
			set_cmm_pages(cfg.cmm_min);
		}
		if (cmm_pagesize_start > cfg.cmm_max) {
			cpuplugd_debug("cmm_pages is above the maximum and will"
				       " be decreased.\n");
			set_cmm_pages(cfg.cmm_max);
		}
	}
}
