/*
 * iSCSI session management helpers
 *
 * Copyright (C) 2010 Mike Christie
 * Copyright (C) 2010 Red Hat, Inc. All rights reserved.
 * Copyright (C) 2011 Dell Inc.
 * maintained by open-iscsi@googlegroups.com
 *
 * 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.
 *
 * See the file COPYING included with this distribution for more details.
 */

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

#include "session_info.h"
#include "idbm.h"
#include "list.h"
#include "iscsi_util.h"
#include "mgmt_ipc.h"
#include "iscsi_sysfs.h"
#include "log.h"
#include "iscsid_req.h"
#include "iscsi_err.h"

static void log_login_msg(struct node_rec *rec, int rc)
{
	if (rc) {
		log_error("Could not login to [iface: %s, target: %s, "
			  "portal: %s,%d].", rec->iface.name,
			  rec->name, rec->conn[0].address,
			  rec->conn[0].port);
		iscsi_err_print_msg(rc);
	} else
		log_info("Login to [iface: %s, target: %s, portal: "
			 "%s,%d] successful.", rec->iface.name,
			 rec->name, rec->conn[0].address,
			 rec->conn[0].port);
}

struct iscsid_async_req {
	struct list_head list;
	void *data;
	int fd;
};

/**
 * iscsid_reqs_close - close open async requests
 * @list: list of async reqs
 *
 * This just closes the socket to the daemon.
 */
static void iscsid_reqs_close(struct list_head *list)
{
	struct iscsid_async_req *tmp, *curr;

	list_for_each_entry_safe(curr, tmp, list, list) {
		close(curr->fd);
		list_del(&curr->list);
		free(curr);
	}
}

static int iscsid_login_reqs_wait(struct list_head *list)
{
	struct iscsid_async_req *tmp, *curr;
	struct node_rec *rec;
	int ret = 0;

	list_for_each_entry_safe(curr, tmp, list, list) {
		int err;

		rec = curr->data;
		err = iscsid_req_wait(MGMT_IPC_SESSION_LOGIN, curr->fd);
		if (err && !ret)
			ret = err;
		log_login_msg(rec, err);
		list_del(&curr->list);
		free(curr);
	}
	return ret;
}

/**
 * __iscsi_login_portal - request iscsid to login to portal
 * @data: If set, copies the session.multiple value to the portal record
 *        so it is propagated to iscsid.
 * @list: If async, list to add session to
 * @rec: portal rec to log into
 */
static int
__iscsi_login_portal(void *data, struct list_head *list, struct node_rec *rec)
{
	struct iscsid_async_req *async_req = NULL;
	int rc = 0, fd;

	if (data && !rec->session.multiple) {
		struct node_rec *pattern_rec = data;
		rec->session.multiple = pattern_rec->session.multiple;
	}

	conn_log_connect(1, NULL, "Logging in to [iface: %s, target: %s, portal: %s,%d]%s",
		 rec->iface.name, rec->name, rec->conn[0].address,
		 rec->conn[0].port,
		 (rec->session.multiple ? " (multiple)" : ""));

	if (list) {
		async_req = calloc(1, sizeof(*async_req));
		if (!async_req)
			conn_log_connect(1, NULL, "Could not allocate memory for async login "
				 "handling. Using sequential login instead.");
		else
			INIT_LIST_HEAD(&async_req->list);
	}

	if (async_req)
		rc = iscsid_req_by_rec_async(MGMT_IPC_SESSION_LOGIN,
					     rec, &fd);
	else
		rc = iscsid_req_by_rec(MGMT_IPC_SESSION_LOGIN, rec);

	if (rc) {
		log_login_msg(rec, rc);
		if (async_req)
			free(async_req);
		return rc;
	}

	if (async_req) {
		list_add_tail(&async_req->list, list);
		async_req->fd = fd;
		async_req->data = rec;
	} else
		log_login_msg(rec, rc);

	return 0;
}

/**
 * iscsi_login_portal - request iscsid to login to portal multiple
 * times, based on the session.nr_sessions in the portal record.
 * @data: If set, session.multiple will cause an additional session to
 *        be created regardless of the value of session.nr_sessions
 * @list: If async, list to add session to
 * @rec: portal rec to log into
 */
int iscsi_login_portal(void *data, struct list_head *list, struct node_rec *rec)
{
	struct node_rec *pattern_rec = data;
	int rc = 0, session_count = 0, i;

	/*
	 * If pattern_rec->session.multiple is set, just add a single new
	 * session by passing things along to __iscsi_login_portal
	 */
	if (pattern_rec && pattern_rec->session.multiple)
		return __iscsi_login_portal(data, list, rec);

	/*
	 * Count the current number of sessions, and only create those
	 * that are missing.
	 */
	rc = iscsi_sysfs_for_each_session(rec, &session_count,
					  iscsi_match_session_count, 0);
	if (rc) {
		log_error("Could not count current number of sessions");
		goto done;
	}
	if (session_count >= rec->session.nr_sessions) {
		log_warning("%s: %d session%s requested, but %d "
			  "already present.",
			  rec->iface.name, rec->session.nr_sessions,
			  rec->session.nr_sessions == 1 ? "" : "s",
			  session_count);
		rc = ISCSI_ERR_SESS_EXISTS;
		goto done;
	}

	/*
	 * Ensure the record's 'multiple' flag is set so __iscsi_login_portal
	 * will allow multiple logins, but only if configured for more
	 * than one 
	 */
	if (rec->session.nr_sessions > 1)
		rec->session.multiple = 1;
	for (i = session_count; i < rec->session.nr_sessions; ++i) {
		conn_log_connect(1, NULL, "%s: Creating session %d/%d", rec->iface.name,
			  i + 1, rec->session.nr_sessions);
		int err = __iscsi_login_portal(pattern_rec, list, rec);
		if (err && !rc)
			rc = err;
	}

done:
	return rc;
}

/**
 * iscsi_login_portal_nowait - request iscsid to login to portal
 * @rec: portal rec to log into
 *
 * This sends the login request, but does not wait for the result.
 */
int iscsi_login_portal_nowait(struct node_rec *rec)
{
	struct list_head list;
	int err;

	INIT_LIST_HEAD(&list);
	err = iscsi_login_portal(NULL, &list, rec);
	if (err > 0)
		return err;
	iscsid_reqs_close(&list);
	return 0;
}

/**
 * __iscsi_login_portals - login into portals on @rec_list,
 * @data: data to pass to login_fn
 * @nr_found: returned with number of portals logged into
 * @wait: bool indicating if the fn should wait for the result
 * @rec_list: list of portals to log into
 * @clear_list: If set, delete and free rec_list after iterating through.
 * @login_fn: list iter function
 *
 * This will loop over the list of portals and login. It
 * will attempt to login asynchronously, and then wait for
 * them to complete if wait is set.
 */
static
int __iscsi_login_portals(void *data, int *nr_found, int wait,
			struct list_head *rec_list, int clear_list,
			int (*login_fn)(void *, struct list_head *,
					 struct node_rec *))
{
	struct node_rec *curr_rec, *tmp;
	struct list_head login_list;
	int ret = 0, err;

	*nr_found = 0;
	INIT_LIST_HEAD(&login_list);

	list_for_each_entry(curr_rec, rec_list, list) {
		err = login_fn(data, &login_list, curr_rec);
		if (err > 0 && !ret)
			ret = err;
		if (!err)
			(*nr_found)++;
	}
	if (wait) {
		err = iscsid_login_reqs_wait(&login_list);
		if (err && !ret)
			ret = err;
	} else
		iscsid_reqs_close(&login_list);

	if (clear_list) {
		list_for_each_entry_safe(curr_rec, tmp, rec_list, list) {
			list_del(&curr_rec->list);
			free(curr_rec);
		}
	}
	return ret;
}

/**
 * iscsi_login_portals - login into portals on @rec_list,
 * @data: data to pass to login_fn
 * @nr_found: returned with number of portals logged into
 * @wait: bool indicating if the fn should wait for the result
 * @rec_list: list of portals to log into.  This list is deleted after
 *            iterating through it.
 * @login_fn: list iter function
 *
 * This will loop over the list of portals and login. It
 * will attempt to login asynchronously, and then wait for
 * them to complete if wait is set.
 */
int iscsi_login_portals(void *data, int *nr_found, int wait,
			struct list_head *rec_list,
			int (*login_fn)(void *, struct list_head *,
					 struct node_rec *))
{
	return __iscsi_login_portals(data, nr_found, wait, rec_list,
				     1, login_fn);
}

/**
 * iscsi_login_portals_safe - login into portals on @rec_list, but do not
 *			      clear out rec_list.
 */
int iscsi_login_portals_safe(void *data, int *nr_found, int wait,
			struct list_head *rec_list,
			int (*login_fn)(void *, struct list_head *,
					 struct node_rec *))
{
	return __iscsi_login_portals(data, nr_found, wait, rec_list,
				     0, login_fn);
}

static void log_logout_msg(struct session_info *info, int rc)
{
	if (rc) {
		log_error("Could not logout of [sid: %d, target: %s, "
			  "portal: %s,%d].", info->sid,
			  info->targetname,
			  info->persistent_address, info->port);
		iscsi_err_print_msg(rc);
	} else
		log_info("Logout of [sid: %d, target: %s, "
			 "portal: %s,%d] successful.",
			 info->sid, info->targetname,
			 info->persistent_address, info->port);
}

static int iscsid_logout_reqs_wait(struct list_head *list)
{
	struct iscsid_async_req *tmp, *curr;
	struct session_info *info;
	int ret = 0;

	list_for_each_entry_safe(curr, tmp, list, list) {
		int err;

		info  = curr->data;
		err = iscsid_req_wait(MGMT_IPC_SESSION_LOGOUT, curr->fd);
		log_logout_msg(info, err);
		if (err)
			ret = err;
		list_del(&curr->list);
		free(curr);
	}
	return ret;
}

/**
 * iscsi_logout_portal - logout of portal
 * @info: session to log out of
 * @list: if async, this is the list to add the logout req to
 */
int iscsi_logout_portal(struct session_info *info, struct list_head *list)
{
	struct iscsid_async_req *async_req = NULL;
	int fd, rc;

	/* TODO: add fn to add session prefix info like dev_printk */
	log_info("Logging out of session [sid: %d, target: %s, portal: "
		 "%s,%d]",
		 info->sid, info->targetname, info->persistent_address,
		 info->port);

	if (list) {
		async_req = calloc(1, sizeof(*async_req));
		if (!async_req)
			log_info("Could not allocate memory for async logout "
				 "handling. Using sequential logout instead.");
	}

	if (!async_req)
		rc = iscsid_req_by_sid(MGMT_IPC_SESSION_LOGOUT, info->sid);
	else {
		INIT_LIST_HEAD(&async_req->list);
		rc = iscsid_req_by_sid_async(MGMT_IPC_SESSION_LOGOUT,
					     info->sid, &fd);
	}

	/* we raced with another app or instance of iscsiadm */
	if (rc) {
		log_logout_msg(info, rc);
		if (async_req)
			free(async_req);
		return rc;
	}

	if (async_req) {
		list_add_tail(&async_req->list, list);
		async_req->fd = fd;
		async_req->data = info;
	} else
		log_logout_msg(info, rc);

	return 0;
}

/**
 * iscsi_logout_portals - logout portals
 * @data: data to pass to iter logout_fn
 * @nr_found: number of sessions logged out
 * @wait: bool indicating if the fn should wait for the result
 * @logout_fn: logout iter function
 *
 * This will loop over the list of sessions and run the logout fn
 * on them. It will attempt to logout asynchronously, and then wait for
 * them to complete if wait is set.
 */
int iscsi_logout_portals(void *data, int *nr_found, int wait,
			 int (*logout_fn)(void *, struct list_head *,
					  struct session_info *))
{
	struct session_info *curr_info;
	struct session_link_info link_info;
	struct list_head session_list, logout_list;
	int ret = 0, err;

	INIT_LIST_HEAD(&session_list);
	INIT_LIST_HEAD(&logout_list);

	memset(&link_info, 0, sizeof(link_info));
	link_info.list = &session_list;
	link_info.data = NULL;
	link_info.match_fn = NULL;
	*nr_found = 0;

	err = iscsi_sysfs_for_each_session(&link_info, nr_found,
					   session_info_create_list, 0);
	if (err && !list_empty(&session_list))
		log_error("Could not read in all sessions: %s",
			  iscsi_err_to_str(err));
	else if (err && list_empty(&session_list)) {
		log_error("Could not read session info.");
		return err;
	} else if (list_empty(&session_list))
		return ISCSI_ERR_NO_OBJS_FOUND;
	ret = err;
	*nr_found = 0;

	list_for_each_entry(curr_info, &session_list, list) {
		err = logout_fn(data, &logout_list, curr_info);
		if (err > 0 && !ret)
			ret = err;
		if (!err)
			(*nr_found)++;
	}

	if (!*nr_found) {
		ret = ISCSI_ERR_NO_OBJS_FOUND;
		goto free_list;
	}

	if (wait) {
		err = iscsid_logout_reqs_wait(&logout_list);
		if (err && !ret)
			ret = err;
	} else
		iscsid_reqs_close(&logout_list);

	if (ret)
		log_error("Could not logout of all requested sessions");

free_list:
	session_info_free_list(&session_list);
	return ret;
}

/* TODO merge with initiator.c implementation */
/* And add locking */
int iscsi_check_for_running_session(struct node_rec *rec)
{
	int nr_found = 0;
	if (iscsi_sysfs_for_each_session(rec, &nr_found, iscsi_match_session,
					 0))
		return 1;
	return 0;
}
