/*
 * zdev - Modify and display the persistent configuration of devices
 *
 * Copyright IBM Corp. 2016, 2017
 *
 * 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 <stdlib.h>
#include <string.h>

#include "lib/util_path.h"

#include "device.h"
#include "devtype.h"
#include "internal.h"
#include "misc.h"
#include "path.h"
#include "root.h"
#include "select.h"
#include "setting.h"
#include "subtype.h"

static bool is_early_removed(struct device *dev)
{
	struct setting *s;

	s = setting_list_find(dev->persistent.settings,
			      internal_attr_early.name);
	if (!s || !s->modified)
		return false;
	if (!s->actual_value || strcmp(s->actual_value, "1") != 0)
		return false;
	if (s->removed || strcmp(s->value, "0") == 0)
		return true;
	return false;
}

static void add_early_removed(struct util_list *selected)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	struct device *dev;

	for (i = 0; devtypes[i]; i++) {
		dt = devtypes[i];
		for (j = 0; dt->subtypes[j]; j++) {
			st = dt->subtypes[j];
			util_list_iterate(&st->devices->hash.list, dev) {
				if (is_early_removed(dev)) {
					selected_dev_list_add(selected, dt, st,
						dev->id, NULL, EXIT_OK);
				}
			}
		}
	}
}

static void add_pers_removed(struct util_list *strlist)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	struct device *dev;

	for (i = 0; devtypes[i]; i++) {
		dt = devtypes[i];
		for (j = 0; dt->subtypes[j]; j++) {
			st = dt->subtypes[j];
			util_list_iterate(&st->devices->hash.list, dev) {
				if (dev->persistent.deconfigured) {
					strlist_add(strlist, "%s %s",
						    dev->subtype->devname, dev->id);
				}
			}
		}
	}
}

static bool is_zdev_early_0(struct selected_dev_node *sel)
{
	struct setting *s;
	struct device *dev;
	int i;

	dev = device_list_find(sel->st->devices, sel->id, NULL);
	if (!dev)
		return false;

	for (i = 0; i < NUM_SITES; i++) {
		s = setting_list_find(dev->site_specific[i].settings,
				      internal_attr_early.name);
		if (!s)
			return false;
		if (s->specified && strcmp(s->value, "0") == 0)
			return true;
	}

	return false;
}

/* When site-specific settings has zdev:early configured, it is not
 * detected with the --by-attr functionality of select_devices. Instead
 * we will have to iterate through each configuration and check if there
 * is a zdev:early set in the device. This is usually applicable only in
 * the case of deconfigure-all command, because, for all other commands
 * we copy the settings-in-action to the persistent settings and do the
 * processing there.
 */
static bool is_zdev_early_site(struct selected_dev_node *sel)
{
	struct setting *s;
	struct device *dev;
	int i;

	if (global_site_id != SITE_FALLBACK)
		return false;

	dev = device_list_find(sel->st->devices, sel->id, NULL);
	if (!dev)
		return false;

	for (i = 0; i < NUM_SITES; i++) {
		s = setting_list_find(dev->site_specific[i].settings,
				      internal_attr_early.name);
		if (s)
			return true;
	}

	return false;
}

/* Determine if initial RAM-disk needs updating. If so, run the corresponding
 * scripts if available. */
exit_code_t initrd_check(bool all_pers, int site_validate)
{
	struct util_list *selected, *params, *mod = strlist_new();
	struct selected_dev_node *sel;
	struct device *dev;
	char *params_str;
	exit_code_t rc = EXIT_OK;
	struct strlist_node *s;
	struct devtype *dt;
	struct select_opts *select;

	debug("Checking for required initial RAM-disk update\n");

	/* Get list of devices that provide the root device or require
	 * early configuration. */
	selected = selected_dev_list_new();

	if (all_pers) {
		/* Add all persistently configured devices. */
		select = select_opts_new();
		select->configured = 1;
		select_devices(select, selected, 1, 0, 0, config_persistent,
			       scope_mandatory, err_ignore);
		select_opts_free(select);

		/* Ensure that removed devices are considered. */
		add_pers_removed(mod);
		goto check_mod;
	}

	/* First add devices that had zdev:early removed or changed to 0.
	 * The subsequent call to select_devices() will filter out any
	 * duplicates. */
	add_early_removed(selected);
	/* Now add devices required for root file system. */
	if (select_by_path(NULL, selected, config_active, scope_mandatory,
			   NULL, NULL, PATH_ROOT, err_ignore)) {
		/* Running from an unknown root device is not an error. */
		verb("Note: Could not determine if root device configuration "
		     "needs to be updated\n");
	}
	/* Finally add devices with zdev:early=1. */
	select = select_opts_new();
	/* If we need information from multiple site, we cannot use the by-attr
	 * functionality of select_devices function. Instead we have to iterate
	 * through individual devices and site settings to find the key.
	 */
	if (!site_validate)
		strlist_add(&select->by_attr, "%s=1", INTERNAL_ATTR_EARLY);
	select_devices(select, selected, 1, 0, 0,
		       config_active | config_persistent, scope_mandatory,
		       err_ignore);
	select_opts_free(select);

check_mod:
	/* Determine if any of the devices or device types has been modified. */
	util_list_iterate(selected, sel) {
		dt = sel->st->devtype;

		/* Check devtype. */
		if (devtype_needs_writing(dt, config_persistent)) {
			strlist_add(mod, "Device type %s",
				    sel->st->devtype->name);
		}

		/* Check devices. */
		dev = device_list_find(sel->st->devices, sel->id, NULL);
		if (dev && dev->persistent.exists &&
		    (device_needs_writing(dev, config_persistent) || force) &&
		    !site_validate) {
			strlist_add(mod, "%s %s", dev->subtype->devname,
				    dev->id);
		} else if (site_validate && is_zdev_early_site(sel) &&
			   device_needs_writing(dev, config_persistent)) {
			strlist_add(mod, "%s %s", dev->subtype->devname,
				    dev->id);
		}
	}

	if (util_list_is_empty(mod))
		goto out;
	info("Note: The initial RAM-disk must be updated for these changes to take effect:\n");
	util_list_iterate(mod, s)
		info("       - %s\n", s->str);

	/* Check if script is available. */
	if (!util_path_is_reg_file(PATH_ROOT_SCRIPT)) {
		warn("A manual update of the initial RAM-disk is required.\n");
		goto out;
	}

	if (!all_pers) {
		/* Ask for confirmation. */
		if (!confirm("Update initial RAM-disk now?")) {
			rc = EXIT_ABORTED;
			goto out;
		}
	}

	/* Build the command line. */
	params = strlist_new();
	util_list_iterate(selected, sel) {
		/* From the selected list, remove the devices with zdev:early=0 */
		if (!is_zdev_early_0(sel)) {
			strlist_add(params, "%s", sel->st->name);
			strlist_add(params, "%s", sel->id);
		}
	}
	params_str = strlist_flatten(params, " ");
	strlist_free(params);

	/* Run update command. */
	if (misc_system(err_delayed_print, "%s %s", PATH_ROOT_SCRIPT,
			params_str) != 0) {
		error("Failure while updating initial RAM-disk\n");
		delayed_print(DELAY_INDENT);
		rc = EXIT_RUNTIME_ERROR;
	}
	free(params_str);

out:
	strlist_free(mod);
	selected_dev_list_free(selected);

	return rc;
}
