root/usr/src/uts/common/io/scsi/adapters/smrt/smrt.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright (c) 2017, Joyent, Inc.
 */

#include <sys/scsi/adapters/smrt/smrt.h>

static int smrt_attach(dev_info_t *, ddi_attach_cmd_t);
static int smrt_detach(dev_info_t *, ddi_detach_cmd_t);
static int smrt_ioctl(dev_t, int, intptr_t, int, cred_t *, int *);
static void smrt_cleanup(smrt_t *);
static int smrt_command_comparator(const void *, const void *);

/*
 * Controller soft state.  Each entry is an object of type "smrt_t".
 */
void *smrt_state;

/*
 * DMA attributes template.  Each controller will make a copy of this template
 * with appropriate customisations; e.g., the Scatter/Gather List Length.
 */
static ddi_dma_attr_t smrt_dma_attr_template = {
        .dma_attr_version =             DMA_ATTR_V0,
        .dma_attr_addr_lo =             0x0000000000000000,
        .dma_attr_addr_hi =             0xFFFFFFFFFFFFFFFF,
        .dma_attr_count_max =           0x00FFFFFF,
        .dma_attr_align =               0x20,
        .dma_attr_burstsizes =          0x20,
        .dma_attr_minxfer =             DMA_UNIT_8,
        .dma_attr_maxxfer =             0xFFFFFFFF,
        /*
         * There is some suggestion that at least some, possibly older, Smart
         * Array controllers cannot tolerate a DMA segment that straddles a 4GB
         * boundary.
         */
        .dma_attr_seg =                 0xFFFFFFFF,
        .dma_attr_sgllen =              1,
        .dma_attr_granular =            512,
        .dma_attr_flags =               0
};

/*
 * Device memory access attributes for device control registers.
 */
ddi_device_acc_attr_t smrt_dev_attributes = {
        .devacc_attr_version =          DDI_DEVICE_ATTR_V0,
        .devacc_attr_endian_flags =     DDI_STRUCTURE_LE_ACC,
        .devacc_attr_dataorder =        DDI_STRICTORDER_ACC,
        .devacc_attr_access =           0
};

/*
 * Character/Block Operations Structure
 */
static struct cb_ops smrt_cb_ops = {
        .cb_rev =                       CB_REV,
        .cb_flag =                      D_NEW | D_MP,

        .cb_open =                      scsi_hba_open,
        .cb_close =                     scsi_hba_close,

        .cb_ioctl =                     smrt_ioctl,

        .cb_strategy =                  nodev,
        .cb_print =                     nodev,
        .cb_dump =                      nodev,
        .cb_read =                      nodev,
        .cb_write =                     nodev,
        .cb_devmap =                    nodev,
        .cb_mmap =                      nodev,
        .cb_segmap =                    nodev,
        .cb_chpoll =                    nochpoll,
        .cb_prop_op =                   ddi_prop_op,
        .cb_str =                       NULL,
        .cb_aread =                     nodev,
        .cb_awrite =                    nodev
};

/*
 * Device Operations Structure
 */
static struct dev_ops smrt_dev_ops = {
        .devo_rev =                     DEVO_REV,
        .devo_refcnt =                  0,

        .devo_attach =                  smrt_attach,
        .devo_detach =                  smrt_detach,

        .devo_cb_ops =                  &smrt_cb_ops,

        .devo_getinfo =                 nodev,
        .devo_identify =                nulldev,
        .devo_probe =                   nulldev,
        .devo_reset =                   nodev,
        .devo_bus_ops =                 NULL,
        .devo_power =                   nodev,
        .devo_quiesce =                 nodev
};

/*
 * Linkage structures
 */
static struct modldrv smrt_modldrv = {
        .drv_modops =                   &mod_driverops,
        .drv_linkinfo =                 "HP Smart Array",
        .drv_dev_ops =                  &smrt_dev_ops
};

static struct modlinkage smrt_modlinkage = {
        .ml_rev =                       MODREV_1,
        .ml_linkage =                   { &smrt_modldrv, NULL }
};


int
_init()
{
        int r;

        VERIFY0(ddi_soft_state_init(&smrt_state, sizeof (smrt_t), 0));

        if ((r = scsi_hba_init(&smrt_modlinkage)) != 0) {
                goto fail;
        }

        if ((r = mod_install(&smrt_modlinkage)) != 0) {
                scsi_hba_fini(&smrt_modlinkage);
                goto fail;
        }

        return (r);

fail:
        ddi_soft_state_fini(&smrt_state);
        return (r);
}

int
_fini()
{
        int r;

        if ((r = mod_remove(&smrt_modlinkage)) == 0) {
                scsi_hba_fini(&smrt_modlinkage);
                ddi_soft_state_fini(&smrt_state);
        }

        return (r);
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&smrt_modlinkage, modinfop));
}

static int
smrt_iport_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        const char *addr;
        dev_info_t *pdip;
        int instance;
        smrt_t *smrt;

        if (cmd != DDI_ATTACH)
                return (DDI_FAILURE);

        /*
         * Note, we cannot get to our parent via the tran's tran_hba_private
         * member.  This pointer is reset to NULL when the scsi_hba_tran_t
         * structure is duplicated.
         */
        addr = scsi_hba_iport_unit_address(dip);
        VERIFY(addr != NULL);
        pdip = ddi_get_parent(dip);
        instance = ddi_get_instance(pdip);
        smrt = ddi_get_soft_state(smrt_state, instance);
        VERIFY(smrt != NULL);

        if (strcmp(addr, SMRT_IPORT_VIRT) == 0) {
                if (smrt_logvol_hba_setup(smrt, dip) != DDI_SUCCESS)
                        return (DDI_FAILURE);
                smrt->smrt_virt_iport = dip;
        } else if (strcmp(addr, SMRT_IPORT_PHYS) == 0) {
                if (smrt_phys_hba_setup(smrt, dip) != DDI_SUCCESS)
                        return (DDI_FAILURE);
                smrt->smrt_phys_iport = dip;
        } else {
                return (DDI_FAILURE);
        }

        return (DDI_SUCCESS);
}

static int
smrt_iport_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        const char *addr;
        scsi_hba_tran_t *tran;
        smrt_t *smrt;

        if (cmd != DDI_DETACH)
                return (DDI_FAILURE);

        tran = ddi_get_driver_private(dip);
        VERIFY(tran != NULL);
        smrt = tran->tran_hba_private;
        VERIFY(smrt != NULL);

        addr = scsi_hba_iport_unit_address(dip);
        VERIFY(addr != NULL);

        if (strcmp(addr, SMRT_IPORT_VIRT) == 0) {
                smrt_logvol_hba_teardown(smrt, dip);
                smrt->smrt_virt_iport = NULL;
        } else if (strcmp(addr, SMRT_IPORT_PHYS) == 0) {
                smrt_phys_hba_teardown(smrt, dip);
                smrt->smrt_phys_iport = NULL;
        } else {
                return (DDI_FAILURE);
        }

        return (DDI_SUCCESS);
}

static int
smrt_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        uint32_t instance;
        smrt_t *smrt;
        boolean_t check_for_interrupts = B_FALSE;
        int r;
        char taskq_name[64];

        if (scsi_hba_iport_unit_address(dip) != NULL)
                return (smrt_iport_attach(dip, cmd));

        if (cmd != DDI_ATTACH) {
                return (DDI_FAILURE);
        }

        /*
         * Allocate the per-controller soft state object and get
         * a pointer to it.
         */
        instance = ddi_get_instance(dip);
        if (ddi_soft_state_zalloc(smrt_state, instance) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "could not allocate soft state");
                return (DDI_FAILURE);
        }
        if ((smrt = ddi_get_soft_state(smrt_state, instance)) == NULL) {
                dev_err(dip, CE_WARN, "could not get soft state");
                ddi_soft_state_free(smrt_state, instance);
                return (DDI_FAILURE);
        }

        /*
         * Initialise per-controller state object.
         */
        smrt->smrt_dip = dip;
        smrt->smrt_instance = instance;
        smrt->smrt_next_tag = SMRT_MIN_TAG_NUMBER;
        list_create(&smrt->smrt_commands, sizeof (smrt_command_t),
            offsetof(smrt_command_t, smcm_link));
        list_create(&smrt->smrt_finishq, sizeof (smrt_command_t),
            offsetof(smrt_command_t, smcm_link_finish));
        list_create(&smrt->smrt_abortq, sizeof (smrt_command_t),
            offsetof(smrt_command_t, smcm_link_abort));
        list_create(&smrt->smrt_volumes, sizeof (smrt_volume_t),
            offsetof(smrt_volume_t, smlv_link));
        list_create(&smrt->smrt_physicals, sizeof (smrt_physical_t),
            offsetof(smrt_physical_t, smpt_link));
        list_create(&smrt->smrt_targets, sizeof (smrt_target_t),
            offsetof(smrt_target_t, smtg_link_ctlr));
        avl_create(&smrt->smrt_inflight, smrt_command_comparator,
            sizeof (smrt_command_t), offsetof(smrt_command_t,
            smcm_node));
        cv_init(&smrt->smrt_cv_finishq, NULL, CV_DRIVER, NULL);

        smrt->smrt_init_level |= SMRT_INITLEVEL_BASIC;

        /*
         * Perform basic device setup, including identifying the board, mapping
         * the I2O registers and the Configuration Table.
         */
        if (smrt_device_setup(smrt) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "device setup failed");
                goto fail;
        }

        /*
         * Select a Transport Method (e.g. Simple or Performant) and update
         * the Configuration Table.  This function also waits for the
         * controller to become ready.
         */
        if (smrt_ctlr_init(smrt) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "controller initialisation failed");
                goto fail;
        }

        /*
         * Each controller may have a different Scatter/Gather Element count.
         * Configure a per-controller set of DMA attributes with the
         * appropriate S/G size.
         */
        VERIFY(smrt->smrt_sg_cnt > 0);
        smrt->smrt_dma_attr = smrt_dma_attr_template;
        smrt->smrt_dma_attr.dma_attr_sgllen = smrt->smrt_sg_cnt;

        /*
         * Now that we have selected a Transport Method, we can configure
         * the appropriate interrupt handlers.
         */
        if (smrt_interrupts_setup(smrt) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "interrupt handler setup failed");
                goto fail;
        }

        /*
         * Now that we have the correct interrupt priority, we can initialise
         * the mutex.  This must be done before the interrupt handler is
         * enabled.
         */
        mutex_init(&smrt->smrt_mutex, NULL, MUTEX_DRIVER,
            DDI_INTR_PRI(smrt->smrt_interrupt_pri));
        smrt->smrt_init_level |= SMRT_INITLEVEL_MUTEX;

        /*
         * From this point forward, the controller is able to accept commands
         * and (at least by polling) return command submissions.  Setting this
         * flag allows the rest of the driver to interact with the device.
         */
        smrt->smrt_status |= SMRT_CTLR_STATUS_RUNNING;

        if (smrt_interrupts_enable(smrt) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "interrupt handler could not be enabled");
                goto fail;
        }

        if (smrt_ctrl_hba_setup(smrt) != DDI_SUCCESS) {
                dev_err(dip, CE_WARN, "SCSI framework setup failed");
                goto fail;
        }

        /*
         * Set the appropriate Interrupt Mask Register bits to start
         * command completion interrupts from the controller.
         */
        smrt_intr_set(smrt, B_TRUE);
        check_for_interrupts = B_TRUE;

        /*
         * Register the maintenance routine for periodic execution:
         */
        smrt->smrt_periodic = ddi_periodic_add(smrt_periodic, smrt,
            SMRT_PERIODIC_RATE * NANOSEC, DDI_IPL_0);
        smrt->smrt_init_level |= SMRT_INITLEVEL_PERIODIC;

        (void) snprintf(taskq_name, sizeof (taskq_name), "smrt_discover_%u",
            instance);
        smrt->smrt_discover_taskq = ddi_taskq_create(smrt->smrt_dip, taskq_name,
            1, TASKQ_DEFAULTPRI, 0);
        if (smrt->smrt_discover_taskq == NULL) {
                dev_err(dip, CE_WARN, "failed to create discovery task queue");
                goto fail;
        }
        smrt->smrt_init_level |= SMRT_INITLEVEL_TASKQ;

        if ((r = smrt_event_init(smrt)) != 0) {
                dev_err(dip, CE_WARN, "could not initialize event subsystem "
                    "(%d)", r);
                goto fail;
        }
        smrt->smrt_init_level |= SMRT_INITLEVEL_ASYNC_EVENT;

        if (scsi_hba_iport_register(dip, SMRT_IPORT_VIRT) != DDI_SUCCESS)
                goto fail;

        if (scsi_hba_iport_register(dip, SMRT_IPORT_PHYS) != DDI_SUCCESS)
                goto fail;

        /*
         * Announce the attachment of this controller.
         */
        ddi_report_dev(dip);

        return (DDI_SUCCESS);

fail:
        if (check_for_interrupts) {
                if (smrt->smrt_stats.smrts_claimed_interrupts == 0) {
                        dev_err(dip, CE_WARN, "controller did not interrupt "
                            "during attach");
                }
        }
        smrt_cleanup(smrt);
        return (DDI_FAILURE);
}

static int
smrt_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        scsi_hba_tran_t *tran = (scsi_hba_tran_t *)ddi_get_driver_private(dip);
        smrt_t *smrt = (smrt_t *)tran->tran_hba_private;

        if (scsi_hba_iport_unit_address(dip) != NULL)
                return (smrt_iport_detach(dip, cmd));

        if (cmd != DDI_DETACH) {
                return (DDI_FAILURE);
        }

        /*
         * First, check to make sure that all SCSI framework targets have
         * detached.
         */
        mutex_enter(&smrt->smrt_mutex);
        if (!list_is_empty(&smrt->smrt_targets)) {
                mutex_exit(&smrt->smrt_mutex);
                dev_err(smrt->smrt_dip, CE_WARN, "cannot detach; targets still "
                    "using HBA");
                return (DDI_FAILURE);
        }

        if (smrt->smrt_virt_iport != NULL || smrt->smrt_phys_iport != NULL) {
                mutex_exit(&smrt->smrt_mutex);
                dev_err(smrt->smrt_dip, CE_WARN, "cannot detach: iports still "
                    "attached");
                return (DDI_FAILURE);
        }

        /*
         * Prevent new targets from attaching now:
         */
        smrt->smrt_status |= SMRT_CTLR_STATUS_DETACHING;
        mutex_exit(&smrt->smrt_mutex);

        /*
         * Clean up all remaining resources.
         */
        smrt_cleanup(smrt);

        return (DDI_SUCCESS);
}

static int
smrt_ioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t *credp,
    int *rval)
{
        int inst = MINOR2INST(getminor(dev));
        int status;

        if (secpolicy_sys_config(credp, B_FALSE) != 0) {
                return (EPERM);
        }

        /*
         * Ensure that we have a soft state object for this instance.
         */
        if (ddi_get_soft_state(smrt_state, inst) == NULL) {
                return (ENXIO);
        }

        switch (cmd) {
        default:
                status = scsi_hba_ioctl(dev, cmd, arg, mode, credp, rval);
                break;
        }

        return (status);
}

static void
smrt_cleanup(smrt_t *smrt)
{
        if (smrt->smrt_init_level & SMRT_INITLEVEL_ASYNC_EVENT) {
                smrt_event_fini(smrt);
                smrt->smrt_init_level &= ~SMRT_INITLEVEL_ASYNC_EVENT;
        }

        smrt_interrupts_teardown(smrt);

        if (smrt->smrt_init_level & SMRT_INITLEVEL_TASKQ) {
                ddi_taskq_destroy(smrt->smrt_discover_taskq);
                smrt->smrt_discover_taskq = NULL;
                smrt->smrt_init_level &= ~SMRT_INITLEVEL_TASKQ;
        }

        if (smrt->smrt_init_level & SMRT_INITLEVEL_PERIODIC) {
                ddi_periodic_delete(smrt->smrt_periodic);
                smrt->smrt_init_level &= ~SMRT_INITLEVEL_PERIODIC;
        }

        smrt_ctrl_hba_teardown(smrt);

        smrt_ctlr_teardown(smrt);

        smrt_device_teardown(smrt);

        if (smrt->smrt_init_level & SMRT_INITLEVEL_BASIC) {
                smrt_logvol_teardown(smrt);
                smrt_phys_teardown(smrt);

                cv_destroy(&smrt->smrt_cv_finishq);

                VERIFY(list_is_empty(&smrt->smrt_commands));
                list_destroy(&smrt->smrt_commands);
                list_destroy(&smrt->smrt_finishq);
                list_destroy(&smrt->smrt_abortq);

                VERIFY(list_is_empty(&smrt->smrt_volumes));
                list_destroy(&smrt->smrt_volumes);

                VERIFY(list_is_empty(&smrt->smrt_physicals));
                list_destroy(&smrt->smrt_physicals);

                VERIFY(list_is_empty(&smrt->smrt_targets));
                list_destroy(&smrt->smrt_targets);

                VERIFY(avl_is_empty(&smrt->smrt_inflight));
                avl_destroy(&smrt->smrt_inflight);

                smrt->smrt_init_level &= ~SMRT_INITLEVEL_BASIC;
        }

        if (smrt->smrt_init_level & SMRT_INITLEVEL_MUTEX) {
                mutex_destroy(&smrt->smrt_mutex);

                smrt->smrt_init_level &= ~SMRT_INITLEVEL_MUTEX;
        }

        VERIFY0(smrt->smrt_init_level);

        ddi_soft_state_free(smrt_state, ddi_get_instance(smrt->smrt_dip));
}

/*
 * Comparator for the "smrt_inflight" AVL tree in a "smrt_t".  This AVL tree
 * allows a tag ID to be mapped back to the relevant "smrt_command_t".
 */
static int
smrt_command_comparator(const void *lp, const void *rp)
{
        const smrt_command_t *l = lp;
        const smrt_command_t *r = rp;

        if (l->smcm_tag > r->smcm_tag) {
                return (1);
        } else if (l->smcm_tag < r->smcm_tag) {
                return (-1);
        } else {
                return (0);
        }
}