add virtio-scsi driver

virtio-scsi is a simple HBA that talks to the host via a single
vring.  The implementation looks like a hybrid of usb-msc and
virtio-blk.

Reviewed-by: Laszlo Ersek <lersek@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
diff --git a/Makefile b/Makefile
index 00da02c..abbac8c 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@
     kbd.c pci.c serial.c clock.c pic.c cdrom.c ps2port.c smp.c resume.c \
     pnpbios.c pirtable.c vgahooks.c ramdisk.c pcibios.c blockcmd.c \
     usb.c usb-uhci.c usb-ohci.c usb-ehci.c usb-hid.c usb-msc.c \
-    virtio-ring.c virtio-pci.c virtio-blk.c apm.c ahci.c
+    virtio-ring.c virtio-pci.c virtio-blk.c virtio-scsi.c apm.c ahci.c
 SRC16=$(SRCBOTH) system.c disk.c font.c
 SRC32FLAT=$(SRCBOTH) post.c shadow.c memmap.c coreboot.c boot.c \
     acpi.c smm.c mptable.c smbios.c pciinit.c optionroms.c mtrr.c \
diff --git a/src/Kconfig b/src/Kconfig
index 250663a..53fda9b 100644
--- a/src/Kconfig
+++ b/src/Kconfig
@@ -113,6 +113,12 @@
         default y
         help
             Support boot from virtio-blk storage.
+    config VIRTIO_SCSI
+        depends on DRIVES && !COREBOOT
+        bool "virtio-scsi controllers"
+        default y
+        help
+            Support boot from virtio-scsi storage.
     config FLOPPY
         depends on DRIVES
         bool "Floppy controller"
diff --git a/src/block.c b/src/block.c
index ccf4ee6..e270cf5 100644
--- a/src/block.c
+++ b/src/block.c
@@ -279,7 +279,7 @@
 static int
 process_scsi_op(struct disk_op_s *op)
 {
-    if (!CONFIG_USB_MSC)
+    if (!CONFIG_VIRTIO_SCSI && !CONFIG_USB_MSC)
         return 0;
     switch (op->command) {
     case CMD_READ:
@@ -320,6 +320,7 @@
     case DTYPE_AHCI:
 	return process_ahci_op(op);
     case DTYPE_USB:
+    case DTYPE_VIRTIO_SCSI:
         return process_scsi_op(op);
     default:
         op->count = 0;
diff --git a/src/blockcmd.c b/src/blockcmd.c
index d51e84e..8d881a5 100644
--- a/src/blockcmd.c
+++ b/src/blockcmd.c
@@ -12,6 +12,7 @@
 #include "ata.h" // atapi_cmd_data
 #include "ahci.h" // atapi_cmd_data
 #include "usb-msc.h" // usb_cmd_data
+#include "virtio-scsi.h" // virtio_scsi_cmd_data
 #include "boot.h" // boot_add_hd
 
 // Route command to low-level handler.
@@ -26,6 +27,8 @@
         return usb_cmd_data(op, cdbcmd, blocksize);
     case DTYPE_AHCI:
         return ahci_cmd_data(op, cdbcmd, blocksize);
+    case DTYPE_VIRTIO_SCSI:
+        return virtio_scsi_cmd_data(op, cdbcmd, blocksize);
     default:
         op->count = 0;
         return DISK_RET_EPARAM;
@@ -80,7 +83,7 @@
 int
 scsi_init_drive(struct drive_s *drive, const char *s, int prio)
 {
-    if (!CONFIG_USB_MSC)
+    if (!CONFIG_USB_MSC && !CONFIG_VIRTIO_SCSI)
         return 0;
 
     struct disk_op_s dop;
diff --git a/src/boot.c b/src/boot.c
index e26dad1..c737ba4 100644
--- a/src/boot.c
+++ b/src/boot.c
@@ -128,6 +128,20 @@
     return find_prio(desc);
 }
 
+int bootprio_find_scsi_device(struct pci_device *pci, int target, int lun)
+{
+    if (!CONFIG_BOOTORDER)
+        return -1;
+    if (!pci)
+        // support only pci machine for now
+        return -1;
+    // Find scsi drive - for example: /pci@i0cf8/scsi@5/channel@0/disk@1,0
+    char desc[256], *p;
+    p = build_pci_path(desc, sizeof(desc), "*", pci);
+    snprintf(p, desc+sizeof(desc)-p, "/*@0/*@%d,%d", target, lun);
+    return find_prio(desc);
+}
+
 int bootprio_find_ata_device(struct pci_device *pci, int chanid, int slave)
 {
     if (!CONFIG_BOOTORDER)
diff --git a/src/boot.h b/src/boot.h
index d776aa1..686f04d 100644
--- a/src/boot.h
+++ b/src/boot.h
@@ -14,6 +14,7 @@
 void boot_prep(void);
 struct pci_device;
 int bootprio_find_pci_device(struct pci_device *pci);
+int bootprio_find_scsi_device(struct pci_device *pci, int target, int lun);
 int bootprio_find_ata_device(struct pci_device *pci, int chanid, int slave);
 int bootprio_find_fdc_device(struct pci_device *pci, int port, int fdid);
 int bootprio_find_pci_rom(struct pci_device *pci, int instance);
diff --git a/src/disk.c b/src/disk.c
index 29714d6..7a58af4 100644
--- a/src/disk.c
+++ b/src/disk.c
@@ -551,7 +551,8 @@
     SET_INT13DPT(regs, blksize, blksize);
 
     if (size < 30 ||
-        (type != DTYPE_ATA && type != DTYPE_ATAPI && type != DTYPE_VIRTIO_BLK)) {
+        (type != DTYPE_ATA && type != DTYPE_ATAPI &&
+         type != DTYPE_VIRTIO_BLK && type != DTYPE_VIRTIO_SCSI)) {
         disk_ret(regs, DISK_RET_SUCCESS);
         return;
     }
diff --git a/src/disk.h b/src/disk.h
index dd7c46a..d344399 100644
--- a/src/disk.h
+++ b/src/disk.h
@@ -207,6 +207,7 @@
 #define DTYPE_USB          0x06
 #define DTYPE_VIRTIO_BLK   0x07
 #define DTYPE_AHCI         0x08
+#define DTYPE_VIRTIO_SCSI  0x09
 
 #define MAXDESCSIZE 80
 
diff --git a/src/pci_ids.h b/src/pci_ids.h
index e1cded2..4b59585 100644
--- a/src/pci_ids.h
+++ b/src/pci_ids.h
@@ -2608,3 +2608,4 @@
 
 #define PCI_VENDOR_ID_REDHAT_QUMRANET	0x1af4
 #define PCI_DEVICE_ID_VIRTIO_BLK	0x1001
+#define PCI_DEVICE_ID_VIRTIO_SCSI	0x1004
diff --git a/src/post.c b/src/post.c
index bd0b530..bd11766 100644
--- a/src/post.c
+++ b/src/post.c
@@ -26,6 +26,7 @@
 #include "xen.h" // xen_probe_hvm_info
 #include "ps2port.h" // ps2port_setup
 #include "virtio-blk.h" // virtio_blk_setup
+#include "virtio-scsi.h" // virtio_scsi_setup
 
 
 /****************************************************************
@@ -189,6 +190,7 @@
     cbfs_payload_setup();
     ramdisk_setup();
     virtio_blk_setup();
+    virtio_scsi_setup();
 }
 
 // Begin the boot process by invoking an int0x19 in 16bit mode.
diff --git a/src/virtio-scsi.c b/src/virtio-scsi.c
new file mode 100644
index 0000000..3c59438
--- /dev/null
+++ b/src/virtio-scsi.c
@@ -0,0 +1,178 @@
+// Virtio SCSI boot support.
+//
+// Copyright (C) 2012 Red Hat Inc.
+//
+// Authors:
+//  Paolo Bonzini <pbonzini@redhat.com>
+//
+// This file may be distributed under the terms of the GNU LGPLv3 license.
+
+#include "util.h" // dprintf
+#include "pci.h" // foreachpci
+#include "config.h" // CONFIG_*
+#include "biosvar.h" // GET_GLOBAL
+#include "pci_ids.h" // PCI_DEVICE_ID_VIRTIO_BLK
+#include "pci_regs.h" // PCI_VENDOR_ID
+#include "boot.h" // bootprio_find_scsi_device
+#include "blockcmd.h" // scsi_init_drive
+#include "virtio-pci.h"
+#include "virtio-ring.h"
+#include "virtio-scsi.h"
+#include "disk.h"
+
+struct virtio_lun_s {
+    struct drive_s drive;
+    struct pci_device *pci;
+    struct vring_virtqueue *vq;
+    u16 ioaddr;
+    u16 target;
+    u16 lun;
+};
+
+static int
+virtio_scsi_cmd(u16 ioaddr, struct vring_virtqueue *vq, struct disk_op_s *op,
+                void *cdbcmd, u16 target, u16 lun, u32 len)
+{
+    struct virtio_scsi_req_cmd req;
+    struct virtio_scsi_resp_cmd resp;
+    struct vring_list sg[3];
+
+    memset(&req, 0, sizeof(req));
+    req.lun[0] = 1;
+    req.lun[1] = target;
+    req.lun[2] = (lun >> 8) | 0x40;
+    req.lun[3] = (lun & 0xff);
+    memcpy(req.cdb, cdbcmd, 16);
+
+    int datain = (req.cdb[0] != CDB_CMD_WRITE_10);
+    int data_idx = (datain ? 2 : 1);
+    int out_num = (datain ? 1 : 2);
+    int in_num = (len ? 3 : 2) - out_num;
+
+    sg[0].addr   = MAKE_FLATPTR(GET_SEG(SS), &req);
+    sg[0].length = sizeof(req);
+
+    sg[out_num].addr   = MAKE_FLATPTR(GET_SEG(SS), &resp);
+    sg[out_num].length = sizeof(resp);
+
+    sg[data_idx].addr   = op->buf_fl;
+    sg[data_idx].length = len;
+
+    /* Add to virtqueue and kick host */
+    vring_add_buf(vq, sg, out_num, in_num, 0, 0);
+    vring_kick(ioaddr, vq, 1);
+
+    /* Wait for reply */
+    while (!vring_more_used(vq))
+        usleep(5);
+
+    /* Reclaim virtqueue element */
+    vring_get_buf(vq, NULL);
+
+    /* Clear interrupt status register.  Avoid leaving interrupts stuck if
+     * VRING_AVAIL_F_NO_INTERRUPT was ignored and interrupts were raised.
+     */
+    vp_get_isr(ioaddr);
+
+    if (resp.response == VIRTIO_SCSI_S_OK && resp.status == 0) {
+        return DISK_RET_SUCCESS;
+    }
+    return DISK_RET_EBADTRACK;
+}
+
+int
+virtio_scsi_cmd_data(struct disk_op_s *op, void *cdbcmd, u16 blocksize)
+{
+    struct virtio_lun_s *vlun =
+        container_of(op->drive_g, struct virtio_lun_s, drive);
+
+    return virtio_scsi_cmd(GET_GLOBAL(vlun->ioaddr),
+                           GET_GLOBAL(vlun->vq), op, cdbcmd,
+                           GET_GLOBAL(vlun->target), GET_GLOBAL(vlun->lun),
+                           blocksize * op->count);
+}
+
+static int
+virtio_scsi_add_lun(struct pci_device *pci, u16 ioaddr,
+                    struct vring_virtqueue *vq, u16 target, u16 lun)
+{
+    struct virtio_lun_s *vlun = malloc_fseg(sizeof(*vlun));
+    if (!vlun) {
+        warn_noalloc();
+        return -1;
+    }
+    memset(vlun, 0, sizeof(*vlun));
+    vlun->drive.type = DTYPE_VIRTIO_SCSI;
+    vlun->drive.cntl_id = pci->bdf;
+    vlun->pci = pci;
+    vlun->ioaddr = ioaddr;
+    vlun->vq = vq;
+    vlun->target = target;
+    vlun->lun = lun;
+
+    int prio = bootprio_find_scsi_device(pci, target, lun);
+    int ret = scsi_init_drive(&vlun->drive, "virtio-scsi", prio);
+    if (ret)
+        goto fail;
+    return 0;
+
+fail:
+    free(vlun);
+    return -1;
+}
+
+static int
+virtio_scsi_scan_target(struct pci_device *pci, u16 ioaddr,
+                        struct vring_virtqueue *vq, u16 target)
+{
+    /* TODO: send REPORT LUNS.  For now, only LUN 0 is recognized.  */
+    int ret = virtio_scsi_add_lun(pci, ioaddr, vq, target, 0);
+    return ret < 0 ? 0 : 1;
+}
+
+static void
+init_virtio_scsi(struct pci_device *pci)
+{
+    u16 bdf = pci->bdf;
+    dprintf(1, "found virtio-scsi at %x:%x\n", pci_bdf_to_bus(bdf),
+            pci_bdf_to_dev(bdf));
+    struct vring_virtqueue *vq = NULL;
+    u16 ioaddr = vp_init_simple(bdf);
+    if (vp_find_vq(ioaddr, 2, &vq) < 0 ) {
+        dprintf(1, "fail to find vq for virtio-scsi %x:%x\n",
+                pci_bdf_to_bus(bdf), pci_bdf_to_dev(bdf));
+        goto fail;
+    }
+
+    int i, tot;
+    for (tot = 0, i = 0; i < 256; i++)
+        tot += virtio_scsi_scan_target(pci, ioaddr, vq, i);
+
+    if (!tot)
+        goto fail;
+
+    vp_set_status(ioaddr, VIRTIO_CONFIG_S_ACKNOWLEDGE |
+                  VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_DRIVER_OK);
+    return;
+
+fail:
+    free(vq);
+}
+
+void
+virtio_scsi_setup(void)
+{
+    ASSERT32FLAT();
+    if (! CONFIG_VIRTIO_SCSI || CONFIG_COREBOOT)
+        return;
+
+    dprintf(3, "init virtio-scsi\n");
+
+    struct pci_device *pci;
+    foreachpci(pci) {
+        if (pci->vendor != PCI_VENDOR_ID_REDHAT_QUMRANET
+            || pci->device != PCI_DEVICE_ID_VIRTIO_SCSI)
+            continue;
+        init_virtio_scsi(pci);
+    }
+}
diff --git a/src/virtio-scsi.h b/src/virtio-scsi.h
new file mode 100644
index 0000000..bbfbf30
--- /dev/null
+++ b/src/virtio-scsi.h
@@ -0,0 +1,47 @@
+#ifndef _VIRTIO_SCSI_H
+#define _VIRTIO_SCSI_H
+
+#define VIRTIO_SCSI_CDB_SIZE      32
+#define VIRTIO_SCSI_SENSE_SIZE    96
+
+struct virtio_scsi_config
+{
+    u32 num_queues;
+    u32 seg_max;
+    u32 max_sectors;
+    u32 cmd_per_lun;
+    u32 event_info_size;
+    u32 sense_size;
+    u32 cdb_size;
+    u16 max_channel;
+    u16 max_target;
+    u32 max_lun;
+} __attribute__((packed));
+
+/* This is the first element of the "out" scatter-gather list. */
+struct virtio_scsi_req_cmd {
+    u8 lun[8];
+    u64 id;
+    u8 task_attr;
+    u8 prio;
+    u8 crn;
+    char cdb[VIRTIO_SCSI_CDB_SIZE];
+};
+
+/* This is the first element of the "in" scatter-gather list. */
+struct virtio_scsi_resp_cmd {
+    u32 sense_len;
+    u32 residual;
+    u16 status_qualifier;
+    u8 status;
+    u8 response;
+    u8 sense[VIRTIO_SCSI_SENSE_SIZE];
+};
+
+#define VIRTIO_SCSI_S_OK            0
+
+struct disk_op_s;
+int virtio_scsi_cmd_data(struct disk_op_s *op, void *cdbcmd, u16 blocksize);
+void virtio_scsi_setup(void);
+
+#endif /* _VIRTIO_SCSI_H */