diff options
Diffstat (limited to 'hw/scsi-generic.c')
-rw-r--r-- | hw/scsi-generic.c | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/hw/scsi-generic.c b/hw/scsi-generic.c new file mode 100644 index 0000000000..9be1cca4c3 --- /dev/null +++ b/hw/scsi-generic.c @@ -0,0 +1,576 @@ +/* + * Generic SCSI Device support + * + * Copyright (c) 2007 Bull S.A.S. + * Based on code by Paul Brook + * Based on code by Fabrice Bellard + * + * Written by Laurent Vivier <Laurent.Vivier@bull.net> + * + * This code is licenced under the LGPL. + * + */ + +#include "qemu-common.h" +#include "qemu-error.h" +#include "scsi.h" +#include "blockdev.h" + +#ifdef __linux__ + +//#define DEBUG_SCSI + +#ifdef DEBUG_SCSI +#define DPRINTF(fmt, ...) \ +do { printf("scsi-generic: " fmt , ## __VA_ARGS__); } while (0) +#else +#define DPRINTF(fmt, ...) do {} while(0) +#endif + +#define BADF(fmt, ...) \ +do { fprintf(stderr, "scsi-generic: " fmt , ## __VA_ARGS__); } while (0) + +#include <stdio.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <scsi/sg.h> +#include "scsi-defs.h" + +#define SCSI_SENSE_BUF_SIZE 96 + +#define SG_ERR_DRIVER_TIMEOUT 0x06 +#define SG_ERR_DRIVER_SENSE 0x08 + +#ifndef MAX_UINT +#define MAX_UINT ((unsigned int)-1) +#endif + +typedef struct SCSIGenericState SCSIGenericState; + +typedef struct SCSIGenericReq { + SCSIRequest req; + uint8_t *buf; + int buflen; + int len; + sg_io_hdr_t io_header; +} SCSIGenericReq; + +struct SCSIGenericState +{ + SCSIDevice qdev; + BlockDriverState *bs; + int lun; + int driver_status; + uint8_t sensebuf[SCSI_SENSE_BUF_SIZE]; + uint8_t senselen; +}; + +static SCSIGenericReq *scsi_new_request(SCSIDevice *d, uint32_t tag, uint32_t lun) +{ + SCSIRequest *req; + + req = scsi_req_alloc(sizeof(SCSIGenericReq), d, tag, lun); + return DO_UPCAST(SCSIGenericReq, req, req); +} + +static void scsi_remove_request(SCSIGenericReq *r) +{ + qemu_free(r->buf); + scsi_req_free(&r->req); +} + +static SCSIGenericReq *scsi_find_request(SCSIGenericState *s, uint32_t tag) +{ + return DO_UPCAST(SCSIGenericReq, req, scsi_req_find(&s->qdev, tag)); +} + +/* Helper function for command completion. */ +static void scsi_command_complete(void *opaque, int ret) +{ + SCSIGenericReq *r = (SCSIGenericReq *)opaque; + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, r->req.dev); + + s->driver_status = r->io_header.driver_status; + if (s->driver_status & SG_ERR_DRIVER_SENSE) + s->senselen = r->io_header.sb_len_wr; + + if (ret != 0) + r->req.status = BUSY; + else { + if (s->driver_status & SG_ERR_DRIVER_TIMEOUT) { + r->req.status = BUSY; + BADF("Driver Timeout\n"); + } else if (r->io_header.status) + r->req.status = r->io_header.status; + else if (s->driver_status & SG_ERR_DRIVER_SENSE) + r->req.status = CHECK_CONDITION; + else + r->req.status = GOOD; + } + DPRINTF("Command complete 0x%p tag=0x%x status=%d\n", + r, r->req.tag, r->req.status); + + scsi_req_complete(&r->req); + scsi_remove_request(r); +} + +/* Cancel a pending data transfer. */ +static void scsi_cancel_io(SCSIDevice *d, uint32_t tag) +{ + DPRINTF("scsi_cancel_io 0x%x\n", tag); + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + SCSIGenericReq *r; + DPRINTF("Cancel tag=0x%x\n", tag); + r = scsi_find_request(s, tag); + if (r) { + if (r->req.aiocb) + bdrv_aio_cancel(r->req.aiocb); + r->req.aiocb = NULL; + scsi_remove_request(r); + } +} + +static int execute_command(BlockDriverState *bdrv, + SCSIGenericReq *r, int direction, + BlockDriverCompletionFunc *complete) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, r->req.dev); + + r->io_header.interface_id = 'S'; + r->io_header.dxfer_direction = direction; + r->io_header.dxferp = r->buf; + r->io_header.dxfer_len = r->buflen; + r->io_header.cmdp = r->req.cmd.buf; + r->io_header.cmd_len = r->req.cmd.len; + r->io_header.mx_sb_len = sizeof(s->sensebuf); + r->io_header.sbp = s->sensebuf; + r->io_header.timeout = MAX_UINT; + r->io_header.usr_ptr = r; + r->io_header.flags |= SG_FLAG_DIRECT_IO; + + r->req.aiocb = bdrv_aio_ioctl(bdrv, SG_IO, &r->io_header, complete, r); + if (r->req.aiocb == NULL) { + BADF("execute_command: read failed !\n"); + return -1; + } + + return 0; +} + +static void scsi_read_complete(void * opaque, int ret) +{ + SCSIGenericReq *r = (SCSIGenericReq *)opaque; + int len; + + if (ret) { + DPRINTF("IO error ret %d\n", ret); + scsi_command_complete(r, ret); + return; + } + len = r->io_header.dxfer_len - r->io_header.resid; + DPRINTF("Data ready tag=0x%x len=%d\n", r->req.tag, len); + + r->len = -1; + r->req.bus->complete(r->req.bus, SCSI_REASON_DATA, r->req.tag, len); + if (len == 0) + scsi_command_complete(r, 0); +} + +/* Read more data from scsi device into buffer. */ +static void scsi_read_data(SCSIDevice *d, uint32_t tag) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + SCSIGenericReq *r; + int ret; + + DPRINTF("scsi_read_data 0x%x\n", tag); + r = scsi_find_request(s, tag); + if (!r) { + BADF("Bad read tag 0x%x\n", tag); + /* ??? This is the wrong error. */ + scsi_command_complete(r, -EINVAL); + return; + } + + if (r->len == -1) { + scsi_command_complete(r, 0); + return; + } + + if (r->req.cmd.buf[0] == REQUEST_SENSE && s->driver_status & SG_ERR_DRIVER_SENSE) + { + s->senselen = MIN(r->len, s->senselen); + memcpy(r->buf, s->sensebuf, s->senselen); + r->io_header.driver_status = 0; + r->io_header.status = 0; + r->io_header.dxfer_len = s->senselen; + r->len = -1; + DPRINTF("Data ready tag=0x%x len=%d\n", r->req.tag, s->senselen); + DPRINTF("Sense: %d %d %d %d %d %d %d %d\n", + r->buf[0], r->buf[1], r->buf[2], r->buf[3], + r->buf[4], r->buf[5], r->buf[6], r->buf[7]); + r->req.bus->complete(r->req.bus, SCSI_REASON_DATA, r->req.tag, s->senselen); + return; + } + + ret = execute_command(s->bs, r, SG_DXFER_FROM_DEV, scsi_read_complete); + if (ret == -1) { + scsi_command_complete(r, -EINVAL); + return; + } +} + +static void scsi_write_complete(void * opaque, int ret) +{ + SCSIGenericReq *r = (SCSIGenericReq *)opaque; + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, r->req.dev); + + DPRINTF("scsi_write_complete() ret = %d\n", ret); + if (ret) { + DPRINTF("IO error\n"); + scsi_command_complete(r, ret); + return; + } + + if (r->req.cmd.buf[0] == MODE_SELECT && r->req.cmd.buf[4] == 12 && + s->qdev.type == TYPE_TAPE) { + s->qdev.blocksize = (r->buf[9] << 16) | (r->buf[10] << 8) | r->buf[11]; + DPRINTF("block size %d\n", s->qdev.blocksize); + } + + scsi_command_complete(r, ret); +} + +/* Write data to a scsi device. Returns nonzero on failure. + The transfer may complete asynchronously. */ +static int scsi_write_data(SCSIDevice *d, uint32_t tag) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + SCSIGenericReq *r; + int ret; + + DPRINTF("scsi_write_data 0x%x\n", tag); + r = scsi_find_request(s, tag); + if (!r) { + BADF("Bad write tag 0x%x\n", tag); + /* ??? This is the wrong error. */ + scsi_command_complete(r, -EINVAL); + return 0; + } + + if (r->len == 0) { + r->len = r->buflen; + r->req.bus->complete(r->req.bus, SCSI_REASON_DATA, r->req.tag, r->len); + return 0; + } + + ret = execute_command(s->bs, r, SG_DXFER_TO_DEV, scsi_write_complete); + if (ret == -1) { + scsi_command_complete(r, -EINVAL); + return 1; + } + + return 0; +} + +/* Return a pointer to the data buffer. */ +static uint8_t *scsi_get_buf(SCSIDevice *d, uint32_t tag) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + SCSIGenericReq *r; + r = scsi_find_request(s, tag); + if (!r) { + BADF("Bad buffer tag 0x%x\n", tag); + return NULL; + } + return r->buf; +} + +static void scsi_req_fixup(SCSIRequest *req) +{ + switch(req->cmd.buf[0]) { + case WRITE_10: + req->cmd.buf[1] &= ~0x08; /* disable FUA */ + break; + case READ_10: + req->cmd.buf[1] &= ~0x08; /* disable FUA */ + break; + case REWIND: + case START_STOP: + if (req->dev->type == TYPE_TAPE) { + /* force IMMED, otherwise qemu waits end of command */ + req->cmd.buf[1] = 0x01; + } + break; + } +} + +/* Execute a scsi command. Returns the length of the data expected by the + command. This will be Positive for data transfers from the device + (eg. disk reads), negative for transfers to the device (eg. disk writes), + and zero if the command does not transfer any data. */ + +static int32_t scsi_send_command(SCSIDevice *d, uint32_t tag, + uint8_t *cmd, int lun) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + SCSIGenericReq *r; + SCSIBus *bus; + int ret; + + if (cmd[0] != REQUEST_SENSE && + (lun != s->lun || (cmd[1] >> 5) != s->lun)) { + DPRINTF("Unimplemented LUN %d\n", lun ? lun : cmd[1] >> 5); + + s->sensebuf[0] = 0x70; + s->sensebuf[1] = 0x00; + s->sensebuf[2] = ILLEGAL_REQUEST; + s->sensebuf[3] = 0x00; + s->sensebuf[4] = 0x00; + s->sensebuf[5] = 0x00; + s->sensebuf[6] = 0x00; + s->senselen = 7; + s->driver_status = SG_ERR_DRIVER_SENSE; + bus = scsi_bus_from_device(d); + bus->complete(bus, SCSI_REASON_DONE, tag, CHECK_CONDITION); + return 0; + } + + r = scsi_find_request(s, tag); + if (r) { + BADF("Tag 0x%x already in use %p\n", tag, r); + scsi_cancel_io(d, tag); + } + r = scsi_new_request(d, tag, lun); + + if (-1 == scsi_req_parse(&r->req, cmd)) { + BADF("Unsupported command length, command %x\n", cmd[0]); + scsi_remove_request(r); + return 0; + } + scsi_req_fixup(&r->req); + + DPRINTF("Command: lun=%d tag=0x%x len %zd data=0x%02x", lun, tag, + r->req.cmd.xfer, cmd[0]); + +#ifdef DEBUG_SCSI + { + int i; + for (i = 1; i < r->req.cmd.len; i++) { + printf(" 0x%02x", cmd[i]); + } + printf("\n"); + } +#endif + + if (r->req.cmd.xfer == 0) { + if (r->buf != NULL) + qemu_free(r->buf); + r->buflen = 0; + r->buf = NULL; + ret = execute_command(s->bs, r, SG_DXFER_NONE, scsi_command_complete); + if (ret == -1) { + scsi_command_complete(r, -EINVAL); + return 0; + } + return 0; + } + + if (r->buflen != r->req.cmd.xfer) { + if (r->buf != NULL) + qemu_free(r->buf); + r->buf = qemu_malloc(r->req.cmd.xfer); + r->buflen = r->req.cmd.xfer; + } + + memset(r->buf, 0, r->buflen); + r->len = r->req.cmd.xfer; + if (r->req.cmd.mode == SCSI_XFER_TO_DEV) { + r->len = 0; + return -r->req.cmd.xfer; + } + + return r->req.cmd.xfer; +} + +static int get_blocksize(BlockDriverState *bdrv) +{ + uint8_t cmd[10]; + uint8_t buf[8]; + uint8_t sensebuf[8]; + sg_io_hdr_t io_header; + int ret; + + memset(cmd, 0, sizeof(cmd)); + memset(buf, 0, sizeof(buf)); + cmd[0] = READ_CAPACITY; + + memset(&io_header, 0, sizeof(io_header)); + io_header.interface_id = 'S'; + io_header.dxfer_direction = SG_DXFER_FROM_DEV; + io_header.dxfer_len = sizeof(buf); + io_header.dxferp = buf; + io_header.cmdp = cmd; + io_header.cmd_len = sizeof(cmd); + io_header.mx_sb_len = sizeof(sensebuf); + io_header.sbp = sensebuf; + io_header.timeout = 6000; /* XXX */ + + ret = bdrv_ioctl(bdrv, SG_IO, &io_header); + if (ret < 0) + return -1; + + return (buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | buf[7]; +} + +static int get_stream_blocksize(BlockDriverState *bdrv) +{ + uint8_t cmd[6]; + uint8_t buf[12]; + uint8_t sensebuf[8]; + sg_io_hdr_t io_header; + int ret; + + memset(cmd, 0, sizeof(cmd)); + memset(buf, 0, sizeof(buf)); + cmd[0] = MODE_SENSE; + cmd[4] = sizeof(buf); + + memset(&io_header, 0, sizeof(io_header)); + io_header.interface_id = 'S'; + io_header.dxfer_direction = SG_DXFER_FROM_DEV; + io_header.dxfer_len = sizeof(buf); + io_header.dxferp = buf; + io_header.cmdp = cmd; + io_header.cmd_len = sizeof(cmd); + io_header.mx_sb_len = sizeof(sensebuf); + io_header.sbp = sensebuf; + io_header.timeout = 6000; /* XXX */ + + ret = bdrv_ioctl(bdrv, SG_IO, &io_header); + if (ret < 0) + return -1; + + return (buf[9] << 16) | (buf[10] << 8) | buf[11]; +} + +static void scsi_generic_purge_requests(SCSIGenericState *s) +{ + SCSIGenericReq *r; + + while (!QTAILQ_EMPTY(&s->qdev.requests)) { + r = DO_UPCAST(SCSIGenericReq, req, QTAILQ_FIRST(&s->qdev.requests)); + if (r->req.aiocb) { + bdrv_aio_cancel(r->req.aiocb); + } + scsi_remove_request(r); + } +} + +static void scsi_generic_reset(DeviceState *dev) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev.qdev, dev); + + scsi_generic_purge_requests(s); +} + +static void scsi_destroy(SCSIDevice *d) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, d); + + scsi_generic_purge_requests(s); + blockdev_mark_auto_del(s->qdev.conf.bs); +} + +static int scsi_generic_initfn(SCSIDevice *dev) +{ + SCSIGenericState *s = DO_UPCAST(SCSIGenericState, qdev, dev); + int sg_version; + struct sg_scsi_id scsiid; + + if (!s->qdev.conf.bs) { + error_report("scsi-generic: drive property not set"); + return -1; + } + s->bs = s->qdev.conf.bs; + + /* check we are really using a /dev/sg* file */ + if (!bdrv_is_sg(s->bs)) { + error_report("scsi-generic: not /dev/sg*"); + return -1; + } + + if (bdrv_get_on_error(s->bs, 0) != BLOCK_ERR_STOP_ENOSPC) { + error_report("Device doesn't support drive option werror"); + return -1; + } + if (bdrv_get_on_error(s->bs, 1) != BLOCK_ERR_REPORT) { + error_report("Device doesn't support drive option rerror"); + return -1; + } + + /* check we are using a driver managing SG_IO (version 3 and after */ + if (bdrv_ioctl(s->bs, SG_GET_VERSION_NUM, &sg_version) < 0 || + sg_version < 30000) { + error_report("scsi-generic: scsi generic interface too old"); + return -1; + } + + /* get LUN of the /dev/sg? */ + if (bdrv_ioctl(s->bs, SG_GET_SCSI_ID, &scsiid)) { + error_report("scsi-generic: SG_GET_SCSI_ID ioctl failed"); + return -1; + } + + /* define device state */ + s->lun = scsiid.lun; + DPRINTF("LUN %d\n", s->lun); + s->qdev.type = scsiid.scsi_type; + DPRINTF("device type %d\n", s->qdev.type); + if (s->qdev.type == TYPE_TAPE) { + s->qdev.blocksize = get_stream_blocksize(s->bs); + if (s->qdev.blocksize == -1) + s->qdev.blocksize = 0; + } else { + s->qdev.blocksize = get_blocksize(s->bs); + /* removable media returns 0 if not present */ + if (s->qdev.blocksize <= 0) { + if (s->qdev.type == TYPE_ROM || s->qdev.type == TYPE_WORM) + s->qdev.blocksize = 2048; + else + s->qdev.blocksize = 512; + } + } + DPRINTF("block size %d\n", s->qdev.blocksize); + s->driver_status = 0; + memset(s->sensebuf, 0, sizeof(s->sensebuf)); + bdrv_set_removable(s->bs, 0); + return 0; +} + +static SCSIDeviceInfo scsi_generic_info = { + .qdev.name = "scsi-generic", + .qdev.desc = "pass through generic scsi device (/dev/sg*)", + .qdev.size = sizeof(SCSIGenericState), + .qdev.reset = scsi_generic_reset, + .init = scsi_generic_initfn, + .destroy = scsi_destroy, + .send_command = scsi_send_command, + .read_data = scsi_read_data, + .write_data = scsi_write_data, + .cancel_io = scsi_cancel_io, + .get_buf = scsi_get_buf, + .qdev.props = (Property[]) { + DEFINE_BLOCK_PROPERTIES(SCSIGenericState, qdev.conf), + DEFINE_PROP_END_OF_LIST(), + }, +}; + +static void scsi_generic_register_devices(void) +{ + scsi_qdev_register(&scsi_generic_info); +} +device_init(scsi_generic_register_devices) + +#endif /* __linux__ */ |