/* -*- linux-c -*- */
/*
 *
 *
 *   Copyright (c) International Business Machines  Corp., 2000
 *
 *   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, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *
 */
/*
 * linux/drivers/evms/s390_part.c
 *
 * EVMS S/390 partition manager
 *
 * Partial code extracted from
 *
 *  linux/fs/partitions/ibm.c
 *
 */

#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/config.h>
#include <linux/string.h>
#include <linux/blk.h>
#include <linux/init.h>
#include <asm/ebcdic.h>
#include <asm/uaccess.h>
#include <asm/dasd.h>
#include <asm/vtoc.h>
#include <linux/evms/evms.h>

/* prefix used in logging messages */
#define LOG_PREFIX "s390_part: "

/* Private instance data structure for node we produced */
struct local_instance_data {
	struct evms_logical_node *source_disk;
	u64 start_sect;		/* starting LBA */
	u64 nr_sects;		/* number of sectors */
	unsigned char type;	/* partition type or filesystem format indicator, can be set to 0 */
};

static int exported_nodes;	/* total # of exported segments
				   * produced during this discovery.
				 */

/* Prototypes */
static int s390_partition_discover(struct evms_logical_node **);
static int s390_partition_delete(struct evms_logical_node *);
static void s390_partition_read(struct evms_logical_node *,
				struct buffer_head *);
static void s390_partition_write(struct evms_logical_node *,
				 struct buffer_head *);
static int s390_partition_ioctl(struct evms_logical_node *,
				struct inode *,
				struct file *, unsigned int, unsigned long);
static int s390_partition_init_io(struct evms_logical_node *,
				  int, u64, u64, void *);

static struct evms_plugin_fops fops = {
	.discover = s390_partition_discover,
	.delete = s390_partition_delete,
	.read = s390_partition_read,
	.write = s390_partition_write,
	.init_io = s390_partition_init_io,
	.ioctl = s390_partition_ioctl
};

#define EVMS_S390_PARTITION_MANAGER_ID 2

static struct evms_plugin_header plugin_header = {
	.id = SetPluginID(IBM_OEM_ID,
			  EVMS_SEGMENT_MANAGER,
			  EVMS_S390_PARTITION_MANAGER_ID),
	.version = {
		    .major = 1,
		    .minor = 1,
		    .patchlevel = 2},
	.required_services_version = {
				      .major = 0,
				      .minor = 5,
				      .patchlevel = 0},
	.fops = &fops
};

/***************************************************/
/* List Support - Typedefs, Variables, & Functions */
/***************************************************/

/* Typedefs */

/* structure to keep status on
 * each disk.
 */
#define S390_DISK_OK			0
#define S390_DISK_FAILED		1
#define S390_FAILED_SKIP_COUNT		1024
struct disk_object {
	int flags;
	atomic_t skipped_ios;
	atomic_t pending_ios;
	atomic_t total_ios;
	atomic_t failed_ios;
	struct evms_logical_node *disk;
};

/* structure to keep status
 * on each device.
 */
struct device_object {
	unsigned char label[8];
	int total_paths;
	struct evms_list_node *disk_object_list;
	struct evms_list_node *segment_list;
};

/* structure used to track in-flight IOs, 
 * and to handle failover scenarios.
 */
struct s390_io {
	struct device_object *devo;
	struct disk_object *dsko;
	struct evms_logical_node *segment;
	int rw_flag;
	int paths_tried;
	struct buffer_head *bh;
	struct s390_io *next;
};
static spinlock_t s390_redrive_list_lock = SPIN_LOCK_UNLOCKED;
static struct s390_io *s390_redrive_list = NULL;
static struct evms_thread *s390_io_redrive_thread;
static struct evms_pool_mgmt *s390_io_track_pool = NULL;

/* Variables */

static struct evms_list_node *my_device_object_list;

static struct evms_list_node **
lookup_device_object(struct evms_logical_node *disk)
{
	struct evms_list_node **devoln;

	devoln = &my_device_object_list;
	while (*devoln) {
		struct evms_list_node **dskoln;
		struct device_object *devo;
		devo = (struct device_object *) (*devoln)->item;
		dskoln = &devo->disk_object_list;
		while (*dskoln) {
			struct disk_object *dsko;
			dsko = (struct disk_object *) (*dskoln)->item;
			if (dsko->disk == disk) {
				return (devoln);
			}
			dskoln = &(*dskoln)->next;
		}
		devoln = &(*devoln)->next;
	}
	return (devoln);
}

static struct evms_list_node **
lookup_label(unsigned char *label, struct evms_list_node **devoln)
{
	if (!devoln) {
		devoln = &my_device_object_list;
	} else {
		devoln = &(*devoln)->next;
	}
	while (*devoln) {
		struct device_object *devo;
		struct disk_object *dsko;
		devo = (struct device_object *) (*devoln)->item;
		dsko = (struct disk_object *) devo->disk_object_list->item;
		LOG_DEBUG("comparing labels: new(%s), %s(%s)\n",
			  label, dsko->disk->name, devo->label);
		if (!strncmp(devo->label, label, 6)) {
			LOG_DEBUG("matching label found!\n");
			break;
		}
		devoln = &(*devoln)->next;
	}
	return (devoln);
}

static struct evms_logical_node *
find_segment_on_disk(struct evms_logical_node *disk,
		     u64 start_sect, u64 nr_sects)
{
	struct evms_logical_node *rc = NULL;
	struct evms_list_node **devoln;

	/* find disk object */
	devoln = lookup_device_object(disk);
	if (*devoln) {
		/* disk object found in list */
		/* attempt to find segment */
		struct evms_list_node **sln;
		struct device_object *devo;

		devo = (struct device_object *) (*devoln)->item;
		sln = &devo->segment_list;
		while (*sln) {
			struct evms_logical_node *segment;
			struct local_instance_data *lid;

			segment = (struct evms_logical_node *) (*sln)->item;
			lid = segment->private;
			if (lid->start_sect == start_sect) {
				if (lid->nr_sects == nr_sects) {
					rc = segment;
					break;
				}
			}
			sln = &(*sln)->next;
		}
	}
	return (rc);
}

static int
add_segment_to_disk(struct evms_logical_node *disk,
		    unsigned char *label, struct evms_logical_node *segment)
{
	int rc = 0;
	struct evms_list_node **devoln;
	struct device_object *devo;

	devoln = lookup_device_object(disk);
	if (*devoln == NULL) {
		struct disk_object *dsko = NULL;
		/* device object not in list, add device object */
		devo = kmalloc(sizeof (*devo), GFP_KERNEL);
		if (devo) {
			memset(devo, 0, sizeof (*devo));
			strncpy(devo->label, label, 6);
			rc = evms_cs_add_item_to_list(devoln, devo);
		} else {
			rc = -ENOMEM;
		}

		/* create a disk object */
		if (!rc) {
			dsko = kmalloc(sizeof (*dsko), GFP_KERNEL);
			if (!dsko) {
				rc = -ENOMEM;
			}
		}
		if (!rc) {
			memset(dsko, 0, sizeof (*dsko));
			/* add disk to disk object */
			dsko->disk = disk;
			/* add disk object to disk object list
			 * in device object */
			rc = evms_cs_add_item_to_list(&devo->disk_object_list,
						      dsko);
		}
		if (!rc) {
			devo->total_paths++;
		} else {
			/* on error clean up allocations */
			if (dsko) {
				kfree(dsko);
			}
			if (*devoln) {
				evms_cs_remove_item_from_list(devoln, devo);
			}
			if (devo)
				kfree(devo);
		}
	} else {
		devo = (struct device_object *) (*devoln)->item;
	}
	if (!rc) {
		/* attempt to add segment */
		rc = evms_cs_add_item_to_list(&devo->segment_list, segment);
	}
	return (rc);
}

static int
remove_segment_from_disk(struct evms_logical_node *disk,
			 struct evms_logical_node *segment,
			 struct evms_list_node **empty_disk_object_list)
{
	int rc = -1;
	struct evms_list_node **devoln;

	*empty_disk_object_list = NULL;
	devoln = lookup_device_object(disk);
	if (*devoln) {
		/* device object found in list */
		/* attempt to remove segment */
		struct device_object *devo;
		devo = (struct device_object *) (*devoln)->item;
		rc = evms_cs_remove_item_from_list(&devo->segment_list,
						   segment);
		if (!rc) {
			if (devo->segment_list == NULL) {
				/* return disk object list to caller */
				*empty_disk_object_list =
				    devo->disk_object_list;
				/* remove device object from list */
				rc = evms_cs_remove_item_from_list(devoln,
								   devo);
				/* free device object */
				kfree(devo);
			}
		}
	}
	return (rc);
}

/* function: s390_load_balance
 *
 * this function is used to route an IO to the appropriate
 * paths of a multipath device.
 *
 * appropriate paths are determine used load balancing
 * techniques. load balancing is accomplished by monitoring
 * pending or in-flight IOs to each path. when a new IO
 * request is received, all paths are examined, and the path
 * with the fewest IOs pending is selected to receive the
 * new request.
 *
 * this routine also utilizes some failed path recovery
 * logic.
 *
 * if a failed path has been skipped for a given number
 * (timeout value) of IO requests. it is then tried again, 
 * and if the path has become functional again, it returned
 * to the active state and it becomes available for load
 * balancing.
 *
 * if a new IO arrives and we find no currently active paths,
 * each failed path will be attempted one time in the hopes
 * that it may have become active from the time between when
 * it was marked failed and now. only when all paths have
 * been tried and found non-active, is the IO marked with
 * an error and returned.
 *
 * this function works in concert with s390_end_io_callback
 * function and the s390iod(aemon), to redrive failed IO
 * requests.
 *
 */
static void
s390_load_balance(struct s390_io **piot, struct evms_logical_node *disk)
{
	struct evms_list_node **dskoln;
	struct disk_object *dsko, *selected_dsko = NULL;
	int dskidx, path = 0;
	struct s390_io *iot;

	/* allocate and initialize an IO tracking structure 
	 * if one was not passed in.
	 */
	if (!*piot) {
		struct evms_list_node **devoln;
		/* allocate IO Track struct */
		*piot = evms_cs_allocate_from_pool(s390_io_track_pool,
						   EVMS_BLOCKABLE);
		memset(*piot, 0, sizeof (*iot));
		/* find the device object */
		devoln = lookup_device_object(disk);
		(*piot)->devo = (*devoln)->item;
	}
	iot = *piot;

	/* find next disk object based on current load */

	/* check for failed paths that have timed-out */
	dskidx = 1;
	dskoln = &iot->devo->disk_object_list;
	while (*dskoln) {
		dsko = (struct disk_object *) (*dskoln)->item;
		do {
			/* skip paths tried earlier */
			if (iot->paths_tried & dskidx) {
				continue;
			}
			/* skip active disks */
			if (dsko->flags == S390_DISK_OK) {
				continue;
			}
			/* skip disks that haven't timed-out yet */
			if (atomic_read(&dsko->skipped_ios)
			    < S390_FAILED_SKIP_COUNT) {
				continue;
			}
			selected_dsko = dsko;
			path = dskidx;
			break;
		} while (0);
		dskoln = &(*dskoln)->next;
		dskidx <<= 1;
	}

	/* if we have no timed-out paths, then check for the
	 * path with lowest pending io count. if that path
	 * happens to be a failed path and there is active
	 * paths, increment the skipped io count, mark this
	 * path as having been selected, then go back and run
	 * the loop again, looking for the next best choice.
	 * continue this process until the best active has 
	 * been selected, or we end up with the best failed
	 * path.
	 */
	if (!selected_dsko) {
		int paths_selected, have_actives;
		paths_selected = 0;
	      s390_repeat_active_search:
		path = 0;
		have_actives = FALSE;
		dskidx = 1;
		dskoln = &iot->devo->disk_object_list;
		while (*dskoln) {
			dsko = (struct disk_object *) (*dskoln)->item;
			do {
				/* skip paths tried earlier */
				if (iot->paths_tried & dskidx) {
					continue;
				}
				/* skip previously selected disks */
				if (paths_selected & dskidx) {
					continue;
				}
				/* remember if we have active disks */
				if (dsko->flags == S390_DISK_OK) {
					have_actives = TRUE;
				}
				/* look for disk with smallest
				 * pending IO count.
				 */
				if (selected_dsko) {
					if (atomic_read(&dsko->pending_ios)
					    >=
					    (atomic_read
					     (&selected_dsko->pending_ios))) {
						continue;
					}
				}
				selected_dsko = dsko;
				path = dskidx;
			} while (0);
			dskoln = &(*dskoln)->next;
			dskidx <<= 1;
		}
		/* if we have unselected active paths
		 * and the currently selected path is
		 * failed, increment its skipped io count,
		 * and then go back to find an active path.
		 *
		 * this loop is structured this way so that
		 * we can accurately determine and track when
		 * a path has been skipped.
		 */
		if (have_actives && selected_dsko) {
			if (selected_dsko->flags & S390_DISK_FAILED) {
				atomic_inc(&selected_dsko->skipped_ios);
				paths_selected |= path;
				selected_dsko = NULL;
				goto s390_repeat_active_search;
			}
		}
	}

	/* if we have a selected path, perform the necessary
	 * bookkeeping on it.
	 */
	if (selected_dsko) {
		atomic_set(&selected_dsko->skipped_ios, 0);
		atomic_inc(&selected_dsko->pending_ios);
		atomic_inc(&selected_dsko->total_ios);
		iot->paths_tried |= path;
	}
	/* store the selected path (disk object) in the
	 * IO tracking structure, for examination by the
	 * caller.
	 */
	iot->dsko = selected_dsko;
}

static void
s390_end_io_callback(void *private,
		     struct buffer_head *bh, int uptodate, int *done)
{
	struct s390_io *iot;
	ulong flags;

	iot = private;

	/* update the disk object's status */
//      spin_lock_irqsave(iot->devo->device_object_lock, flags);
	atomic_dec(&iot->dsko->pending_ios);
	iot->dsko->flags = !uptodate;
//      spin_unlock_irqrestore(iot->devo->device_object_lock, flags);

	if (!uptodate) {
		atomic_inc(&iot->dsko->failed_ios);
		/* encountered error */

		/* is this a multipath device? */
		if (iot->devo->total_paths > 1) {
			/* yes, its a multipath device */

			/* determine alternate path */
			s390_load_balance(&iot, NULL);
			if (iot->dsko) {
				/* queue up redrive request */
				spin_lock_irqsave(&s390_redrive_list_lock,
						  flags);
				iot->next = s390_redrive_list;
				s390_redrive_list = iot;
				spin_unlock_irqrestore(&s390_redrive_list_lock,
						       flags);
				/* wake up redrive daemon */
				evms_cs_wakeup_thread(s390_io_redrive_thread);

				/* prevent the end_io to caller of EVMS */
				*done = TRUE;
			}
		}
	}
	if (*done == FALSE) {
		evms_cs_deallocate_to_pool(s390_io_track_pool, iot);
	}
}

/****************************************************
* Function: s390iod
*
* This is a kernel thread that handles read/write of mirrorss
* This shouldn't ever run on a non-mirrored LV read/write
* 
*
*****************************************************/
static void
s390iod(void *data)
{
	struct s390_io *iot;
	unsigned long flags;
	int rc;

	while (1) {
		spin_lock_irqsave(&s390_redrive_list_lock, flags);
		if (s390_redrive_list == NULL) {
			spin_unlock_irqrestore(&s390_redrive_list_lock, flags);
			break;
		}
		iot = s390_redrive_list;
		s390_redrive_list = iot->next;
		iot->next = NULL;
		spin_unlock_irqrestore(&s390_redrive_list_lock, flags);

		/* register for callback */
		rc = evms_cs_register_for_end_io_notification(iot, iot->bh,
							      s390_end_io_callback);
		if (rc) {
			LOG_ERROR
			    ("error(%d): unable to register for end io callback!\n",
			     rc);
		} else {
			/* redrive IO */
			if (!iot->rw_flag) {
				R_IO(iot->dsko->disk, iot->bh);
			} else {
				W_IO(iot->dsko->disk, iot->bh);
			}
		}
	}
}

/*
 * Function:  add_segment
 */
static int
s390_process_segment(struct evms_logical_node **discover_list,
		     struct evms_logical_node *node,
		     unsigned char *label,
		     u64 start_sect,
		     u64 nr_sects, unsigned char type, int part_num)
{
	struct local_instance_data *InstData = NULL;
	struct evms_logical_node *segment;
	int rc = 0;

	segment = find_segment_on_disk(node, start_sect, nr_sects);
	if (segment) {
		LOG_DETAILS("exporting segment '%s'.\n", segment->name);
	} else {
		InstData = kmalloc(sizeof (*InstData), GFP_KERNEL);
		if (InstData) {
			memset(InstData, 0, sizeof (*InstData));
			InstData->source_disk = node;
			InstData->start_sect = start_sect;
			InstData->nr_sects = nr_sects;
			InstData->type = type;
			rc = evms_cs_allocate_logical_node(&segment);
		} else {
			rc = -ENOMEM;
		}

		if (!rc) {
			segment->plugin = &plugin_header;
			segment->system_id = (unsigned int) type;
			segment->total_vsectors = nr_sects;
			segment->block_size = node->block_size;
			segment->hardsector_size = node->hardsector_size;
			segment->private = InstData;
			segment->flags = node->flags;
			strcpy(segment->name, node->name);
			sprintf(segment->name + strlen(segment->name), "%d",
				part_num);
			LOG_DETAILS("creating segment '%s'.\n", segment->name);
			rc = add_segment_to_disk(node, label, segment);
			if (rc) {
				LOG_ERROR
				    ("%s: error(%d) adding segment '%s'!\n",
				     __FUNCTION__, rc, segment->name);
				rc = 0;
			} else {
				MOD_INC_USE_COUNT;
			}
		}
		if (rc) {
			if (InstData)
				kfree(InstData);
			if (segment)
				evms_cs_deallocate_logical_node(segment);
		}
	}
	if (!rc) {
		evms_cs_add_logical_node_to_list(discover_list, segment);
		exported_nodes++;
	}
	return rc;
}

typedef enum {
	ibm_partition_lnx1 = 0,
	ibm_partition_vol1 = 1,
	ibm_partition_cms1 = 2,
	ibm_partition_none = 3
} ibm_partition_t;

static char *part_names[] = {
	[ibm_partition_lnx1] = "LNX1",
	[ibm_partition_vol1] = "VOL1",
	[ibm_partition_cms1] = "CMS1",
	[ibm_partition_none] = "(nonl)"
};

static ibm_partition_t
get_partition_type(char *type)
{
	int i;
	for (i = 0; i < 3; i++) {
		if (!strncmp(type, part_names[i], 4))
			break;
	}
	return i;
}

/*
 * compute the block number from a
 * cyl-cyl-head-head structure
 */
static inline int
cchh2blk(cchh_t * ptr, struct hd_geometry *geo)
{
	return ptr->cc * geo->heads * geo->sectors + ptr->hh * geo->sectors;
}

/*
 * compute the block number from a
 * cyl-cyl-head-head-block structure
 */
static inline int
cchhb2blk(cchhb_t * ptr, struct hd_geometry *geo)
{
	int block = 0;

	block = ptr->cc * geo->heads * geo->sectors + ptr->hh * geo->sectors;
	if (ptr->b) {
		block += ptr->b - 1;
	}

	return block;
}

static void
print_mem(void *buffer, int length)
{
	int i, done;
	unsigned char *bufptr;

	bufptr = (unsigned char *) buffer;
	i = done = 0;
	while (!done) {
		if ((i % 16) == 0)
			printk(KERN_INFO "\n0x%p->", buffer + i);
		printk(KERN_INFO "%02x ", bufptr[i]);
		if (++i >= length)
			done++;
	}
	printk(KERN_INFO "\n");
}

static int
s390_probe_multipath(struct evms_logical_node *disk,
		     unsigned char *label,
		     u64 label_lba, int label_offset, unsigned char *org_buf)
{
	int rc = FALSE;
	struct evms_list_node **devoln;
	unsigned char *sector_buf = NULL;

	LOG_ENTRY_EXIT("%s: Entry\n", __FUNCTION__);
	/* check if this disk is already known.
	 * if it is already in our device list
	 * then we don't need to check for
	 * multipath associations.
	 */
	devoln = lookup_device_object(disk);
	/* is this disk in our list? */
	if (*devoln) {
		struct device_object *devo;
		struct disk_object *dsko;
		/* yes, disk already known */

		/* we need to determine if this
		 * is our first path to this
		 * device.
		 */
		devo = (struct device_object *) (*devoln)->item;
		/* if this is the first path to this
		 * device, return FALSE so the main 
		 * routine will process its segments.
		 * if this is not the first path,
		 * return TRUE so the main routine
		 * will not process its segments.
		 */
		dsko = (struct disk_object *) devo->disk_object_list->item;
		if (dsko->disk != disk) {
			rc = TRUE;
		}
		/* only print multipath log msgs if its
		 * active on this device.
		 */
		if (devo->total_paths > 1) {
			LOG_DEBUG
			    ("skipping probe of known multipath device '%s'.\n",
			     disk->name);
		}
		LOG_ENTRY_EXIT("%s: Exit RC(%d)\n", __FUNCTION__, rc);
		return (rc);
	}

	/* search device object list for a matching label */
	devoln = NULL;
	while (*(devoln = lookup_label(label, devoln))) {
		struct device_object *devo;
		struct disk_object *dsko;
		unsigned char org_label[6];
#define S390_TEST_LABEL     "~!@#$"

		/* yes, found matching label */
		if (!sector_buf) {
			/* allocate buffer for incoming label sector */
			sector_buf = kmalloc(disk->hardsector_size, GFP_KERNEL);
			if (!sector_buf) {
				rc = -ENOMEM;
				break;
			}
		}

		/* save original label */
		memcpy(org_label, org_buf + label_offset, 6);
		/* alter label to test pattern */
		strcpy(org_buf + label_offset, S390_TEST_LABEL);
		/* write test pattern to this disk */
		LOG_DEBUG("writing test label to '%s'.\n", disk->name);
		rc = INIT_IO(disk, WRITE, label_lba, 1, org_buf);
		if (rc) {
			LOG_ERROR("error(%d) reading sector("PFU64") from '%s'.\n",
				  rc, label_lba, disk->name);
			break;
		}

		/* read label from device object with matching label */
		devo = (struct device_object *) (*devoln)->item;
		dsko = (struct disk_object *) devo->disk_object_list->item;
		LOG_DEBUG("reading label from '%s'.\n", dsko->disk->name);
		rc = INIT_IO(dsko->disk, READ, label_lba, 1, sector_buf);
		if (rc) {
			LOG_ERROR("error(%d) writing sector("PFU64") to '%s'.\n",
				  rc, label_lba, dsko->disk->name);
		}

		/* restore original label */
		memcpy(org_buf + label_offset, org_label, 6);
		LOG_DEBUG("restoring original label to '%s'.\n", disk->name);
		rc = INIT_IO(disk, WRITE, label_lba, 1, org_buf);
		if (rc) {
			LOG_ERROR("error(%d) reading sector("PFU64") from '%s'.\n",
				  rc, label_lba, disk->name);
			break;
		}

		LOG_DEBUG("checking label: %s(%6s), reference(%6s).\n",
			  dsko->disk->name,
			  sector_buf + label_offset, S390_TEST_LABEL);
		if (!strcmp(sector_buf + label_offset, S390_TEST_LABEL)) {
			LOG_DETAILS("assigning '%s' as first path to device.\n",
				    dsko->disk->name);
			LOG_DETAILS("assigning '%s' as next path to device.\n",
				    disk->name);
			/* store this disk in the disk object's
			 * disk list.
			 */
			/* create a disk object */
			dsko = NULL;
			if (!rc) {
				dsko = kmalloc(sizeof (*dsko), GFP_KERNEL);
				if (!dsko) {
					rc = -ENOMEM;
				}
			}
			if (!rc) {
				memset(dsko, 0, sizeof (*dsko));
				/* add disk to disk object */
				dsko->disk = disk;
				/* add disk object to disk object list
				 * in device object */
				rc = evms_cs_add_item_to_list(&devo->
							      disk_object_list,
							      dsko);
			}
			if (!rc) {
				devo->total_paths++;
			} else {
				if (dsko) {
					kfree(dsko);
				}
				break;
			}

			/* indicate we found a multipath device */
			rc = TRUE;
			break;
		}
	}
	if (sector_buf) {
		kfree(sector_buf);
	}

	LOG_ENTRY_EXIT("%s: Exit RC(%d)\n", __FUNCTION__, rc);
	return (rc);
}

static int
s390_probe_for_segments(struct evms_logical_node **discover_list,
			struct evms_logical_node *disk)
{
	char type[5] = { 0, }, name[7] = {
	0,};
	int rc, vsects_per_hardsect = 0;
	unsigned int blk;
	u64 io_start, label_lba = 3;
	dasd_information_t *info = NULL;
	struct hd_geometry *geo = NULL;
	unchar *data = NULL;

	/* allocate space for DASD ioctl packet
	 */
	info = kmalloc(sizeof (dasd_information_t), GFP_KERNEL);
	if (info) {
		memset(info, 0, sizeof (dasd_information_t));
		LOG_DEBUG("probing '%s' for 390 DASD info...\n", disk->name);
		/* issue DASD info ioctl
		 */
		rc = evms_cs_kernel_ioctl(disk, BIODASDINFO,
					  (unsigned long) info);
		if (rc) {
			LOG_DEBUG("error(%d) from BIODASDINFO ioctl.\n", rc);
			LOG_DEBUG("assuming '%s' is not a valid 390 device!\n",
				  disk->name);
		}
	} else {
		rc = -ENOMEM;
	}

	if (!rc) {
		/* if we successfully completed the previous
		 * get DASD info ioctl, we will assume that
		 * the device is a valid 390 disk.
		 *
		 * remove it from the discover list.
		 */
		rc = evms_cs_remove_logical_node_from_list(discover_list, disk);
		if (rc) {
			LOG_ERROR
			    ("error(%d) removing disk(%s) from discover list.\n",
			     rc, disk->name);
		}
	}
	if (!rc) {
		/* allocate space for the geometry packet
		 */
		geo = kmalloc(sizeof (struct hd_geometry), GFP_KERNEL);
		if (!geo) {
			rc = -ENOMEM;
		}
	}
	if (!rc) {
		memset(geo, 0, sizeof (struct hd_geometry));
		/* issue the Get GEO ioctl
		 */
		rc = evms_cs_kernel_ioctl(disk, HDIO_GETGEO,
					  (unsigned long) geo);
		if (rc) {
			LOG_ERROR("error(%d) from HDIO_GETGEO ioctl.\n", rc);
		}
	}
	if (!rc) {
		/* retrieve the vsects_per_hardsect (hardsector size)
		 */
		vsects_per_hardsect = disk->hardsector_size;
		vsects_per_hardsect >>= EVMS_VSECTOR_SIZE_SHIFT;
		data = kmalloc(EVMS_VSECTOR_SIZE, GFP_KERNEL);
		if (!data) {
			rc = -ENOMEM;
		}
	}
	if (!rc) {
		/* go read the 1st block on the disk
		 */
		label_lba = info->label_block * vsects_per_hardsect;
		io_start = label_lba;
		rc = INIT_IO(disk, READ, io_start, 1, data);
		if (rc) {
			LOG_ERROR("error(%d) reading sector("PFU64") from '%s'.\n",
				  rc, io_start, disk->name);
		} else {
//          print_mem(data, EVMS_VSECTOR_SIZE);
		}
	}
	if (!rc) {
		int offset, size, psize, counter = 0, label_offset;
		int vstart = 0, vend = 0;
		int vtoc_record_count, vtoc_index;
		format1_label_t f1;
		format4_label_t *f4;
		volume_label_t vlabel;
		ibm_partition_t partition_type;

		/* determine the format type
		 */

		strncpy(type, data, 4);
		if ((!info->FBA_layout) && (!strcmp(info->type, "ECKD"))) {
			label_offset = 8;
		} else {
			label_offset = 4;
		}
		strncpy(name, data + label_offset, 6);
		memcpy(&vlabel, data, sizeof (volume_label_t));

		EBCASC(type, 4);
		EBCASC(name, 6);
		partition_type = get_partition_type(type);
		LOG_DETAILS("disk: raw type(%s), type(%s), name(%s)\n",
			    type, part_names[partition_type], name);

		rc = s390_probe_multipath(disk, name, label_lba, label_offset,
					  data);
		if (!rc) {
			switch (partition_type) {
			case ibm_partition_cms1:
				if (*((long *) data + 13) != 0) {
					/* disk is reserved minidisk */
					long *label = (long *) data;
					vsects_per_hardsect =
					    label[3] >> EVMS_VSECTOR_SIZE_SHIFT;
					offset = label[13];
					size =
					    (label[7] -
					     1) * vsects_per_hardsect;
					LOG_DEBUG("(MDSK)");
				} else {
					offset = info->label_block + 1;
					size = disk->total_vsectors;
				}
				offset *= vsects_per_hardsect;
				/* adjust for 0 thru label block offset
				 */
				size -= offset;
				rc = s390_process_segment(discover_list,
							  disk,
							  name,
							  offset, size, 0, 1);
				break;
			case ibm_partition_lnx1:
			case ibm_partition_none:
				offset = info->label_block + 1;
				offset *= vsects_per_hardsect;
				size = disk->total_vsectors;
				/* adjust for 0 thru label block offset
				 */
				size -= offset;
				rc = s390_process_segment(discover_list,
							  disk,
							  name,
							  offset, size, 0, 1);
				break;
			case ibm_partition_vol1:
				/* set max dscb record count == single track till we see the vtoc descriptor */
				vtoc_record_count = geo->sectors;
				/* set current index into vtoc */
				vtoc_index = 0;
				/* get block number and read then first dscb */
				blk = cchhb2blk(&vlabel.vtoc, geo);
				io_start = blk * vsects_per_hardsect;
				rc = INIT_IO(disk, READ, io_start, 1, data);
				if (rc) {
					LOG_ERROR
					    ("error(%d) reading sector("PFU64") from '%s'.\n",
					     rc, io_start, disk->name);
					break;
				} else {
					//              print_mem(data, EVMS_VSECTOR_SIZE);
				}
				memcpy(&f1, data, sizeof (format1_label_t));

				// read vtoc records ... terminate when :
				// (1) we hit first NULL record
				// (2) we get an error processing a vtoc record
				// (3) we run out of vtoc records to process
				while (f1.DS1FMTID != 0x00 && rc == 0
				       && vtoc_index < vtoc_record_count) {
					if (f1.DS1FMTID == 0xf4) {	// vtoc descriptor
						f4 = (format4_label_t *) data;
						vstart =
						    cchh2blk(&f4->DS4VTOCE.
							     llimit, geo);
						vend =
						    cchh2blk(&f4->DS4VTOCE.
							     ulimit, geo);
						vtoc_record_count =
						    (vend - vstart) +
						    geo->sectors;
					} else if (f1.DS1FMTID == 0xf1) {	// dataset descriptor

						offset =
						    cchh2blk(&f1.DS1EXT1.llimit,
							     geo);
						psize =
						    cchh2blk(&f1.DS1EXT1.ulimit,
							     geo) - offset +
						    geo->sectors;

						counter++;
						rc = s390_process_segment
						    (discover_list, disk, name,
						     offset *
						     vsects_per_hardsect,
						     psize *
						     vsects_per_hardsect, 0,
						     counter);
					}
					if (!rc) {	// get next dscb
						++vtoc_index;
						++blk;
						io_start =
						    blk * vsects_per_hardsect;
						rc = INIT_IO(disk, READ,
							     io_start, 1, data);
						if (rc) {
							LOG_ERROR
							    ("error(%d) reading sector("PFU64") from '%s'.\n",
							     rc, io_start,
							     disk->name);
							break;
						} else {
							//                      print_mem(data, EVMS_VSECTOR_SIZE);
						}
						memcpy(&f1, data,
						       sizeof
						       (format1_label_t));
					}
				}
				break;
			default:
				rc = s390_process_segment(discover_list,
							  disk, name,
							  0, 0, 0, 1);
				break;
			}
		}
	}
	if (info) {
		kfree(info);
	}
	if (geo) {
		kfree(geo);
	}
	if (data)
		kfree(data);

	return (rc);
}

/*
 * Function: s390_partition_discover
 *
 */
static int
s390_partition_discover(struct evms_logical_node **discover_list)
{
	int rc = 0;
	struct evms_logical_node *node, *next_node;

	MOD_INC_USE_COUNT;
	LOG_ENTRY_EXIT("%s: ENTRY\n", __FUNCTION__);

	/* initialize global variable */
	exported_nodes = 0;

	/* examine each node on the discover list */
	next_node = *discover_list;
	while (next_node) {
		node = next_node;
		next_node = node->next;
		if (GetPluginType(node->plugin->id) != EVMS_DEVICE_MANAGER)
			/* only process disk nodes
			 */
			continue;
		if (node->iflags & EVMS_TOP_SEGMENT)
			continue;
		s390_probe_for_segments(discover_list, node);
	}

	LOG_ENTRY_EXIT("%s: EXIT(exported nodes:%d, error code:%d)\n",
		       __FUNCTION__, exported_nodes, rc);
	if (exported_nodes)
		rc = exported_nodes;
	MOD_DEC_USE_COUNT;
	return (rc);
}

/*
 * Function: s390_partition_delete
 *
 */
static int
s390_partition_delete(struct evms_logical_node *segment)
{
	int rc = 0;
	struct local_instance_data *LID;

	LOG_DETAILS("deleting segment '%s'.\n", segment->name);

	if (!segment) {
		rc = -ENODEV;
	} else {
		struct evms_list_node *empty_disk_object_list = NULL;
		LID = segment->private;
		if (LID) {
			/* remove the segment from the
			 * disk's segment list
			 */
			rc = remove_segment_from_disk(LID->source_disk,
						      segment,
						      &empty_disk_object_list);
			/* free the local instance data */
			kfree(LID);
		}
		/* free the segment node */
		evms_cs_deallocate_logical_node(segment);
		MOD_DEC_USE_COUNT;
		/* if the last segment on the disk was
		 * deleted, delete the disk node(s) too
		 */
		while (empty_disk_object_list) {
			struct disk_object *dsko;
			dsko =
			    (struct disk_object *) empty_disk_object_list->item;
			rc = evms_cs_remove_item_from_list
			    (&empty_disk_object_list, dsko);
			if (!rc) {
				rc = DELETE(dsko->disk);
				if (rc) {
					LOG_ERROR
					    ("error(%d): attempting to delete '%s'.\n",
					     rc, dsko->disk->name);
					rc = 0;
				}
			}
			kfree(dsko);
		}
	}
	return (rc);
}

/*
 * function: s390_partition_io_error
 *
 * this function was primarily created because the function
 * buffer_IO_error is inline and kgdb doesn't allow breakpoints
 * to be set on inline functions. Since this was an error path
 * and not mainline, I decided to add a trace statement to help
 * report on the failing condition.
 *
 */
static void
s390_partition_io_error(int rc,
			struct evms_logical_node *node,
			int io_flag, struct buffer_head *bh)
{
	switch (rc) {
	case 1:
		LOG_SERIOUS
		    ("attempt to %s beyond partition boundary("PFU64") on (%s), rsector(%ld).\n",
		     (io_flag) ? "WRITE" : "READ", node->total_vsectors - 1,
		     node->name, bh->b_rsector);
		break;
	case 2:
		LOG_ERROR
		    ("%s error(no active paths) on '%s' to drive the I/O.\n",
		     (io_flag) ? "WRITE" : "READ", node->name);
		break;
	}

	bh->b_end_io(bh, 0);
}

/*
 * Function: s390_partition_read
 *
 */
static void
s390_partition_read(struct evms_logical_node *partition, struct buffer_head *bh)
{
	struct local_instance_data *LID = partition->private;
	struct s390_io *iot = NULL;
	int rc = 1;

	if (bh->b_rsector + (bh->b_size >> EVMS_VSECTOR_SIZE_SHIFT) <=
	    partition->total_vsectors) {
		bh->b_rsector += LID->start_sect;

		s390_load_balance(&iot, LID->source_disk);
		if (iot->dsko) {
			iot->segment = partition;
			iot->bh = bh;
			iot->rw_flag = READ;
			/* register the callback */
			evms_cs_register_for_end_io_notification(iot, bh,
								 s390_end_io_callback);
			/* drive the IO */
			R_IO(iot->dsko->disk, bh);
			return;
		} else {
			rc = 2;
		}
	}
	s390_partition_io_error(rc, partition, READ, bh);
}

/*
 * Function: s390_partition_write
 *
 */
static void
s390_partition_write(struct evms_logical_node *partition,
		     struct buffer_head *bh)
{
	struct local_instance_data *LID = partition->private;
	struct s390_io *iot = NULL;
	int rc = 1;

	if (bh->b_rsector + (bh->b_size >> EVMS_VSECTOR_SIZE_SHIFT) <=
	    partition->total_vsectors) {
		bh->b_rsector += LID->start_sect;

		s390_load_balance(&iot, LID->source_disk);
		if (iot->dsko) {
			iot->segment = partition;
			iot->bh = bh;
			iot->rw_flag = WRITE;
			/* register the callback */
			evms_cs_register_for_end_io_notification(iot, bh,
								 s390_end_io_callback);
			/* drive the IO */
			W_IO(iot->dsko->disk, bh);
			return;
		} else {
			rc = 2;
		}
	}
	s390_partition_io_error(rc, partition, WRITE, bh);
}

/*
 * Function: s390_partition_init_io
 *
 */
static int
s390_partition_init_io(struct evms_logical_node *partition, int io_flag,	/* 0=read, 1=write */
		       u64 sect_nr,	/* disk LBA */
		       u64 num_sects,	/* # of sectors */
		       void *buf_addr)
{				/* buffer address */
	int rc;
	struct local_instance_data *LID = partition->private;
	struct s390_io *iot = NULL;

	if ((sect_nr + num_sects) <= partition->total_vsectors) {
		do {
			s390_load_balance(&iot, LID->source_disk);
			if (!iot->dsko) {
				rc = -EIO;
				break;
			}
			rc = INIT_IO(iot->dsko->disk, io_flag,
				     sect_nr + LID->start_sect, num_sects,
				     buf_addr);
			/* do disk object IO bookkeeping */
			atomic_dec(&iot->dsko->pending_ios);
			if (rc == -EIO) {
				atomic_inc(&iot->dsko->failed_ios);
				iot->dsko->flags = S390_DISK_FAILED;
			} else {
				iot->dsko->flags = S390_DISK_OK;
			}
		} while (rc == -EIO);
		evms_cs_deallocate_to_pool(s390_io_track_pool, iot);
	} else {
		LOG_SERIOUS
		    ("init_io: attempt to %s beyond partition(%s) boundary("PFU64") at sector("PFU64") for count("PFU64").\n",
		     (io_flag) ? "WRITE" : "READ", partition->name,
		     (LID->nr_sects - 1), sect_nr, num_sects);
		rc = -EINVAL;
	}

	return (rc);
}

/*
 * Function: s390_partition_ioctl
 *
 */
static int
s390_partition_ioctl(struct evms_logical_node *partition,
		     struct inode *inode,
		     struct file *file, unsigned int cmd, unsigned long arg)
{
	struct local_instance_data *LID;
	struct hd_geometry hd_geo;
	int rc;

	rc = 0;
	LID = partition->private;
	if (!inode)
		return -EINVAL;
	switch (cmd) {
	case HDIO_GETGEO:
		{
			rc = IOCTL(LID->source_disk, inode, file, cmd, arg);
			if (rc)
				break;
			if (copy_from_user
			    (&hd_geo, (void *) arg,
			     sizeof (struct hd_geometry)))
				rc = -EFAULT;
			if (rc)
				break;
			hd_geo.start = LID->start_sect;
			if (copy_to_user
			    ((void *) arg, &hd_geo,
			     sizeof (struct hd_geometry)))
				rc = -EFAULT;
		}
		break;
	case EVMS_GET_BMAP:
		{
			struct evms_get_bmap_pkt *bmap =
			    (struct evms_get_bmap_pkt *) arg;
			bmap->rsector += LID->start_sect;
			/* intentionally fall thru to
			 * default ioctl down to device
			 * manager.
			 */
		}
	default:
		rc = IOCTL(LID->source_disk, inode, file, cmd, arg);
	}
	return rc;
}

/*
 * Function: s390_part_init
 *
 */
static int __init
s390_part_init(void)
{
	const char *name = "evms_s390iod";

	/* create s390 IODaemon thread */
	s390_io_redrive_thread = evms_cs_register_thread(s390iod, NULL, name);
	/* create pool of IO tracking structures */
	s390_io_track_pool =
	    evms_cs_create_pool(sizeof (struct s390_io), "EVMS_s390_IO", NULL,
				NULL);

	return evms_cs_register_plugin(&plugin_header);	/* register with EVMS */
}

static void __exit
s390_part_exit(void)
{
	evms_cs_destroy_pool(s390_io_track_pool);
	evms_cs_unregister_plugin(&plugin_header);
}

module_init(s390_part_init);
module_exit(s390_part_exit);
#ifdef MODULE_LICENSE
MODULE_LICENSE("GPL");
#endif
