/*
 * Copyright (C) IBM Corporation. 2007
 * Author: Konrad Rzeszutek <konradr@linux.vnet.ibm.com>
 * Copyright (C) Red Hat, Inc.  All rights reserved. 2008 - 2010
 * Copyright (C) Mike Christie 2008 - 2010
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define  _XOPEN_SOURCE 500
#define _DEFAULT_SOURCE
#include <ftw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "sysfs.h"
#include "fw_context.h"
#include "fwparam.h"
#include "sysdeps.h"
#include "iscsi_net_util.h"
#include "iscsi_err.h"

#define ISCSI_BOOT_MAX		255
#define IBFT_SYSFS_ROOT		"/sys/firmware/ibft/"
#define IBFT_SUBSYS		"ibft"

#define ISCSI_LLD_ROOT		"/sys/firmware/"
#define ISCSI_LLD_SUBSYS_PREFIX	"iscsi_boot"

static char *target_list[ISCSI_BOOT_MAX];
static char *nic_list[ISCSI_BOOT_MAX];
static int nic_cnt;
static int tgt_cnt;

static int file_exist(const char *file)
{
	struct stat bootpath_stat;

	return !stat(file, &bootpath_stat);
}

/*
 * Finds the etherrnetX and targetX under the sysfs directory.
 */
static int find_sysfs_dirs(const char *fpath, const struct stat *sb,
			   int tflag, struct FTW *ftw)
{
	if (tflag == FTW_D && (strstr(fpath + ftw->base, "target"))) {
		if (tgt_cnt == ISCSI_BOOT_MAX) {
			printf("Too many targets found in iSCSI boot data."
			       "Max number of targets %d\n", ISCSI_BOOT_MAX);
			return 0;
		}
		target_list[tgt_cnt++] = strdup(strstr(fpath, "target"));
	}

	if (tflag == FTW_D && (strstr(fpath + ftw->base, "ethernet"))) {
		if (nic_cnt == ISCSI_BOOT_MAX) {
			printf("Too many nics found in iSCSI boot data."
			       "Max number of nics %d\n", ISCSI_BOOT_MAX);
			return 0;
		}
		nic_list[nic_cnt++] = strdup(strstr(fpath, "ethernet"));
	}

	return 0;
}
 
static int get_iface_from_device(char *id, struct boot_context *context)
{
	char dev_dir[FILENAMESZ];
	int rc = ENODEV;
	DIR *dirfd;
	struct dirent *dent;

	memset(dev_dir, 0, FILENAMESZ);
	snprintf(dev_dir, FILENAMESZ, IBFT_SYSFS_ROOT"/%s/device", id);

	if (!file_exist(dev_dir))
		return 0;

	dirfd = opendir(dev_dir);
	if (!dirfd)
		return errno;

	while ((dent = readdir(dirfd))) {
		if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, "..") ||
		    strncmp(dent->d_name, "net:", 4))
			continue;

		if (!strncmp(dent->d_name, "net:", 4)) {
			if ((strlen(dent->d_name) - 4) >
			    (sizeof(context->iface) - 1)) {
				rc = EINVAL;
				printf("Net device %s too big for iface "
				       "buffer.\n", dent->d_name);
				break;
			}

			if (sscanf(dent->d_name, "net:%s", context->iface) != 1)
				rc = EINVAL;
			rc = 0;
			break;
		} else {
			printf("Could not read ethernet to net link.\n");
			rc = EOPNOTSUPP;
			break;
		}
	}

	closedir(dirfd);

	if (rc != ENODEV)
		return rc;

	/* If not found try again with newer kernel networkdev sysfs layout */
	strlcat(dev_dir, "/net", FILENAMESZ);

	if (!file_exist(dev_dir))
		return rc;

	dirfd = opendir(dev_dir);
	if (!dirfd)
		return errno;

	while ((dent = readdir(dirfd))) {
		if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
			continue;

		/* Take the first "regular" directory entry */
		if (strlen(dent->d_name) > (sizeof(context->iface) - 1)) {
			rc = EINVAL;
			printf("Net device %s too big for iface buffer.\n",
			       dent->d_name);
			break;
		}

		strcpy(context->iface, dent->d_name);
		rc = 0;
		break;
	}

	closedir(dirfd);
	return rc;
}

/*
 * Routines to fill in the context values.
 */
static int fill_nic_context(char *subsys, char *id,
			    struct boot_context *context)
{
	int rc;

	rc = sysfs_get_int(id, subsys, "flags", &context->nic_flags);
	/*
	 * Per spec we would need to check against Bit 0
	 * (Block Valid Flag), but some firmware only
	 * sets Bit 1 (Firmware Booting Selected).
	 * So any setting is deemed okay.
	 */
	if (!rc && (context->nic_flags == 0))
		rc = ENODEV;
	if (rc)
		return rc;

	rc = sysfs_get_str(id, subsys, "mac", context->mac,
			   sizeof(context->mac));
	if (rc)
		return rc;

	/*
	 * Some offload cards like bnx2i use different MACs for the net and
	 * iscsi functions, so we have to follow the sysfs links.
	 *
	 * Other ibft implementations may not be tied to a pci function,
	 * so there will not be any device/net link, so we drop down to
	 * the MAC matching.
	 *
	 * And finally, some cards like be2iscsi and qla4xxx do not have
	 * any linux network subsys representation. These hosts will
	 * not have the ibft subsys. Instead the subsys is the scsi host
	 * number.
	 */
	if (!strcmp(IBFT_SUBSYS, subsys)) {
		rc = get_iface_from_device(id, context);
		if (rc) {
			rc = net_get_netdev_from_hwaddress(context->mac,
							   context->iface);
			if (rc)
				return rc;
		}
	} else
		strlcpy(context->scsi_host_name, subsys,
			sizeof(context->scsi_host_name));

	memset(&context->boot_nic, 0, sizeof(context->boot_nic));
	snprintf(context->boot_nic, sizeof(context->boot_nic), "%s", id);

	sysfs_get_str(id, subsys, "ip-addr", context->ipaddr,
		      sizeof(context->ipaddr));
	sysfs_get_str(id, subsys, "vlan", context->vlan,
		      sizeof(context->vlan));
	sysfs_get_str(id, subsys, "subnet-mask", context->mask,
		      sizeof(context->mask));
	sysfs_get_int(id, subsys, "prefix-len", &context->prefix);
	sysfs_get_str(id, subsys, "gateway", context->gateway,
		      sizeof(context->gateway));
	sysfs_get_str(id, subsys, "primary-dns", context->primary_dns,
		      sizeof(context->primary_dns));
	sysfs_get_str(id, subsys, "secondary-dns", context->secondary_dns,
		      sizeof(context->secondary_dns));
	sysfs_get_str(id, subsys, "dhcp", context->dhcp,
		      sizeof(context->dhcp));
	sysfs_get_int(id, subsys, "origin", (int *)&context->origin);
	return 0;
}

static void fill_initiator_context(char *subsys, struct boot_context *context)
{
	sysfs_get_str("initiator", subsys, "initiator-name",
		      context->initiatorname,
		      sizeof(context->initiatorname));
	sysfs_get_str("initiator", subsys, "isid", context->isid,
		      sizeof(context->isid));

	strlcpy(context->boot_root, subsys, sizeof(context->boot_root));
}
static int fill_tgt_context(char *subsys, char *id,
			    struct boot_context *context)
{
	int rc;

	rc = sysfs_get_int(id, subsys, "flags", &context->target_flags);
	/*
	 * Per spec we would need to check against Bit 0
	 * (Block Valid Flag), but some firmware only
	 * sets Bit 1 (Firmware Booting Selected).
	 * So any setting is deemed okay.
	 */
	if (!rc && (context->target_flags == 0))
		rc = ENODEV;
	if (rc)
		return rc;

	rc = sysfs_get_str(id, subsys, "target-name", context->targetname,
			   sizeof(context->targetname));
	if (rc)
		return rc;

	rc = sysfs_get_str(id, subsys, "ip-addr", context->target_ipaddr,
			   sizeof(context->target_ipaddr));
	if (rc)
		return rc;

	memset(&context->boot_target, 0, sizeof(context->boot_target));
	snprintf(context->boot_target, sizeof(context->boot_target), "%s", id);

	/*
	 * We can live without the rest of they do not exist. If we
	 * failed to get them we will figure it out when we login.
	 */
	if (sysfs_get_int(id, subsys, "port", &context->target_port))
		context->target_port = ISCSI_LISTEN_PORT;

	sysfs_get_str(id, subsys, "lun", context->lun,
		      sizeof(context->lun));
	sysfs_get_str(id, subsys, "chap-name", context->chap_name,
		      sizeof(context->chap_name));
	sysfs_get_str(id, subsys, "chap-secret", context->chap_password,
		      sizeof(context->chap_password));
	sysfs_get_str(id, subsys, "rev-chap-name", context->chap_name_in,
		      sizeof(context->chap_name_in));
	sysfs_get_str(id, subsys, "rev-chap-name-secret",
		      context->chap_password_in,
		      sizeof(context->chap_password_in));
	return 0;
}

#define IBFT_SYSFS_FLAG_FW_SEL_BOOT 2

static int find_boot_flag(char *subsys, char *list[], ssize_t size,
			  int *boot_idx)
{
	int rc = ENODEV;
	int i, flag = 0;

	for (i = 0; i < size; i++, flag = -1) {
		rc = sysfs_get_int(list[i], subsys, "flags", &flag);
		if (rc)
			continue;

		if (flag & IBFT_SYSFS_FLAG_FW_SEL_BOOT) {
			*boot_idx = i;
			rc = 0;
			break;
		}
		rc = ENODEV;
		flag = 0;

	}

	return rc;
}

static void deallocate_lists(void)
{
	int i;

	for (i = 0; i < nic_cnt; i++)
		free(nic_list[i]);

	nic_cnt = 0;
	for (i = 0; i < tgt_cnt; i++)
		free(target_list[i]);

	tgt_cnt = 0;

}

static int get_boot_info(struct boot_context *context, char *rootdir,
			 char *subsys)
{
	char initiator_dir[FILENAMESZ];
	int rc = ENODEV;
	int nic_idx = -1, tgt_idx = -1;

	memset(&initiator_dir, 0 , FILENAMESZ);
	snprintf(initiator_dir, FILENAMESZ, "%sinitiator", rootdir);

	nic_cnt = 0;
	tgt_cnt = 0;
	if (file_exist(initiator_dir)) {
		/* Find the targets and the ethernets */
		rc = nftw(rootdir, find_sysfs_dirs, 20, 1);

		/* Find wihch target and which ethernet have
		the boot flag set. */
		rc = find_boot_flag(subsys, nic_list, nic_cnt, &nic_idx);
		if (rc)
			goto free;

		rc = find_boot_flag(subsys, target_list, tgt_cnt, &tgt_idx);
		if (rc)
			goto free;

		/* Fill in the context values */
		rc = fill_nic_context(subsys, nic_list[nic_idx], context);
		rc |= fill_tgt_context(subsys, target_list[tgt_idx], context);
		fill_initiator_context(subsys, context);
	}
free:
	deallocate_lists();
	return rc;
}

int fwparam_sysfs_boot_info(struct boot_context *context)
{
	struct dirent *dent;
	DIR *dirfd;
	int rc = 0;

	if (!get_boot_info(context, IBFT_SYSFS_ROOT, IBFT_SUBSYS))
		return 0;
	/*
	 * We could have multiple iscsi llds and each lld could have
	 * multiple targets/ethernet ports
	 */
	dirfd = opendir(ISCSI_LLD_ROOT);
	if (!dirfd)
		return ISCSI_ERR_SYSFS_LOOKUP;

	while ((dent = readdir(dirfd))) {
		char lld_root[FILENAMESZ];

		memset(&lld_root, 0 , FILENAMESZ);

		if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
			continue;

		if (strncmp(dent->d_name, ISCSI_LLD_SUBSYS_PREFIX, 10))
			continue;

		snprintf(lld_root, FILENAMESZ, ISCSI_LLD_ROOT"%s/",
			 dent->d_name);
		if (!get_boot_info(context, lld_root, dent->d_name))
			goto done;
	}
	rc = ISCSI_ERR_NO_OBJS_FOUND;
done:
	closedir(dirfd);
	return rc;
}

static int get_targets(struct list_head *list, char *rootdir, char *subsys)
{
	struct boot_context *context;
	int rc = 0, i, nic_idx, nic;
	char initiator_dir[FILENAMESZ];

	memset(&initiator_dir, 0 , FILENAMESZ);
	snprintf(initiator_dir, FILENAMESZ, "%sinitiator", rootdir);

	if (!file_exist(initiator_dir))
		return ENODEV;

	nic_cnt = 0;
	tgt_cnt = 0;

	/* Find the targets and the ethernets */
	nftw(rootdir, find_sysfs_dirs, 20, 1);
	for (i = 0; i < tgt_cnt; i++) {
		context = calloc(1, sizeof(*context));
		if (!context) {
			rc = ENOMEM;
			break;
		}

		rc = fill_tgt_context(subsys, target_list[i], context);
		if (rc)
			goto cleanup;

		rc = sysfs_get_int(target_list[i], subsys, "nic-assoc",
				   &nic_idx);
		if (rc)
			goto cleanup;

		for (nic = 0; nic < nic_cnt; nic++) {
			int id;

			rc = sysfs_get_int(nic_list[nic], subsys, "index",
					   &id);
			if (!rc && (id == nic_idx))
				break;
		}

		if (nic == nic_cnt) {
			printf("Invalid nic-assoc of %d. Max id %d.\n",
			       nic_idx, nic_cnt);
			goto cleanup;
		}

		rc = fill_nic_context(subsys, nic_list[nic], context);
		if (rc)
			goto cleanup;

		fill_initiator_context(subsys, context);
		list_add_tail(&context->list, list);
		continue;
cleanup:
		free(context);
		context = NULL;
	}

	if (rc) {
		if (context)
			free(context);
		/*
		 * If there are some valid targets return them. Most likely,
		 * the driver/ibft-implementation reported partial info
		 * for targets/initiators that were not used for boot.
		 */
		if (!list_empty(list))
			rc = 0;
	}

	deallocate_lists();
	return rc;
}

int fwparam_sysfs_get_targets(struct list_head *list)
{
	struct dirent *dent;
	DIR *dirfd;
	int rc = 0;

	/* ibft only has one instance */
	get_targets(list, IBFT_SYSFS_ROOT, IBFT_SUBSYS);
	/*
	 * We could have multiple iscsi llds and each lld could have
	 * multiple targets/ethernet ports
	 */
	dirfd = opendir(ISCSI_LLD_ROOT);
	if (!dirfd) {
		rc = ISCSI_ERR_SYSFS_LOOKUP;
		goto done;
	}

	while ((dent = readdir(dirfd))) {
		char lld_root[FILENAMESZ];

		memset(&lld_root, 0 , FILENAMESZ);
		if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
			continue;

		if (strncmp(dent->d_name, ISCSI_LLD_SUBSYS_PREFIX, 10))
			continue;

		snprintf(lld_root, FILENAMESZ, ISCSI_LLD_ROOT"%s/",
			 dent->d_name);
		get_targets(list, lld_root, dent->d_name);
	}
	closedir(dirfd);
done:
	if (!rc && list_empty(list))
		rc = ISCSI_ERR_NO_OBJS_FOUND;
	if (rc)
		fw_free_targets(list);
	return rc;
}
