/*
 * Copyright 2010-2011 Red Hat, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License v2 or (at your option) any later version.
 */

#include <inttypes.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/sysmacros.h>
#include <sys/utsname.h>
#include <blkid/blkid.h>
#include <scsi/sg.h> /* SG_IO */
#include <aio.h>     /* posix aio */
#include <libdevmapper.h>

#include "sanlock_internal.h"
#include "diskio.h"
#include "direct.h"
#include "log.h"

int read_sysfs_uint(char *path, unsigned int *val)
{
	char buf[32] = { 0 };
	int len;
	int fd;
	int rv = -1;

	fd = open(path, O_RDONLY, 0);
	if (fd < 0)
		return -1;

	rv = read(fd, buf, sizeof(buf));
	if (rv < 0) {
		close(fd);
		return -1;
	}

	if ((len = strlen(buf)) && buf[len - 1] == '\n')
		buf[--len] = '\0';

	if (strlen(buf)) {
		*val = atoi(buf);
		rv = 0;
	}

	close(fd);
	return rv;
}

int write_sysfs_uint(char *path, unsigned int val)
{
	char buf[32] = { 0 };
	int fd;
	int rv;

	fd = open(path, O_RDWR, 0);
	if (fd < 0) {
		log_debug("write_sysfs_uint open error %d %s", errno, path);
		return -1;
	}

	snprintf(buf, sizeof(buf), "%u", val);

	rv = write(fd, buf, strlen(buf));
	if (rv < 0) {
		log_debug("write_sysfs_uint write %s error %d %s", buf, errno, path);
		close(fd);
		return -1;
	}

	close(fd);
	return 0;
}

int read_sysfs_size(const char *disk_path, const char *name, unsigned int *val)
{
	char sysfs_path[PATH_MAX] = { 0 };
	struct stat st;
	int ma, mi;
	int rv;

	rv = stat(disk_path, &st);
	if (rv < 0)
		return -1;

	ma = (int)major(st.st_rdev);
	mi = (int)minor(st.st_rdev);

	snprintf(sysfs_path, sizeof(sysfs_path), "/sys/dev/block/%d:%d/queue/%s", ma, mi, name);
	sysfs_path[PATH_MAX-1] = '\0';

	rv = read_sysfs_uint(sysfs_path, val);

	return rv;
}

static int write_sysfs_size(const char *disk_path, const char *name, unsigned int val)
{
	char sysfs_path[PATH_MAX] = { 0 };
	struct stat st;
	int major, minor;
	int rv;

	rv = stat(disk_path, &st);
	if (rv < 0) {
		log_debug("write_sysfs_size stat error %d %s", errno, disk_path);
		return -1;
	}

	major = (int)major(st.st_rdev);
	minor = (int)minor(st.st_rdev);

	snprintf(sysfs_path, sizeof(sysfs_path), "/sys/dev/block/%d:%d/queue/%s", major, minor, name);
	sysfs_path[PATH_MAX-1] = '\0';

	rv = write_sysfs_uint(sysfs_path, val);

	return rv;
}

/*
 * The default max_sectors_kb is 512 (KB), so a 1MB read is split into two
 * 512KB reads.  Adjust this to at least do 1MB io's.
 */

int set_max_sectors_kb(struct sync_disk *disk, uint32_t set_kb)
{
	unsigned int cur_kb = 0;
	int rv;

	rv = read_sysfs_size(disk->path, "max_sectors_kb", &cur_kb);
	if (rv < 0) {
		log_debug("set_max_sectors_kb read error %d %s", rv, disk->path);
		return rv;
	}

	if (cur_kb >= set_kb)
		return 0;

	rv = write_sysfs_size(disk->path, "max_sectors_kb", set_kb);
	if (rv < 0) {
		log_debug("set_max_sectors_kb write %u error %d %s", set_kb, rv, disk->path);
		return rv;
	}

	return 0;
}

int get_max_sectors_kb(struct sync_disk *disk, uint32_t *max_sectors_kb)
{
	unsigned int max = 0;
	int rv;

	rv = read_sysfs_size(disk->path, "max_sectors_kb", &max);
	if (!rv)
		*max_sectors_kb = max;
	return rv;
}

static int set_disk_properties(struct sync_disk *disk)
{
	blkid_probe probe;
	uint32_t sector_size;

	probe = blkid_new_probe_from_filename(disk->path);
	if (!probe) {
		log_error("cannot get blkid probe %s", disk->path);
		return -1;
	}

	sector_size = blkid_probe_get_sectorsize(probe);

	blkid_free_probe(probe);

	disk->sector_size = sector_size;

	return 0;
}

void close_disks(struct sync_disk *disks, int num_disks)
{
	int d;

	for (d = 0; d < num_disks; d++) {
		if (disks[d].fd == -1)
			continue;
		close(disks[d].fd);
		disks[d].fd = -1;
	}
}

int majority_disks(int num_disks, int num)
{
	if (num_disks == 1 && !num)
		return 0;

	/* odd number of disks */

	if (num_disks % 2)
		return num >= ((num_disks / 2) + 1);

	/* even number of disks */

	if (num > (num_disks / 2))
		return 1;

	if (num < (num_disks / 2))
		return 0;

	/* TODO: half of disks are majority if tiebreaker disk is present */
	return 0;
}

/* 
 * set fd in each disk
 * returns 0 if majority of disks were opened successfully, -EXXX otherwise
 */

int open_disks_fd(struct sync_disk *disks, int num_disks)
{
	struct sync_disk *disk;
	int num_opens = 0;
	int d, fd, rv = -1;

	for (d = 0; d < num_disks; d++) {
		disk = &disks[d];

		if (disk->fd != -1) {
			log_error("open fd %d exists %s", disk->fd, disk->path);
			rv = -1;
			goto fail;
		}

		fd = open(disk->path, O_RDWR | O_DIRECT | O_SYNC, 0);
		if (fd < 0) {
			rv = -errno;
			if (rv == -EACCES) {
				log_error("open error %d EACCES: no permission to open %s", rv, disk->path);
				log_error("check that daemon user %s %d group %s %d has access to disk or file.",
					  com.uname, com.uid, com.gname, com.gid);
			} else
				log_error("open error %d %s", fd, disk->path);
			continue;
		}

		disk->fd = fd;
		num_opens++;
	}

	if (!majority_disks(num_disks, num_opens)) {
		/* rv is open errno */
		goto fail;
	}

	return 0;

 fail:
	close_disks(disks, num_disks);
	return rv;
}

/* 
 * set fd and sector_size
 * verify offset is correctly aligned
 * returns 0 for success or -EXXX
 */

int open_disk(struct sync_disk *disk)
{
	struct stat st;
	int fd, rv;

	fd = open(disk->path, O_RDWR | O_DIRECT | O_SYNC, 0);
	if (fd < 0) {
		rv = -errno;
		if (rv == -EACCES) {
			log_error("open error %d EACCES: no permission to open %s", rv, disk->path);
			log_error("check that daemon user %s %d group %s %d has access to disk or file.",
				  com.uname, com.uid, com.gname, com.gid);
		} else
			log_error("open error %d %s", rv, disk->path);
		goto fail;
	}

	if (fstat(fd, &st) < 0) {
		rv = -errno;
		log_error("fstat error %d %s", rv, disk->path);
		close(fd);
		goto fail;
	}

	if (S_ISREG(st.st_mode)) {
		disk->sector_size = 512;
	} else {
		rv = set_disk_properties(disk);
		if (rv < 0) {
			close(fd);
			goto fail;
		}
	}

	disk->fd = fd;
	return 0;

 fail:
	if (rv >= 0)
		rv = -1;
	return rv;
}

/*
 * set fd and sector_size in each disk
 * verify all sector_size's match
 * returns 0 if majority of disks were opened successfully, -EXXX otherwise
 */

int open_disks(struct sync_disk *disks, int num_disks)
{
	struct sync_disk *disk;
	int num_opens = 0;
	int d, err, rv = -1;
	uint32_t ss = 0;

	for (d = 0; d < num_disks; d++) {
		disk = &disks[d];

		if (disk->fd != -1) {
			log_error("open fd %d exists %s", disk->fd, disk->path);
			rv = -ENOTEMPTY;
			goto fail;
		}

		err = open_disk(disk);
		if (err < 0) {
			rv = err;
			continue;
		}

		if (!ss) {
			ss = disk->sector_size;
		} else if (ss != disk->sector_size) {
			log_error("inconsistent sector sizes %u %u %s",
				  ss, disk->sector_size, disk->path);
		}

		num_opens++;
	}

	if (!majority_disks(num_disks, num_opens)) {
		/* rv is from open err */
		goto fail;
	}

	return 0;

 fail:
	close_disks(disks, num_disks);
	return rv;
}

static int do_write(int fd, uint64_t offset, const char *buf, int len, struct task *task, int *wr_ms)
{
	off_t ret;
	int rv;
	int pos = 0;
	int sys_error = 0; 
	int save_errno = 0;
	struct timespec begin, end, diff;
	const char *len_str;
	char off_str[16];
	char ms_str[8];

	if (task)
		task->io_count++;

	if (com.debug_io_submit) {
		len_str = align_size_debug_str(len);
		offset_to_str(offset, sizeof(off_str), off_str);

		if (len_str)
			log_taskd(task, "WR %s at %s", len_str, off_str);
		else
			log_taskd(task, "WR %d at %s", len, off_str);
	}

	ret = lseek(fd, offset, SEEK_SET);
	if (ret == -1) {
		log_taskw(task, "WR %d at %s seek errno %d", len, off_str, errno);
		return -1;
	}
	if (ret != offset) {
		log_taskw(task, "WR %d at %s seek error %llu", len, off_str, (unsigned long long)ret);
		return -1;
	}

	if (wr_ms)
		clock_gettime(CLOCK_MONOTONIC_RAW, &begin);

 retry:
	rv = write(fd, buf + pos, len);
	if (rv == -1 && errno == EINTR)
		goto retry;
	if (rv < 0) {
		sys_error = 1;
		save_errno = errno;
		goto out;
	}

	/* if (rv != len && len == sector_size) return error?
	   partial sector writes should not happen AFAIK, and
	   some uses depend on atomic single sector writes */

	if (rv != len) {
		len -= rv;
		pos += rv;
		goto retry;
	}

	if (sys_error)
		rv = -1;
	else
		rv = 0;

 out:
	if (wr_ms) {
		clock_gettime(CLOCK_MONOTONIC_RAW, &end);
		ts_diff(&begin, &end, &diff);
		*wr_ms = (diff.tv_sec * 1000) + (diff.tv_nsec / 1000000);
	}

	if (com.debug_io_complete || sys_error) {
		len_str = align_size_debug_str(len);
		offset_to_str(offset, sizeof(off_str), off_str);

		if (wr_ms) {
			memset(ms_str, 0, sizeof(ms_str));
			snprintf(ms_str, 7, "%u", *wr_ms);
		}

		if (len_str)
			log_taskd(task, "WR %s at %s done %s",
				  len_str, off_str, wr_ms ? ms_str : "");
		else
			log_taskd(task, "WR %d at %s done %s",
				  len, off_str, wr_ms ? ms_str : "");

		if (sys_error)
			log_taskw(task, "WR %d at %s error %d %s",
				  len, off_str, save_errno, wr_ms ? ms_str : "");
	}

	return rv;
}

static int do_read(int fd, uint64_t offset, char *buf, int len, struct task *task, int *rd_ms)
{
	off_t ret;
	int rv, pos = 0;
	int sys_error = 0;
	int save_errno = 0;
	struct timespec begin, end, diff;
	const char *len_str;
	char off_str[16];
	char ms_str[8];

	if (task)
		task->io_count++;

	if (com.debug_io_submit) {
		len_str = align_size_debug_str(len);
		offset_to_str(offset, sizeof(off_str), off_str);

		if (len_str)
			log_taskd(task, "RD %s at %s", len_str, off_str);
		else
			log_taskd(task, "RD %d at %s", len, off_str);
	}

	ret = lseek(fd, offset, SEEK_SET);
	if (ret != offset) {
		log_taskw(task, "RD %d at %s seek error %llu", len, off_str, (unsigned long long)ret);
		return -1;
	}

	if (rd_ms)
		clock_gettime(CLOCK_MONOTONIC_RAW, &begin);

	while (pos < len) {
		rv = read(fd, buf + pos, len - pos);
		if (rv == 0) {
			sys_error = 1;
			save_errno = errno;
			break;
		}
		if (rv == -1 && errno == EINTR)
			continue;
		if (rv < 0) {
			sys_error = 1;
			save_errno = errno;
			break;
		}
		pos += rv;
	}

	if (sys_error)
		rv = -1;
	else
		rv = 0;

	if (rd_ms) {
		clock_gettime(CLOCK_MONOTONIC_RAW, &end);
		ts_diff(&begin, &end, &diff);
		*rd_ms = (diff.tv_sec * 1000) + (diff.tv_nsec / 1000000);
	}

	if (com.debug_io_complete || sys_error) {
		len_str = align_size_debug_str(len);
		offset_to_str(offset, sizeof(off_str), off_str);

		if (rd_ms) {
			memset(ms_str, 0, sizeof(ms_str));
			snprintf(ms_str, 7, "%u", *rd_ms);
		}

		if (len_str)
			log_taskd(task, "RD %s at %s done %s",
				  len_str, off_str, rd_ms ? ms_str : "");
		else
			log_taskd(task, "RD %d at %s done %s",
				  len, off_str, rd_ms ? ms_str : "");

		if (sys_error)
			log_taskw(task, "RD %d at %s error %d %s",
				  len, off_str, save_errno, rd_ms ? ms_str : "");
	}

	return rv;
}

static struct aicb *find_callback_slot(struct task *task, int ioto)
{
	struct timespec ts;
	struct io_event event;
	int cleared = 0;
	int rv;
	int i;

 find:
	for (i = 0; i < task->cb_size; i++) {
		if (task->callbacks[i].used)
			continue;
		return &task->callbacks[i];
	}

	if (cleared++)
		return NULL;

	memset(&ts, 0, sizeof(struct timespec));
	ts.tv_sec = ioto;
 retry:
	memset(&event, 0, sizeof(event));

	rv = io_getevents(task->aio_ctx, 1, 1, &event, &ts);
	if (rv == -EINTR)
		goto retry;
	if (rv < 0)
		return NULL;
	if (rv == 1) {
		struct iocb *ev_iocb = event.obj;
		struct aicb *ev_aicb = container_of(ev_iocb, struct aicb, iocb);
		int op = ev_iocb ? ev_iocb->aio_lio_opcode : -1;
		const char *op_str;

		if (op == IO_CMD_PREAD)
			op_str = "RD";
		else if (op == IO_CMD_PWRITE)
			op_str = "WR";
		else
			op_str = "UK";

		log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld old free",
			  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);
		ev_aicb->used = 0;
		free(ev_aicb->buf);
		ev_aicb->buf = NULL;
		goto find;
	}
	return NULL;
}

void offset_to_str(unsigned long long offset, int buflen, char *off_str)
{
	uint64_t num_mb;

	if (!offset) {
		strncpy(off_str, "0", buflen);
	} else if (!(offset % 1048576)) {
		num_mb = offset / 1048576;
		snprintf(off_str, buflen, "%uM", (uint32_t)num_mb);
	} else {
		snprintf(off_str, buflen, "%llu", (unsigned long long)offset);
	}
}

/*
 * If this function returns SANLK_AIO_TIMEOUT, it means the io has timed out
 * and the event for the timed out io has not been reaped; the caller cannot
 * free the buf it passed in.  It will be freed by a subsequent call when the
 * event is reaped.  (Using my own error value here because I'm not certain
 * what values we might return from event.res.)
 */

static int do_linux_aio(int fd, uint64_t offset, char *buf, int len,
			struct task *task, int ioto, int cmd, int *ms)
{
	struct timespec ts;
	struct aicb *aicb;
	struct iocb *iocb;
	struct io_event event;
	struct timespec begin, end, diff;
	const char *op_str;
	const char *len_str;
	char ms_str[8];
	char off_str[16];
	int rv;

	if (!ioto) {
		log_taske(task, "aio %d zero io timeout", cmd);
		return -EINVAL;
	}

	/* I expect this pre-emptively catches the io_submit EAGAIN case */

	aicb = find_callback_slot(task, ioto);
	if (!aicb)
		return -ENOENT;

	iocb = &aicb->iocb;

	memset(iocb, 0, sizeof(struct iocb));
	iocb->aio_fildes = fd;
	iocb->aio_lio_opcode = cmd;
	iocb->u.c.buf = buf;
	iocb->u.c.nbytes = len;
	iocb->u.c.offset = offset;

	if (cmd == IO_CMD_PREAD)
		op_str = "RD";
	else if (cmd == IO_CMD_PWRITE)
		op_str = "WR";
	else
		op_str = "UK";

	if (com.debug_io_submit) {
		len_str = align_size_debug_str(len);
		offset_to_str(offset, sizeof(off_str), off_str);

		if (len_str)
			log_taskd(task, "%s %s at %s", op_str, len_str, off_str);
		else
			log_taskd(task, "%s %d at %s", op_str, len, off_str);
	}

	if (ms)
		clock_gettime(CLOCK_MONOTONIC_RAW, &begin);

	rv = io_submit(task->aio_ctx, 1, &iocb);
	if (rv < 0) {
		log_taske(task, "aio submit %d %p:%p:%p rv %d fd %d",
			  cmd, aicb, iocb, buf, rv, fd);
		goto out;
	}

	task->io_count++;

	/* don't reuse aicb->iocb or free the buf until we reap the event */
	aicb->used = 1;
	aicb->buf = buf;

	memset(&ts, 0, sizeof(struct timespec));
	ts.tv_sec = ioto;
 retry:
	memset(&event, 0, sizeof(event));

	rv = io_getevents(task->aio_ctx, 1, 1, &event, &ts);
	if (rv == -EINTR)
		goto retry;
	if (rv < 0) {
		log_taske(task, "aio getevent %p:%p:%p rv %d",
			  aicb, iocb, buf, rv);
		goto out;
	}
	if (rv == 1) {
		struct iocb *ev_iocb = event.obj;
		struct aicb *ev_aicb = container_of(ev_iocb, struct aicb, iocb);
		int op = ev_iocb ? ev_iocb->aio_lio_opcode : -1;

		if (op == IO_CMD_PREAD)
			op_str = "RD";
		else if (op == IO_CMD_PWRITE)
			op_str = "WR";
		else
			op_str = "UK";

		if (ms) {
			clock_gettime(CLOCK_MONOTONIC_RAW, &end);
			ts_diff(&begin, &end, &diff);
			*ms = (diff.tv_sec * 1000) + (diff.tv_nsec / 1000000);
		}

		ev_aicb->used = 0;

		if (ev_iocb != iocb) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld other free",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);
			free(ev_aicb->buf);
			ev_aicb->buf = NULL;
			goto retry;
		}
		if ((int)event.res < 0) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld match res",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);
			rv = event.res;
			goto out;
		}
		if (event.res != len) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld match len %d",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2, len);
			rv = -EMSGSIZE;
			goto out;
		}

		/* standard success case */

		if (com.debug_io_complete) {
			len_str = align_size_debug_str(len);
			offset_to_str(offset, sizeof(off_str), off_str);

			if (ms) {
				memset(ms_str, 0, sizeof(ms_str));
				snprintf(ms_str, 7, "%u", *ms);
			}

			if (len_str)
				log_taskd(task, "%s %s at %s done %s",
					  op_str, len_str, off_str, ms ? ms_str : "");
			else
				log_taskd(task, "%s %d at %s done %s",
					  op_str, len, off_str, ms ? ms_str : "");
		}

		rv = 0;
		goto out;
	}

	/* Timed out waiting for result.  If cancel fails, we could try retry
	   io_getevents indefinitely, but that removes the whole point of using
	   aio, which is the timeout.  So, we need to be prepared to reap the
	   event the next time we call io_getevents for a different i/o.  We
	   can't reuse the iocb for this timed out io until we get an event for
	   it because we need to compare the iocb to event.obj to distinguish
	   events for separate submissions.

	   <phro> dct: io_cancel doesn't work, in general.  you are very
	   likely going to get -EINVAL from that call */

	task->to_count++;

	if (cmd == IO_CMD_PREAD)
		op_str = "RD";
	else if (cmd == IO_CMD_PWRITE)
		op_str = "WR";
	else
		op_str = "UK";

	log_taskw(task, "aio timeout %s %p:%p:%p ioto %d to_count %d",
		  op_str, aicb, iocb, buf, ioto, task->to_count);

	rv = io_cancel(task->aio_ctx, iocb, &event);
	if (!rv) {
		aicb->used = 0;
		rv = -ECANCELED;
	} else {
		/* aicb->used and aicb->buf both remain set */
		rv = SANLK_AIO_TIMEOUT;

		if (cmd == IO_CMD_PREAD)
			task->read_iobuf_timeout_aicb = aicb;
	}
 out:
	return rv;
}

static int do_write_aio_linux(int fd, uint64_t offset, char *buf, int len,
			      struct task *task, int ioto, int *wr_ms)
{
	return do_linux_aio(fd, offset, buf, len, task, ioto, IO_CMD_PWRITE, wr_ms);
}

static int do_read_aio_linux(int fd, uint64_t offset, char *buf, int len,
			     struct task *task, int ioto, int *rd_ms)
{
	return do_linux_aio(fd, offset, buf, len, task, ioto, IO_CMD_PREAD, rd_ms);
}

int write_iobuf(int fd, uint64_t offset, char *iobuf, int iobuf_len,
		struct task *task, int use_aio, int ioto, int *wr_ms)
{
	if (use_aio == USE_AIO)
		return do_write_aio_linux(fd, offset, iobuf, iobuf_len, task, ioto, wr_ms);
	else if (use_aio == NO_AIO)
		return do_write(fd, offset, iobuf, iobuf_len, task, wr_ms);
	else
		return SANLK_IOTYPE;
}

static int _write_sectors(const struct sync_disk *disk, int sector_size, uint64_t sector_nr,
			  uint32_t sector_count GNUC_UNUSED,
			  const char *data, int data_len, int iobuf_len,
			  struct task *task, int use_aio, int ioto,
			  const char *blktype)
{
	char *iobuf, **p_iobuf;
	uint64_t offset;
	int rv;

	offset = disk->offset + (sector_nr * sector_size);

	p_iobuf = &iobuf;

	rv = posix_memalign((void *)p_iobuf, getpagesize(), iobuf_len);
	if (rv) {
		log_error("write_sectors %s posix_memalign rv %d %s",
			  blktype, rv, disk->path);
		rv = -ENOMEM;
		goto out;
	}

	memset(iobuf, 0, iobuf_len);
	memcpy(iobuf, data, data_len);

	rv = write_iobuf(disk->fd, offset, iobuf, iobuf_len, task, use_aio, ioto, NULL);
	if (rv < 0) {
		log_error("write_sectors %s offset %llu rv %d %s",
			  blktype, (unsigned long long)offset, rv, disk->path);
	}

	if (rv != SANLK_AIO_TIMEOUT)
		free(iobuf);
 out:
	return rv;
}

/* sector_nr is logical sector number within the sync_disk.
   the sync_disk itself begins at disk->offset (in bytes) from
   the start of the block device identified by disk->path,
   data_len must be <= sector_size */

int write_sector(const struct sync_disk *disk, int sector_size, uint64_t sector_nr,
		 const char *data, int data_len,
		 struct task *task, int use_aio, int ioto,
		 const char *blktype)
{
	int iobuf_len = sector_size;

	if ((sector_size != 4096) && (sector_size != 512)) {
		log_error("write_sector bad sector_size %d", sector_size);
		return -EINVAL;
	}

	if (data_len > iobuf_len) {
		log_error("write_sector %s data_len %d max %d %s",
			  blktype, data_len, iobuf_len, disk->path);
		return -1;
	}

	return _write_sectors(disk, sector_size, sector_nr, 1, data, data_len,
			      iobuf_len, task, use_aio, ioto, blktype);
}

/* write multiple complete sectors, data_len must be multiple of sector size */

int write_sectors(const struct sync_disk *disk, int sector_size, uint64_t sector_nr,
		  uint32_t sector_count, const char *data, int data_len,
		  struct task *task, int use_aio, int ioto,
		  const char *blktype)
{
	int iobuf_len = data_len;

	if ((sector_size != 4096) && (sector_size != 512)) {
		log_error("write_sectors bad sector_size %d", sector_size);
		return -EINVAL;
	}

	if (data_len != sector_count * sector_size) {
		log_error("write_sectors %s data_len %d sector_count %d %s",
			  blktype, data_len, sector_count, disk->path);
		return -1;
	}

	return _write_sectors(disk, sector_size, sector_nr, sector_count, data, data_len,
			      iobuf_len, task, use_aio, ioto, blktype);
}

int read_iobuf(int fd, uint64_t offset, char *iobuf, int len,
	       struct task *task, int use_aio, int ioto, int *rd_ms)
{
	if (use_aio == USE_AIO)
		return do_read_aio_linux(fd, offset, iobuf, len, task, ioto, rd_ms);
	else if (use_aio == NO_AIO)
		return do_read(fd, offset, iobuf, len, task, rd_ms);
	else
		return SANLK_IOTYPE;
}

/* read sector_count sectors starting with sector_nr, where sector_nr
   is a logical sector number within the sync_disk.  the caller will
   generally want to look at the first N bytes of each sector.
   when reading multiple sectors, data_len will generally equal iobuf_len,
   but when reading one sector, data_len may be less than iobuf_len. */

int read_sectors(const struct sync_disk *disk, int sector_size, uint64_t sector_nr,
	 	 uint32_t sector_count, char *data, int data_len,
		 struct task *task, int use_aio, int ioto,
		 const char *blktype)
{
	char *iobuf, **p_iobuf;
	uint64_t offset;
	int iobuf_len;
	int rv;

	if ((sector_size != 512) && (sector_size != 4096)) {
		log_error("read_sectors %s bad sector_size %d", blktype, sector_size);
		return -EINVAL;
	}

	iobuf_len = sector_count * sector_size;
	offset = disk->offset + (sector_nr * sector_size);

	p_iobuf = &iobuf;

	rv = posix_memalign((void *)p_iobuf, getpagesize(), iobuf_len);
	if (rv) {
		log_error("read_sectors %s posix_memalign rv %d %s",
			  blktype, rv, disk->path);
		rv = -ENOMEM;
		goto out;
	}

	memset(iobuf, 0, iobuf_len);

	rv = read_iobuf(disk->fd, offset, iobuf, iobuf_len, task, use_aio, ioto, NULL);
	if (!rv) {
		memcpy(data, iobuf, data_len);
	} else if (rv == -EINVAL) {
		/* when disk sector size is not yet known, this happens when
		   initially reading 512 but the disk has 4k sectors */
		log_debug("read_sectors %s offset %llu rv %d %s",
			  blktype, (unsigned long long)offset, rv, disk->path);
	} else {
		log_error("read_sectors %s offset %llu rv %d %s",
			  blktype, (unsigned long long)offset, rv, disk->path);
	}

	if (rv != SANLK_AIO_TIMEOUT)
		free(iobuf);
 out:
	return rv;
}

int read_sector(const struct sync_disk *disk, int sector_size, uint64_t sector_nr,
		char *data, int data_len,
		struct task *task, int use_aio, int ioto,
		const char *blktype)
{
	return read_sectors(disk, sector_size, sector_nr, 1,
			    data, data_len, task, use_aio, ioto, blktype);
}

/* Try to reap the event of a previously timed out read_iobuf.
   The aicb used in a task's last timed out read_iobuf is
   task->read_iobuf_timeout_aicb . */

int read_iobuf_reap(int fd, uint64_t offset, char *iobuf, int read_len,
		    struct task *task, uint32_t ioto_msec)
{
	struct timespec ts;
	struct aicb *aicb;
	struct iocb *iocb;
	struct io_event event;
	int rv;

	aicb = task->read_iobuf_timeout_aicb;
	iocb = &aicb->iocb;

	if (!aicb->used)
		return -EINVAL;
	if (iocb->aio_fildes != fd)
		return -EINVAL;
	if (iocb->u.c.buf != iobuf)
		return -EINVAL;
	if (iocb->u.c.nbytes != read_len)
		return -EINVAL;
	if (iocb->u.c.offset != offset)
		return -EINVAL;
	if (iocb->aio_lio_opcode != IO_CMD_PREAD)
		return -EINVAL;

	memset(&ts, 0, sizeof(struct timespec));
	ts.tv_sec = ioto_msec / 1000;
	ts.tv_nsec = (ioto_msec % 1000) * 1000000;
 retry:
	memset(&event, 0, sizeof(event));

	rv = io_getevents(task->aio_ctx, 1, 1, &event, &ts);
	if (rv == -EINTR)
		goto retry;
	if (rv < 0) {
		log_taske(task, "aio getevent %p:%p:%p rv %d r",
			  aicb, iocb, iobuf, rv);
		goto out;
	}
	if (rv == 1) {
		struct iocb *ev_iocb = event.obj;
		struct aicb *ev_aicb = container_of(ev_iocb, struct aicb, iocb);
		int op = ev_iocb ? ev_iocb->aio_lio_opcode : -1;
		const char *op_str;

		if (op == IO_CMD_PREAD)
			op_str = "RD";
		else if (op == IO_CMD_PWRITE)
			op_str = "WR";
		else
			op_str = "UK";

		ev_aicb->used = 0;

		if (ev_iocb != iocb) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld other free r",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);
			free(ev_aicb->buf);
			ev_aicb->buf = NULL;
			goto retry;
		}
		if ((int)event.res < 0) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld match res r",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);
			rv = event.res;
			goto out;
		}
		if (event.res != read_len) {
			log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld match len %d r",
				  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2, read_len);
			rv = -EMSGSIZE;
			goto out;
		}

		log_taskw(task, "aio collect %s %p:%p:%p result %ld:%ld match reap",
			  op_str, ev_aicb, ev_iocb, ev_aicb->buf, event.res, event.res2);

		rv = 0;
		goto out;
	}

	/* timed out again */
	rv = SANLK_AIO_TIMEOUT;
 out:
	return rv;
}

int lv_to_target(uint32_t major, uint32_t minor, uint64_t offset,
		 uint32_t *target_major, uint32_t *target_minor, uint64_t *target_offset)
{
	struct dm_task *dmt;
	struct dm_info info;
	void *next = NULL;
	char *target_type, *params;
	uint64_t start, length;
	uint64_t tgt_offset;
	int tgt_major, tgt_minor;
	int ret = -1;

	if (!target_major || !target_minor || !target_offset)
		return -1;

	if (!(dmt = dm_task_create(DM_DEVICE_TABLE))) {
		log_debug("lv_to_target dm_task_create error");
		return -1;
	}

	if (!dm_task_set_major_minor(dmt, major, minor, 0)) {
		log_debug("lv_to_target dm_task_set_major_minor error");
		goto out;
	}

	if (!dm_task_run(dmt)) {
		log_debug("dm_task_run error");
		goto out;
	}

	if (!dm_task_get_info(dmt, &info)) {
		log_debug("dm_task_info error");
		goto out;
	}

	if (!info.exists) {
		log_debug("dm_task_info does not exist");
		goto out;
	}

	/* Iterate through targets to find the one containing our offset */
	do {
		next = dm_get_next_target(dmt, next, &start, &length, &target_type, &params);

		/* Check if offset falls within this target's range */
		if (offset >= start && offset < (start + length)) {
			if (!target_type || strcmp(target_type, "linear") != 0) {
				log_debug("lv_to_target dm target at offset %" PRIu64 " is not linear (type: %s)",
					  offset, target_type ? target_type : "unknown");
				goto out;
			}

			/* Parse linear target parameters: "major:minor offset" */
			if (!params || sscanf(params, "%d:%d %" SCNu64,
					      &tgt_major, &tgt_minor, &tgt_offset) != 3) {
				log_debug("lv_to_target failed to parse linear target parameters: %s",
					  params ? params : "(null)");
				goto out;
			}

			/* Calculate the actual offset in the target device */
			*target_major = (uint32_t)tgt_major;
			*target_minor = (uint32_t)tgt_minor;
			*target_offset = tgt_offset + (offset - start);
			ret = 0;
			break;
		}
	} while (next);

	if (ret)
		log_debug("lv_to_target offset %" PRIu64 " not found in device %u:%u table", offset, major, minor);
out:
	dm_task_destroy(dmt);
	return ret;
}

int dev_is_lv(uint32_t major, uint32_t minor)
{
	char path[PATH_MAX] = { 0 };
	char uuid[256] = { 0 };
	ssize_t num;
	int fd;

	snprintf(path, PATH_MAX-1, "/sys/dev/block/%u:%u/dm/uuid", major, minor);

	if ((fd = open(path, O_RDONLY, 0)) < 0)
		return 0;

	num = read(fd, uuid, sizeof(uuid)-1);
	close(fd);

	if ((num > 4) && !strncmp(uuid, "LVM-", 4))
		return 1;
	return 0;
}

int dev_is_mpath(uint32_t major, uint32_t minor)
{

	char path[PATH_MAX] = { 0 };
	char uuid[256] = { 0 };
	ssize_t num;
	int fd;

	snprintf(path, PATH_MAX-1, "/sys/dev/block/%u:%u/dm/uuid", major, minor);

	if ((fd = open(path, O_RDONLY, 0)) < 0)
		return 0;

	num = read(fd, uuid, sizeof(uuid)-1);
	close(fd);

	if ((num > 6) && !strncmp(uuid, "mpath-", 6))
		return 1;
	return 0;
}

int dev_is_scsi(uint32_t major, uint32_t minor)
{
	char path[PATH_MAX] = { 0 };
	struct stat st;

	snprintf(path, PATH_MAX-1, "/sys/dev/block/%u:%u/device/scsi_device", major, minor);

	if (stat(path, &st) < 0)
		return 0;
	return 1;
}

int devno_to_path(uint32_t major, uint32_t minor, char *path)
{
	char name[128] = { 0 };

	if (!dm_device_get_name(major, minor, 1, name, sizeof(name)))
		return -1;

	snprintf(path, PATH_MAX-1, "/dev/%s", name);
	return 0;
}

/*
static void log_debug_data(unsigned char *data, int len)
{
	int i;

	for (i = 0; i < len ; i++) {
		if (!(i % 32))
			printf("\n");
		printf("%02x ", data[i]);
	}
	printf("\n");
}
*/

/* SCSI sense key definitions */
#define SENSE_KEY_NO_SENSE		0x00
#define SENSE_KEY_RECOVERED_ERROR	0x01
#define SENSE_KEY_NOT_READY		0x02
#define SENSE_KEY_MEDIUM_ERROR		0x03
#define SENSE_KEY_HARDWARE_ERROR	0x04
#define SENSE_KEY_ILLEGAL_REQUEST	0x05
#define SENSE_KEY_UNIT_ATTENTION	0x06
#define SENSE_KEY_DATA_PROTECT		0x07
#define SENSE_KEY_ABORTED_COMMAND	0x0B
#define SENSE_KEY_MISCOMPARE		0x0E

/* SCSI host status definitions */
#define DID_OK				0x00
#define DID_NO_CONNECT			0x01
#define DID_BUS_BUSY			0x02
#define DID_TIME_OUT			0x03
#define DID_BAD_TARGET			0x04
#define DID_ABORT			0x05
#define DID_ERROR			0x07
#define DID_RESET			0x08
#define DID_TRANSPORT_DISRUPTED		0x0e
#define DID_TARGET_FAILURE		0x10
#define DID_NEXUS_FAILURE		0x11

/* SCSI driver status definitions */
#define DRIVER_OK			0x00
#define DRIVER_BUSY			0x01
#define DRIVER_SOFT			0x02
#define DRIVER_MEDIA			0x03
#define DRIVER_ERROR			0x04
#define DRIVER_INVALID			0x05
#define DRIVER_TIMEOUT			0x06
#define DRIVER_SENSE			0x08

#define FLAG_FUA        0x8
#define FLAG_DPO        0x10
#define CAW_CDB_SIZE	16

/*
 * Log SCSI sense key with detailed information
 * Returns error code to use:
 * 0 for success
 * errors SANLK_CAW_MISCOMPARE/SANLK_CAW_ERROR/SANLK_CAW_ILLEGAL
 */
static int check_scsi_sense_key(struct task *task, uint32_t major, uint32_t minor,
			      uint64_t lba, int sector_size, unsigned char sense_key,
			      unsigned char asc, unsigned char ascq, uint64_t info,
			      int info_valid, int may_fail)
{
	int level = may_fail ? LOG_DEBUG : LOG_ERR;

	switch (sense_key) {
	case SENSE_KEY_NO_SENSE:
	case SENSE_KEY_RECOVERED_ERROR:
		/* No error or recovered error - treat as success */
		return 0;

	case SENSE_KEY_MISCOMPARE:
		if (info_valid) {
			log_debug("scsi_compare_and_write %u:%u lba %llu miscompare at byte offset %llu ASC/ASCQ 0x%02x/0x%02x",
				  major, minor, (unsigned long long)lba,
				  (unsigned long long)info, asc, ascq);
		} else {
			log_debug("scsi_compare_and_write %u:%u lba %llu miscompare ASC/ASCQ 0x%02x/0x%02x",
				  major, minor, (unsigned long long)lba, asc, ascq);
		}
		return SANLK_CAW_MISCOMPARE;

	case SENSE_KEY_ILLEGAL_REQUEST:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu illegal request ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, asc, ascq);
		return SANLK_CAW_ILLEGAL;

	case SENSE_KEY_NOT_READY:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu device not ready ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, asc, ascq);
		return SANLK_CAW_ERROR;

	case SENSE_KEY_MEDIUM_ERROR:
	case SENSE_KEY_HARDWARE_ERROR:
		if (info_valid) {
			log_level(0, 0, NULL, level,
				  "scsi_compare_and_write %u:%u lba %llu %s error at lba %llu ASC/ASCQ 0x%02x/0x%02x",
				  major, minor, (unsigned long long)lba,
				  (sense_key == SENSE_KEY_MEDIUM_ERROR) ? "medium" : "hardware",
				  (unsigned long long)info, asc, ascq);
		} else {
			log_level(0, 0, NULL, level,
				  "scsi_compare_and_write %u:%u lba %llu %s error ASC/ASCQ 0x%02x/0x%02x",
				  major, minor, (unsigned long long)lba,
				  (sense_key == SENSE_KEY_MEDIUM_ERROR) ? "medium" : "hardware",
				  asc, ascq);
		}
		return SANLK_CAW_ERROR;

	case SENSE_KEY_DATA_PROTECT:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu data protect error ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, asc, ascq);
		return SANLK_CAW_ERROR;

	case SENSE_KEY_UNIT_ATTENTION:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu unit attention ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, asc, ascq);
		return SANLK_CAW_ERROR;

	case SENSE_KEY_ABORTED_COMMAND:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu command aborted ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, asc, ascq);
		return SANLK_CAW_ERROR;

	default:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu sense key 0x%02x ASC/ASCQ 0x%02x/0x%02x",
			  major, minor, (unsigned long long)lba, sense_key, asc, ascq);
		return SANLK_CAW_ERROR;
	}
}

static void log_scsi_status(uint32_t major, uint32_t minor, uint64_t lba,
			    int sector_size, uint32_t scsi_status, int may_fail)
{
	int level = may_fail ? LOG_DEBUG : LOG_ERR;
	const char *status_str;

	switch (scsi_status) {
	case 0x02:
		status_str = "check condition";
		break;
	case 0x08:
		status_str = "busy";
		break;
	case 0x18:
		status_str = "reservation conflict";
		break;
	case 0x28:
		status_str = "task set full";
		break;
	default:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu ss %d scsi_status 0x%02x",
			  major, minor, (unsigned long long)lba, sector_size, scsi_status);
		return;
	}

	log_level(0, 0, NULL, level,
		  "scsi_compare_and_write %u:%u lba %llu ss %d %s",
		  major, minor, (unsigned long long)lba, sector_size, status_str);
}

static void log_scsi_host_status(uint32_t major, uint32_t minor, uint64_t lba,
				 int sector_size, uint32_t host_status, int may_fail)
{
	int level = may_fail ? LOG_DEBUG : LOG_ERR;
	const char *status_str;

	switch (host_status) {
	case DID_OK:
		return; /* No error */
	case DID_NO_CONNECT:
		status_str = "no connect";
		break;
	case DID_BUS_BUSY:
		status_str = "bus busy";
		break;
	case DID_TIME_OUT:
		status_str = "timeout";
		break;
	case DID_BAD_TARGET:
		status_str = "bad target";
		break;
	case DID_ABORT:
		status_str = "abort";
		break;
	case DID_ERROR:
		status_str = "internal error";
		break;
	case DID_RESET:
		status_str = "reset";
		break;
	case DID_TRANSPORT_DISRUPTED:
		status_str = "transport disrupted";
		break;
	case DID_TARGET_FAILURE:
		status_str = "target failure";
		break;
	case DID_NEXUS_FAILURE:
		status_str = "nexus failure";
		break;
	default:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu ss %d host_status 0x%02x",
			  major, minor, (unsigned long long)lba, sector_size, host_status);
		return;
	}

	log_level(0, 0, NULL, level,
		  "scsi_compare_and_write %u:%u lba %llu ss %d host %s",
		  major, minor, (unsigned long long)lba, sector_size, status_str);
}

static void log_scsi_driver_status(uint32_t major, uint32_t minor, uint64_t lba,
				   int sector_size, uint32_t driver_status, int may_fail)
{
	int level = may_fail ? LOG_DEBUG : LOG_ERR;
	const char *status_str;

	switch (driver_status) {
	case DRIVER_OK:
		return; /* No error */
	case DRIVER_BUSY:
		status_str = "busy";
		break;
	case DRIVER_SOFT:
		status_str = "soft error";
		break;
	case DRIVER_MEDIA:
		status_str = "media error";
		break;
	case DRIVER_ERROR:
		status_str = "error";
		break;
	case DRIVER_INVALID:
		status_str = "invalid";
		break;
	case DRIVER_TIMEOUT:
		status_str = "timeout";
		break;
	case DRIVER_SENSE:
		status_str = "sense";
		break;
	default:
		log_level(0, 0, NULL, level,
			  "scsi_compare_and_write %u:%u lba %llu ss %d driver_status 0x%02x",
			  major, minor, (unsigned long long)lba, sector_size, driver_status);
		return;
	}

	log_level(0, 0, NULL, level,
		  "scsi_compare_and_write %u:%u lba %llu ss %d driver %s",
		  major, minor, (unsigned long long)lba, sector_size, status_str);
}

int scsi_compare_and_write(struct task *task, int fd, int io_timeout, uint32_t major, uint32_t minor, int sector_size, uint64_t lba,
			   const char *both_data, const char *prev_data, const char *next_data, int may_fail)
{
	sg_io_hdr_t io_hdr;
	unsigned char sense_buf[32] = { 0 };
	unsigned char cdb[CAW_CDB_SIZE];
	unsigned char **p_caw_data;
	unsigned char *caw_data = (unsigned char *)both_data;
	unsigned short num_blocks = 1; /* number of blocks to compare/write */
	uint32_t scsi_status, host_status, driver_status;
	unsigned char sense_key;
	unsigned char asc = 0, ascq = 0;
	int sense_len;
	int ret = -1;
	int rv;

	if (!both_data) {
		p_caw_data = &caw_data;
		rv = posix_memalign((void *)p_caw_data, getpagesize(), 2 * sector_size);
		if (rv)
			return -ENOMEM;
		/* first sector is prev data, second sector is next data */
		memcpy(caw_data, prev_data, sector_size);
		memcpy(caw_data + sector_size, next_data, sector_size);
	}
	
	if (!io_timeout) {
		log_warn("scsi_compare_and_write zero io_timeout using 1");
		io_timeout = 1;
	}

	memset(cdb, 0, sizeof(cdb));

	cdb[0] = 0x89; /* COMPARE AND WRITE */
	if (com.caw_dpo)
		cdb[1] |= FLAG_DPO;
	if (com.caw_fua)
		cdb[1] |= FLAG_FUA;

	/* LBA (Logical Block Address) is a 64-bit value, stored in bytes 2-9 (Big Endian) */
	cdb[2] = (unsigned char)((lba >> 56) & 0xFF);
	cdb[3] = (unsigned char)((lba >> 48) & 0xFF);
	cdb[4] = (unsigned char)((lba >> 40) & 0xFF);
	cdb[5] = (unsigned char)((lba >> 32) & 0xFF);
	cdb[6] = (unsigned char)((lba >> 24) & 0xFF);
	cdb[7] = (unsigned char)((lba >> 16) & 0xFF);
	cdb[8] = (unsigned char)((lba >> 8) & 0xFF);
	cdb[9] = (unsigned char)(lba & 0xFF);

	cdb[13] = (uint8_t)(num_blocks & 0xFF);

	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
	io_hdr.interface_id = 'S';
	io_hdr.cmd_len = sizeof(cdb);
	io_hdr.mx_sb_len = sizeof(sense_buf);
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.dxfer_len = (2 * sector_size);
	io_hdr.dxferp = caw_data;
	io_hdr.cmdp = cdb;
	io_hdr.sbp = sense_buf;
	io_hdr.timeout = io_timeout * 1000;

	if (ioctl(fd, SG_IO, &io_hdr) < 0) {
		log_debug("scsi_compare_and_write %u:%u lba %llu ss %d ioctl errno %d",
			  major, minor, (unsigned long long)lba, sector_size, errno);
		if (!both_data)
			free(caw_data);
		return SANLK_CAW_IOCTL;
	}

	scsi_status   = io_hdr.status & 0x7e;
	host_status   = io_hdr.host_status;
	driver_status = io_hdr.driver_status & 0x0f;
	sense_len     = io_hdr.sb_len_wr;

	/* success */
	if (!scsi_status && !host_status && !driver_status) {
		ret = 0;
		goto out;
	}

	/* generic error, may be replaced with specific error */
	ret = SANLK_CAW_ERROR;

	if (scsi_status && (scsi_status != 0x02))
		log_scsi_status(major, minor, lba, sector_size, scsi_status, may_fail);

	if (scsi_status && (scsi_status == 0x02) && (sense_len < 1))
		log_error("scsi_compare_and_write check condition without sense data");

	if (scsi_status && (scsi_status == 0x02) && (sense_len > 1)) {
		/* check condition (0x02) with sense data */
		unsigned char response_code;
		int info_valid = 0;
		uint64_t info = 0;

		response_code = sense_buf[0] & 0x7f;

		if (response_code == 0x70 || response_code == 0x71) {
			/* Fixed format sense data */
			if (sense_len < 14) {
				log_error("scsi_compare_and_write sense buffer too short: %d", sense_len);
				goto other_status;
			}
			sense_key = sense_buf[2] & 0x0f;
			asc = sense_buf[12];
			ascq = sense_buf[13];

			/* Extract INFORMATION field if valid bit is set */
			if (sense_buf[0] & 0x80) {
				info_valid = 1;
				info = ((uint64_t)sense_buf[3] << 24) |
					((uint64_t)sense_buf[4] << 16) |
					((uint64_t)sense_buf[5] << 8) |
					(uint64_t)sense_buf[6];
			}
		} else if (response_code == 0x72 || response_code == 0x73) {
			if (sense_len < 8) {
				log_error("scsi_compare_and_write descriptor sense buffer too short: %d", sense_len);
				goto other_status;
			}
			sense_key = sense_buf[1] & 0x0f;
			asc = sense_buf[2];
			ascq = sense_buf[3];
		} else {
			log_error("scsi_compare_and_write unknown sense format 0x%02x", response_code);
			goto other_status;
		}

		ret = check_scsi_sense_key(task, major, minor, lba, sector_size,
					   sense_key, asc, ascq, info, info_valid, may_fail);
		if (ret == SANLK_CAW_MISCOMPARE) {
			task->miscompares++;
			goto out;
		}
	}

 other_status:
	if (host_status)
		log_scsi_host_status(major, minor, lba, sector_size, host_status, may_fail);

	if (driver_status)
		log_scsi_driver_status(major, minor, lba, sector_size, driver_status, may_fail);

 out:
	if (!both_data)
		free(caw_data);
	return ret;
}

int compare_and_write(struct task *task, int fd, int io_timeout,
		      uint32_t major, uint32_t minor, int sector_size, uint64_t offset_bytes,
		      const char *both_data, const char *prev_data, const char *next_data)
{
	if (!fd || !major || !sector_size || !io_timeout) {
		log_error("compare_and_write invalid args %d %u %u %d %d", fd, major, minor, sector_size, io_timeout);
		return -1;
	}

	/* scsi compare-and-write units are sector size of the LUN */
	return scsi_compare_and_write(task, fd, io_timeout, major, minor, sector_size, offset_bytes / sector_size,
				      both_data, prev_data, next_data, 0);
}

/*
 * test verification levels:
 * 1. read sector, compare-and-write same data back
 * 2. read sector, compare-and-write new data back
 * 3. read sector, compare-and-write new data back,
 *    reread sector, verify new data is read
 * 4. read sector, compare-and-write new data back,
 *    reread sector, verify new data is read,
 *    compare-and-write with modified prev_data,
 *    verify miscompare error
 * 5. read sector, compare-and-write new data back,
 *    reread sector, verify new data is read,
 *    compare-and-write with modified prev_data,
 *    verify miscompare error,
 *    read same data through LV, verify correct
 */

int compare_and_write_test(struct task *task, char *disk_path, uint64_t disk_offset_bytes, int sector_size, int test_verify)
{
	char test_path[PATH_MAX] = { 0 };
	char **p_prev_data = NULL;
	char **p_next_data = NULL;
	char *prev_data = NULL;
	char *next_data = NULL;
	struct stat st;
	uint32_t disk_major, disk_minor;
	uint32_t test_major, test_minor;
	uint64_t disk_offset_sect512;
	uint64_t disk_offset_sectors;
	uint64_t test_offset_sect512;
	uint64_t test_offset_sectors;
	uint64_t test_offset_bytes;
	ssize_t num;
	int disk_fd = -1;
	int test_fd;
	int is_lv;
	int err;
	int ret = -1;
	int rv;

	if (!test_verify)
		return 0;

	if (stat(disk_path, &st) < 0) {
		log_debug("compare_and_write_test %s disk stat error %d", disk_path, errno);
		return -1;
	}

	/*
	 * offset units:
	 * sanlock uses units of bytes,
	 * libdm uses units of 512 bytes,
	 * sg_io uses units of disk sector size
	 */

	disk_major = (uint32_t)major(st.st_rdev);
	disk_minor = (uint32_t)minor(st.st_rdev);
	disk_offset_sect512 = disk_offset_bytes / 512;
	disk_offset_sectors = disk_offset_bytes / sector_size; /* sector_size is either 512 or 4096 */

	/*
	 * If "disk" is an LV, then it is translated to "test"
	 * which is the PV used by the LV at the given offset.
	 * SG_IO must be submitted to the PV.
	 */

	if ((is_lv = dev_is_lv(disk_major, disk_minor))) {
		err = lv_to_target(disk_major, disk_minor, disk_offset_sect512, &test_major, &test_minor, &test_offset_sect512);
		if (err < 0) {
			log_debug("compare_and_write_test %s %u:%u lv_to_target error %d",
				  disk_path, disk_major, disk_minor, err);
			return -1;
		}
		test_offset_bytes = test_offset_sect512 * 512;
		test_offset_sectors = test_offset_bytes / sector_size;

		if (!dev_is_scsi(test_major, test_minor) &&
		    !dev_is_mpath(test_major, test_minor)) {
			log_debug("compare_and_write_test %s %u:%u off %llu test %u:%u off %llu unsupported dev type", disk_path,
				  disk_major, disk_minor, (unsigned long long)disk_offset_bytes,
				  test_major, test_minor, (unsigned long long)test_offset_bytes);
			return -1;
		}

		/* get path of device with test_major:test_minor */
		if (devno_to_path(test_major, test_minor, test_path) < 0) {
			log_debug("compare_and_write_test %s %u:%u devno_to_path %u:%u error",
				  disk_path, disk_major, disk_minor, test_major, test_minor);
			return -1;
		}
	} else {
		test_major = disk_major;
		test_minor = disk_minor;
		test_offset_bytes = disk_offset_bytes;
		test_offset_sect512 = disk_offset_sect512;
		test_offset_sectors = disk_offset_sectors;
		strncpy(test_path, disk_path, PATH_MAX-1);
	}

	/*
	log_debug("compare_and_write_test disk %s %u:%u off %llu %llu %llu test %s %u:%u off %llu %llu %llu",
		  disk_path, disk_major, disk_minor,
		  (unsigned long long)disk_offset_bytes,
		  (unsigned long long)disk_offset_sect512,
		  (unsigned long long)disk_offset_sectors,
		  test_path, test_major, test_minor,
		  (unsigned long long)test_offset_bytes,
		  (unsigned long long)test_offset_sect512,
		  (unsigned long long)test_offset_sectors);
	*/

	if ((test_fd = open(test_path, O_RDWR | O_DIRECT | O_SYNC, 0)) < 0) {
		log_debug("compare_and_write_test %s %u:%u %u:%u open error %d test_path %s",
			  disk_path, disk_major, disk_minor, test_major, test_minor, errno, test_path);
		return -1;
	}

	p_prev_data = &prev_data;
	rv = posix_memalign((void *)p_prev_data, getpagesize(), sector_size);
	if (rv)
		goto out;
	p_next_data = &next_data;
	rv = posix_memalign((void *)p_next_data, getpagesize(), sector_size);
	if (rv)
		goto out;

	num = pread(test_fd, prev_data, sector_size, test_offset_bytes);
	if (num != sector_size) {
		log_debug("compare_and_write_test %s %u:%u %s %u:%u %d pread error %d %zd test_offset_bytes %llu",
			  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
			  errno, num, (unsigned long long)test_offset_bytes);
		goto out;
	}

	memset(next_data, 0, sector_size);

	if (test_verify == 1) {
		/* write the same data back */
		memcpy(next_data, prev_data, sector_size);
	} else {
		struct utsname name = { 0 };
		uname(&name);
		/* write some random data to disk, making it self identifying to some degree in
		   case there's some need to diagnose where the data came from if it goes wrong. */
		snprintf(next_data, 511, "sanlock_test_compare_and_write_from_host_%s_at_%llu_to_%s_%llu_%s_%llu",
			name.nodename,
			(unsigned long long)time(NULL),
			disk_path, (unsigned long long)disk_offset_bytes,
			test_path, (unsigned long long)test_offset_bytes);
	}

	err = scsi_compare_and_write(task, test_fd, 1, test_major, test_minor, sector_size, test_offset_sectors, NULL, prev_data, next_data, 1);
	if (err) {
		log_debug("compare_and_write_test %s %u:%u %s %u:%u %d scsi_caw error %d test_offset_sectors %llu",
			  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
			  err, (unsigned long long)test_offset_sectors);
		ret = err;
		goto out;
	}

	if (test_verify > 2) {
		num = pread(test_fd, prev_data, sector_size, test_offset_bytes);
		if (num != sector_size) {
			log_debug("compare_and_write_test %s %u:%u %s %u:%u %d pread2 error %d %zd test_offset_bytes %llu",
				  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
				  errno, num, (unsigned long long)test_offset_bytes);
			goto out;
		}
		if (memcmp(prev_data, next_data, sector_size)) {
			log_debug("compare_and_write_test %s %u:%u %s %u:%u %d memcmp error test_offset_bytes %llu",
				  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
				  (unsigned long long)test_offset_bytes);
			goto out;
		}
	}
	if (test_verify > 3) {
		prev_data[0] = 'X'; /* unmatching prev_data should cause failure to write */
		next_data[1] = 'Y'; /* slightly modify new data, this shouldn't be written */

		err = scsi_compare_and_write(task, test_fd, 1, test_major, test_minor, sector_size, test_offset_sectors, NULL, prev_data, next_data, 1);
		if (err != SANLK_CAW_MISCOMPARE) {
			/* compare-and-write failed to report expected error */
			log_debug("compare_and_write_test %s %u:%u %s %u:%u %d scsi_caw result %d test_offset_sectors %llu",
				  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
				  err, (unsigned long long)test_offset_sectors);
			goto out;
		}
	}

	if ((test_verify > 4) && is_lv) {
		if ((disk_fd = open(disk_path, O_RDWR | O_DIRECT | O_SYNC, 0)) < 0) {
			log_debug("compare_and_write_test %s %u:%u open disk error %d",
				  disk_path, disk_major, disk_minor, errno);
			disk_fd = -1;
			goto out;
		}
		memset(prev_data, 0, sector_size);
		next_data[1] = 'a'; /* correct data from 'Y' above */

		num = pread(disk_fd, prev_data, sector_size, disk_offset_bytes);
		if (num != sector_size) {
			log_debug("compare_and_write_test %s %u:%u %s %u:%u %d pread3 error %d %zd disk_offset_bytes %llu",
				  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
				  errno, num, (unsigned long long)disk_offset_bytes);
			goto out;
		}
		if (memcmp(prev_data, next_data, sector_size)) {
			/* TODO: write the original data back to test_fd? */
			log_debug("compare_and_write_test %s %u:%u %s %u:%u %d memcmp2 error disk_offset_bytes %llu",
				  disk_path, disk_major, disk_minor, test_path, test_major, test_minor, sector_size,
				  (unsigned long long)disk_offset_bytes);
			goto out;
		}
	}

	ret = 0;
out:
	if (next_data)
		free(next_data);
	if (prev_data)
		free(prev_data);
	if (disk_fd != -1)
		close(disk_fd);
	close(test_fd);

	return ret;
}

