/*
 * zdev_id - Determine system characteristics required by zdev udev rules
 *
 * Copyright IBM Corp. 2021
 *
 * 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 <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "zdev.h"
#include "zdev_id.h"

#define	SYSINFO		"/proc/sysinfo"
#define	CMDLINE		"/proc/cmdline"
#define LOADPARM	"/sys/firmware/ipl/loadparm"
#define IPL_DEV_ID	"/sys/firmware/ipl/device"
#define IPL_DEV_TYPE	"/sys/firmware/ipl/ipl_type"

#define	WHITESPACE	" \t\n"

static void array_add(char ***array_p, int *num_p, const char *str)
{
	char **array = *array_p, *copy;
	int num = *num_p + 1;

	array = realloc(array, num * sizeof(char *));
	copy = strdup(str);
	if (!array || !copy)
		err(1, "Could not allocate memory");
	array[num - 1] = copy;
	*array_p = array;
	*num_p = num;
}

static void array_free(char **array, int num)
{
	int i;

	for (i = 0; i < num; i++)
		free(array[i]);
	free(array);
}

static void process_sysinfo(const char *filename)
{
	char *line = NULL, *substr, **cps = NULL;
	int is_dpm = 0, num_cps = 0, level, i;
	FILE *fd;
	size_t n;

	fd = fopen(filename, "r");
	if (!fd)
		err(1, "Could not open sysinfo file '%s'", filename);

	while (getline(&line, &n, fd) != -1) {
		if (sscanf(line, "LPAR %m[^:]", &substr) == 1) {
			if (strcmp(substr, "UUID") == 0) {
				/* Heuristic: only DPM LPARs have UUIDs. */
				is_dpm = 1;
			} else if (strcmp(substr, "Number") == 0) {
				array_add(&cps, &num_cps, "LPAR");
			}
			free(substr);
		} else if (sscanf(line, "VM%*d Control Program: %ms ",
				  &substr) == 1) {
			array_add(&cps, &num_cps, substr);
			free(substr);
		}
	}

	free(line);
	fclose(fd);

	/*
	 * ZDEV_NEST_LEVEL=n
	 *
	 * Virtualization nesting level of running system:
	 *  1: LPAR
	 *  2: first level VM
	 *  3: second level VM
	 */
	printf("ZDEV_NEST_LEVEL=%d\n", num_cps);

	/*
	 * ZDEV_HYPERVISOR_<n>=LPAR|z/VM|KVM/Linux
	 *
	 * Type of hypervisor that provides virtualization at nesting level <n>.
	 */
	for (i = 0; i < num_cps; i++) {
		/* Sysinfo lists VMs in reverse order .*/
		level = (i == 0) ? i : (num_cps - i);
		printf("ZDEV_HYPERVISOR_%d=\"%s\"\n", i, cps[level]);
	}
	array_free(cps, num_cps);

	/*
	 * ZDEV_IS_DPM=0|1
	 *
	 * Indicator if top-level LPAR is managed by Dynamic Partition
	 * Manager (DPM):
	 *  0: Classic mode
	 *  1: DPM mode
	 */
	printf("ZDEV_IS_DPM=%d\n", is_dpm);
}

static void process_cmdline(const char *filename)
{
	char *line = NULL, *substr, *next = NULL, *value;
	int no_auto = 0;
	size_t n;
	FILE *fd;

	fd = fopen(filename, "r");
	if (!fd)
		err(1, "Could not open cmdline file '%s'", filename);

	if (getline(&line, &n, fd) == -1)
		goto out;

	for (substr = strtok_r(line, WHITESPACE, &next); substr;
	     substr = strtok_r(NULL, WHITESPACE, &next)) {
		if (sscanf(substr, "rd.zdev=%ms ", &value) != 1)
			continue;
		if (strstr(value, "no-auto"))
			no_auto = 1;

		free(value);
	}

out:
	free(line);
	fclose(fd);

	/*
	 * ZDEV_NO_AUTO=0|1
	 *
	 * Indicator if auto-configuration is requested.
	 *  0: Perform auto-configuration
	 *  1: Skip auto-configuration
	 */
	printf("ZDEV_NO_AUTO=%d\n", no_auto);
}

/*
 * If the erraneous loadparm provides a site_id which is not valid,
 * default it to the common-site id.
 */
static int validate_site(int site_id)
{
	if (site_id < SITE_FALLBACK)
		return site_id;
	return SITE_FALLBACK;
}

/* get the ipl device and extract the SSID */
static int get_site_id(void)
{
	FILE *fd;
	char *line;
	size_t n;
	int site_id = SITE_FALLBACK, ssid;

	fd = fopen(IPL_DEV_ID, "r");
	if (!fd)
		goto fail_safe;

	if (getline(&line, &n, fd) == -1)
		goto out;

	if (sscanf(line, "%*d.%d.%*d", &ssid) == 1)
		site_id = validate_site(ssid);

out:
	free(line);
	fclose(fd);
fail_safe:
	return site_id;
}

/*
 * Find the ipl type and see if it belongs to ccw  or zfcp, if different,
 * default it to common-site.
 */
static int read_ssid(void)
{
	FILE *fd;
	char *line = NULL;
	size_t n;
	int site_id = SITE_FALLBACK;

	fd = fopen(IPL_DEV_TYPE, "r");
	if (!fd)
		goto out;

	if (getline(&line, &n, fd) != -1) {
		if (strcmp(line, "ccw") == 0 || strcmp(line, "zfcp"))
			site_id =  get_site_id();
	}

	free(line);
	fclose(fd);
out:
	return site_id;
}

static void write_zdev_site_id(int site_id)
{
	FILE *fd;
	int tmpfd, rc;
	const char zdev_id_file[] = ZDEV_SITE_ID_FILE;
	char zdev_id_tmpfile[] = ZDEV_SITE_ID_FILE "-XXXXXX";

	tmpfd = mkstemp(zdev_id_tmpfile);
	if (tmpfd == -1)
		goto err;

	/* Open the temp file to use with fprintf */
	fd = fdopen(tmpfd, "w");
	if (!fd)
		goto err;

	if (fprintf(fd, CHZDEV_HEADER "\n") < 0) {
		fclose(fd);
		goto err;
	}

	if (site_id == SITE_FALLBACK)
		rc = fprintf(fd, "ZDEV_SITE_ID=\"\"\n");
	else
		rc = fprintf(fd, "ZDEV_SITE_ID=%d\n", site_id);

	if (rc < 0) {
		fclose(fd);
		goto err;
	}

	if (fclose(fd))
		goto err;

	/* Rename the temporary file to ZDEV_SITE_ID_FILE*/
	if (rename(zdev_id_tmpfile, zdev_id_file) == -1) {
		remove(zdev_id_tmpfile);
		goto err;
	}

	return;
err:
	err(1, "Could not write to zdev_site_id file");
}

/* Read the loadparm and extract the current site_id.
 * loadparm can contains either Sn or "Ss". Sn indicate the site_id, where
 * 'n' is the integer which could be one of the valid site_ids from 0 to 9.
 * When loadparm value is "Ss", zdev_id extracts the site_id from the SSID
 * of the current ipl device. For ccw and zfcp devices, the current ipl
 * device-id can be found at /sys/firmware/ipl/device.
 * For all other invalid cases, set ZDEV_SITE_ID to NULL.
 */

static void process_loadparm(const char *filename)
{
	size_t n;
	FILE *fd;
	char *line = NULL, *substr;
	int site_id = SITE_FALLBACK;

	fd = fopen(filename, "r");
	if (!fd)
		goto out;

	/*
	 * We expect the value here to be either SX, where X is the site-id
	 * or SS,in which case, the site-id will be derived from the SSID
	 */
	if ((getline(&line, &n, fd)) != -1) {
		substr = strchr(line, 'S');
		if (!substr)
			goto out;

		if (isdigit(substr[1]))
			site_id = validate_site(atoi(&substr[1]));
		else if (substr[1] == 'S')
			site_id = read_ssid();
	}

	free(line);
	fclose(fd);
out:
	write_zdev_site_id(site_id);
	if (site_id == SITE_FALLBACK)
		printf("ZDEV_SITE_ID=\"\"\n");
	else
		printf("ZDEV_SITE_ID=%d\n", site_id);
}

int main(int argc, char *argv[])
{
	char *sysinfo, *cmdline, *loadparm;

	sysinfo = argc < 2 ? SYSINFO : argv[1];
	cmdline = argc < 3 ? CMDLINE : argv[2];
	loadparm = argc < 4 ? LOADPARM : argv[3];

	process_sysinfo(sysinfo);
	process_cmdline(cmdline);
	process_loadparm(loadparm);

	return 0;
}
