diff options
Diffstat (limited to 'drivers/scsi/libsas')
-rw-r--r-- | drivers/scsi/libsas/Kconfig | 34 | ||||
-rw-r--r-- | drivers/scsi/libsas/Makefile | 21 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_ata.c | 864 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_discover.c | 587 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_event.c | 218 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_expander.c | 2181 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_host_smp.c | 354 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_init.c | 673 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_internal.h | 195 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_phy.c | 162 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_port.c | 362 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_scsi_host.c | 970 | ||||
-rw-r--r-- | drivers/scsi/libsas/sas_task.c | 38 |
13 files changed, 6659 insertions, 0 deletions
diff --git a/drivers/scsi/libsas/Kconfig b/drivers/scsi/libsas/Kconfig new file mode 100644 index 000000000..052ee3a26 --- /dev/null +++ b/drivers/scsi/libsas/Kconfig @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Kernel configuration file for the SAS Class +# +# Copyright (C) 2005 Adaptec, Inc. All rights reserved. +# Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> +# + +config SCSI_SAS_LIBSAS + tristate "SAS Domain Transport Attributes" + depends on SCSI + select SCSI_SAS_ATTRS + select BLK_DEV_BSGLIB + help + This provides transport specific helpers for SAS drivers which + use the domain device construct (like the aic94xxx). + +config SCSI_SAS_ATA + bool "ATA support for libsas (requires libata)" + depends on SCSI_SAS_LIBSAS + depends on ATA = y || ATA = SCSI_SAS_LIBSAS + select SATA_HOST + help + Builds in ATA support into libsas. Will necessitate + the loading of libata along with libsas. + +config SCSI_SAS_HOST_SMP + bool "Support for SMP interpretation for SAS hosts" + default y + depends on SCSI_SAS_LIBSAS + help + Allows sas hosts to receive SMP frames. Selecting this + option builds an SMP interpreter into libsas. Say + N here if you want to save the few kb this consumes. diff --git a/drivers/scsi/libsas/Makefile b/drivers/scsi/libsas/Makefile new file mode 100644 index 000000000..e63a54f5a --- /dev/null +++ b/drivers/scsi/libsas/Makefile @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Kernel Makefile for the libsas helpers +# +# Copyright (C) 2005 Adaptec, Inc. All rights reserved. +# Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> +# + +obj-$(CONFIG_SCSI_SAS_LIBSAS) += libsas.o +libsas-y += sas_init.o \ + sas_phy.o \ + sas_port.o \ + sas_event.o \ + sas_discover.o \ + sas_expander.o \ + sas_scsi_host.o \ + sas_task.o +libsas-$(CONFIG_SCSI_SAS_ATA) += sas_ata.o +libsas-$(CONFIG_SCSI_SAS_HOST_SMP) += sas_host_smp.o + +ccflags-y := -DDEBUG diff --git a/drivers/scsi/libsas/sas_ata.c b/drivers/scsi/libsas/sas_ata.c new file mode 100644 index 000000000..f92b88936 --- /dev/null +++ b/drivers/scsi/libsas/sas_ata.c @@ -0,0 +1,864 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Support for SATA devices on Serial Attached SCSI (SAS) controllers + * + * Copyright (C) 2006 IBM Corporation + * + * Written by: Darrick J. Wong <djwong@us.ibm.com>, IBM Corporation + */ + +#include <linux/scatterlist.h> +#include <linux/slab.h> +#include <linux/async.h> +#include <linux/export.h> + +#include <scsi/sas_ata.h> +#include "sas_internal.h" +#include <scsi/scsi_host.h> +#include <scsi/scsi_device.h> +#include <scsi/scsi_tcq.h> +#include <scsi/scsi.h> +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include "../scsi_sas_internal.h" +#include "../scsi_transport_api.h" +#include <scsi/scsi_eh.h> + +static enum ata_completion_errors sas_to_ata_err(struct task_status_struct *ts) +{ + /* Cheesy attempt to translate SAS errors into ATA. Hah! */ + + /* transport error */ + if (ts->resp == SAS_TASK_UNDELIVERED) + return AC_ERR_ATA_BUS; + + /* ts->resp == SAS_TASK_COMPLETE */ + /* task delivered, what happened afterwards? */ + switch (ts->stat) { + case SAS_DEV_NO_RESPONSE: + return AC_ERR_TIMEOUT; + + case SAS_INTERRUPTED: + case SAS_PHY_DOWN: + case SAS_NAK_R_ERR: + return AC_ERR_ATA_BUS; + + + case SAS_DATA_UNDERRUN: + /* + * Some programs that use the taskfile interface + * (smartctl in particular) can cause underrun + * problems. Ignore these errors, perhaps at our + * peril. + */ + return 0; + + case SAS_DATA_OVERRUN: + case SAS_QUEUE_FULL: + case SAS_DEVICE_UNKNOWN: + case SAS_SG_ERR: + return AC_ERR_INVALID; + + case SAS_OPEN_TO: + case SAS_OPEN_REJECT: + pr_warn("%s: Saw error %d. What to do?\n", + __func__, ts->stat); + return AC_ERR_OTHER; + + case SAM_STAT_CHECK_CONDITION: + case SAS_ABORTED_TASK: + return AC_ERR_DEV; + + case SAS_PROTO_RESPONSE: + /* This means the ending_fis has the error + * value; return 0 here to collect it */ + return 0; + default: + return 0; + } +} + +static void sas_ata_task_done(struct sas_task *task) +{ + struct ata_queued_cmd *qc = task->uldd_task; + struct domain_device *dev = task->dev; + struct task_status_struct *stat = &task->task_status; + struct ata_task_resp *resp = (struct ata_task_resp *)stat->buf; + struct sas_ha_struct *sas_ha = dev->port->ha; + enum ata_completion_errors ac; + unsigned long flags; + struct ata_link *link; + struct ata_port *ap; + + spin_lock_irqsave(&dev->done_lock, flags); + if (test_bit(SAS_HA_FROZEN, &sas_ha->state)) + task = NULL; + else if (qc && qc->scsicmd) + ASSIGN_SAS_TASK(qc->scsicmd, NULL); + spin_unlock_irqrestore(&dev->done_lock, flags); + + /* check if libsas-eh got to the task before us */ + if (unlikely(!task)) + return; + + if (!qc) + goto qc_already_gone; + + ap = qc->ap; + link = &ap->link; + + spin_lock_irqsave(ap->lock, flags); + /* check if we lost the race with libata/sas_ata_post_internal() */ + if (unlikely(ap->pflags & ATA_PFLAG_FROZEN)) { + spin_unlock_irqrestore(ap->lock, flags); + if (qc->scsicmd) + goto qc_already_gone; + else { + /* if eh is not involved and the port is frozen then the + * ata internal abort process has taken responsibility + * for this sas_task + */ + return; + } + } + + if (stat->stat == SAS_PROTO_RESPONSE || + stat->stat == SAS_SAM_STAT_GOOD || + (stat->stat == SAS_SAM_STAT_CHECK_CONDITION && + dev->sata_dev.class == ATA_DEV_ATAPI)) { + memcpy(dev->sata_dev.fis, resp->ending_fis, ATA_RESP_FIS_SIZE); + + if (!link->sactive) { + qc->err_mask |= ac_err_mask(dev->sata_dev.fis[2]); + } else { + link->eh_info.err_mask |= ac_err_mask(dev->sata_dev.fis[2]); + if (unlikely(link->eh_info.err_mask)) + qc->flags |= ATA_QCFLAG_FAILED; + } + } else { + ac = sas_to_ata_err(stat); + if (ac) { + pr_warn("%s: SAS error 0x%x\n", __func__, stat->stat); + /* We saw a SAS error. Send a vague error. */ + if (!link->sactive) { + qc->err_mask = ac; + } else { + link->eh_info.err_mask |= AC_ERR_DEV; + qc->flags |= ATA_QCFLAG_FAILED; + } + + dev->sata_dev.fis[3] = 0x04; /* status err */ + dev->sata_dev.fis[2] = ATA_ERR; + } + } + + qc->lldd_task = NULL; + ata_qc_complete(qc); + spin_unlock_irqrestore(ap->lock, flags); + +qc_already_gone: + sas_free_task(task); +} + +static unsigned int sas_ata_qc_issue(struct ata_queued_cmd *qc) + __must_hold(ap->lock) +{ + struct sas_task *task; + struct scatterlist *sg; + int ret = AC_ERR_SYSTEM; + unsigned int si, xfer = 0; + struct ata_port *ap = qc->ap; + struct domain_device *dev = ap->private_data; + struct sas_ha_struct *sas_ha = dev->port->ha; + struct Scsi_Host *host = sas_ha->core.shost; + struct sas_internal *i = to_sas_internal(host->transportt); + + /* TODO: we should try to remove that unlock */ + spin_unlock(ap->lock); + + /* If the device fell off, no sense in issuing commands */ + if (test_bit(SAS_DEV_GONE, &dev->state)) + goto out; + + task = sas_alloc_task(GFP_ATOMIC); + if (!task) + goto out; + task->dev = dev; + task->task_proto = SAS_PROTOCOL_STP; + task->task_done = sas_ata_task_done; + + if (qc->tf.command == ATA_CMD_FPDMA_WRITE || + qc->tf.command == ATA_CMD_FPDMA_READ || + qc->tf.command == ATA_CMD_FPDMA_RECV || + qc->tf.command == ATA_CMD_FPDMA_SEND || + qc->tf.command == ATA_CMD_NCQ_NON_DATA) { + /* Need to zero out the tag libata assigned us */ + qc->tf.nsect = 0; + } + + ata_tf_to_fis(&qc->tf, qc->dev->link->pmp, 1, (u8 *)&task->ata_task.fis); + task->uldd_task = qc; + if (ata_is_atapi(qc->tf.protocol)) { + memcpy(task->ata_task.atapi_packet, qc->cdb, qc->dev->cdb_len); + task->total_xfer_len = qc->nbytes; + task->num_scatter = qc->n_elem; + task->data_dir = qc->dma_dir; + } else if (!ata_is_data(qc->tf.protocol)) { + task->data_dir = DMA_NONE; + } else { + for_each_sg(qc->sg, sg, qc->n_elem, si) + xfer += sg_dma_len(sg); + + task->total_xfer_len = xfer; + task->num_scatter = si; + task->data_dir = qc->dma_dir; + } + task->scatter = qc->sg; + task->ata_task.retry_count = 1; + task->task_state_flags = SAS_TASK_STATE_PENDING; + qc->lldd_task = task; + + task->ata_task.use_ncq = ata_is_ncq(qc->tf.protocol); + task->ata_task.dma_xfer = ata_is_dma(qc->tf.protocol); + + if (qc->scsicmd) + ASSIGN_SAS_TASK(qc->scsicmd, task); + + ret = i->dft->lldd_execute_task(task, GFP_ATOMIC); + if (ret) { + pr_debug("lldd_execute_task returned: %d\n", ret); + + if (qc->scsicmd) + ASSIGN_SAS_TASK(qc->scsicmd, NULL); + sas_free_task(task); + qc->lldd_task = NULL; + ret = AC_ERR_SYSTEM; + } + + out: + spin_lock(ap->lock); + return ret; +} + +static bool sas_ata_qc_fill_rtf(struct ata_queued_cmd *qc) +{ + struct domain_device *dev = qc->ap->private_data; + + ata_tf_from_fis(dev->sata_dev.fis, &qc->result_tf); + return true; +} + +static struct sas_internal *dev_to_sas_internal(struct domain_device *dev) +{ + return to_sas_internal(dev->port->ha->core.shost->transportt); +} + +static int sas_get_ata_command_set(struct domain_device *dev); + +int sas_get_ata_info(struct domain_device *dev, struct ex_phy *phy) +{ + if (phy->attached_tproto & SAS_PROTOCOL_STP) + dev->tproto = phy->attached_tproto; + if (phy->attached_sata_dev) + dev->tproto |= SAS_SATA_DEV; + + if (phy->attached_dev_type == SAS_SATA_PENDING) + dev->dev_type = SAS_SATA_PENDING; + else { + int res; + + dev->dev_type = SAS_SATA_DEV; + res = sas_get_report_phy_sata(dev->parent, phy->phy_id, + &dev->sata_dev.rps_resp); + if (res) { + pr_debug("report phy sata to %016llx:%02d returned 0x%x\n", + SAS_ADDR(dev->parent->sas_addr), + phy->phy_id, res); + return res; + } + memcpy(dev->frame_rcvd, &dev->sata_dev.rps_resp.rps.fis, + sizeof(struct dev_to_host_fis)); + dev->sata_dev.class = sas_get_ata_command_set(dev); + } + return 0; +} + +static int sas_ata_clear_pending(struct domain_device *dev, struct ex_phy *phy) +{ + int res; + + /* we weren't pending, so successfully end the reset sequence now */ + if (dev->dev_type != SAS_SATA_PENDING) + return 1; + + /* hmmm, if this succeeds do we need to repost the domain_device to the + * lldd so it can pick up new parameters? + */ + res = sas_get_ata_info(dev, phy); + if (res) + return 0; /* retry */ + else + return 1; +} + +static int smp_ata_check_ready(struct ata_link *link) +{ + int res; + struct ata_port *ap = link->ap; + struct domain_device *dev = ap->private_data; + struct domain_device *ex_dev = dev->parent; + struct sas_phy *phy = sas_get_local_phy(dev); + struct ex_phy *ex_phy = &ex_dev->ex_dev.ex_phy[phy->number]; + + res = sas_ex_phy_discover(ex_dev, phy->number); + sas_put_local_phy(phy); + + /* break the wait early if the expander is unreachable, + * otherwise keep polling + */ + if (res == -ECOMM) + return res; + if (res != SMP_RESP_FUNC_ACC) + return 0; + + switch (ex_phy->attached_dev_type) { + case SAS_SATA_PENDING: + return 0; + case SAS_END_DEVICE: + if (ex_phy->attached_sata_dev) + return sas_ata_clear_pending(dev, ex_phy); + fallthrough; + default: + return -ENODEV; + } +} + +static int local_ata_check_ready(struct ata_link *link) +{ + struct ata_port *ap = link->ap; + struct domain_device *dev = ap->private_data; + struct sas_internal *i = dev_to_sas_internal(dev); + + if (i->dft->lldd_ata_check_ready) + return i->dft->lldd_ata_check_ready(dev); + else { + /* lldd's that don't implement 'ready' checking get the + * old default behavior of not coordinating reset + * recovery with libata + */ + return 1; + } +} + +static int sas_ata_printk(const char *level, const struct domain_device *ddev, + const char *fmt, ...) +{ + struct ata_port *ap = ddev->sata_dev.ap; + struct device *dev = &ddev->rphy->dev; + struct va_format vaf; + va_list args; + int r; + + va_start(args, fmt); + + vaf.fmt = fmt; + vaf.va = &args; + + r = printk("%s" SAS_FMT "ata%u: %s: %pV", + level, ap->print_id, dev_name(dev), &vaf); + + va_end(args); + + return r; +} + +static int sas_ata_hard_reset(struct ata_link *link, unsigned int *class, + unsigned long deadline) +{ + int ret = 0, res; + struct sas_phy *phy; + struct ata_port *ap = link->ap; + int (*check_ready)(struct ata_link *link); + struct domain_device *dev = ap->private_data; + struct sas_internal *i = dev_to_sas_internal(dev); + + res = i->dft->lldd_I_T_nexus_reset(dev); + if (res == -ENODEV) + return res; + + if (res != TMF_RESP_FUNC_COMPLETE) + sas_ata_printk(KERN_DEBUG, dev, "Unable to reset ata device?\n"); + + phy = sas_get_local_phy(dev); + if (scsi_is_sas_phy_local(phy)) + check_ready = local_ata_check_ready; + else + check_ready = smp_ata_check_ready; + sas_put_local_phy(phy); + + ret = ata_wait_after_reset(link, deadline, check_ready); + if (ret && ret != -EAGAIN) + sas_ata_printk(KERN_ERR, dev, "reset failed (errno=%d)\n", ret); + + *class = dev->sata_dev.class; + + ap->cbl = ATA_CBL_SATA; + return ret; +} + +/* + * notify the lldd to forget the sas_task for this internal ata command + * that bypasses scsi-eh + */ +static void sas_ata_internal_abort(struct sas_task *task) +{ + struct sas_internal *si = dev_to_sas_internal(task->dev); + unsigned long flags; + int res; + + spin_lock_irqsave(&task->task_state_lock, flags); + if (task->task_state_flags & SAS_TASK_STATE_ABORTED || + task->task_state_flags & SAS_TASK_STATE_DONE) { + spin_unlock_irqrestore(&task->task_state_lock, flags); + pr_debug("%s: Task %p already finished.\n", __func__, task); + goto out; + } + task->task_state_flags |= SAS_TASK_STATE_ABORTED; + spin_unlock_irqrestore(&task->task_state_lock, flags); + + res = si->dft->lldd_abort_task(task); + + spin_lock_irqsave(&task->task_state_lock, flags); + if (task->task_state_flags & SAS_TASK_STATE_DONE || + res == TMF_RESP_FUNC_COMPLETE) { + spin_unlock_irqrestore(&task->task_state_lock, flags); + goto out; + } + + /* XXX we are not prepared to deal with ->lldd_abort_task() + * failures. TODO: lldds need to unconditionally forget about + * aborted ata tasks, otherwise we (likely) leak the sas task + * here + */ + pr_warn("%s: Task %p leaked.\n", __func__, task); + + if (!(task->task_state_flags & SAS_TASK_STATE_DONE)) + task->task_state_flags &= ~SAS_TASK_STATE_ABORTED; + spin_unlock_irqrestore(&task->task_state_lock, flags); + + return; + out: + sas_free_task(task); +} + +static void sas_ata_post_internal(struct ata_queued_cmd *qc) +{ + if (qc->flags & ATA_QCFLAG_FAILED) + qc->err_mask |= AC_ERR_OTHER; + + if (qc->err_mask) { + /* + * Find the sas_task and kill it. By this point, libata + * has decided to kill the qc and has frozen the port. + * In this state sas_ata_task_done() will no longer free + * the sas_task, so we need to notify the lldd (via + * ->lldd_abort_task) that the task is dead and free it + * ourselves. + */ + struct sas_task *task = qc->lldd_task; + + qc->lldd_task = NULL; + if (!task) + return; + task->uldd_task = NULL; + sas_ata_internal_abort(task); + } +} + + +static void sas_ata_set_dmamode(struct ata_port *ap, struct ata_device *ata_dev) +{ + struct domain_device *dev = ap->private_data; + struct sas_internal *i = dev_to_sas_internal(dev); + + if (i->dft->lldd_ata_set_dmamode) + i->dft->lldd_ata_set_dmamode(dev); +} + +static void sas_ata_sched_eh(struct ata_port *ap) +{ + struct domain_device *dev = ap->private_data; + struct sas_ha_struct *ha = dev->port->ha; + unsigned long flags; + + spin_lock_irqsave(&ha->lock, flags); + if (!test_and_set_bit(SAS_DEV_EH_PENDING, &dev->state)) + ha->eh_active++; + ata_std_sched_eh(ap); + spin_unlock_irqrestore(&ha->lock, flags); +} + +void sas_ata_end_eh(struct ata_port *ap) +{ + struct domain_device *dev = ap->private_data; + struct sas_ha_struct *ha = dev->port->ha; + unsigned long flags; + + spin_lock_irqsave(&ha->lock, flags); + if (test_and_clear_bit(SAS_DEV_EH_PENDING, &dev->state)) + ha->eh_active--; + spin_unlock_irqrestore(&ha->lock, flags); +} + +static int sas_ata_prereset(struct ata_link *link, unsigned long deadline) +{ + struct ata_port *ap = link->ap; + struct domain_device *dev = ap->private_data; + struct sas_phy *local_phy = sas_get_local_phy(dev); + int res = 0; + + if (!local_phy->enabled || test_bit(SAS_DEV_GONE, &dev->state)) + res = -ENOENT; + sas_put_local_phy(local_phy); + + return res; +} + +static struct ata_port_operations sas_sata_ops = { + .prereset = sas_ata_prereset, + .hardreset = sas_ata_hard_reset, + .error_handler = ata_std_error_handler, + .post_internal_cmd = sas_ata_post_internal, + .qc_defer = ata_std_qc_defer, + .qc_prep = ata_noop_qc_prep, + .qc_issue = sas_ata_qc_issue, + .qc_fill_rtf = sas_ata_qc_fill_rtf, + .port_start = ata_sas_port_start, + .port_stop = ata_sas_port_stop, + .set_dmamode = sas_ata_set_dmamode, + .sched_eh = sas_ata_sched_eh, + .end_eh = sas_ata_end_eh, +}; + +static struct ata_port_info sata_port_info = { + .flags = ATA_FLAG_SATA | ATA_FLAG_PIO_DMA | ATA_FLAG_NCQ | + ATA_FLAG_SAS_HOST | ATA_FLAG_FPDMA_AUX, + .pio_mask = ATA_PIO4, + .mwdma_mask = ATA_MWDMA2, + .udma_mask = ATA_UDMA6, + .port_ops = &sas_sata_ops +}; + +int sas_ata_init(struct domain_device *found_dev) +{ + struct sas_ha_struct *ha = found_dev->port->ha; + struct Scsi_Host *shost = ha->core.shost; + struct ata_host *ata_host; + struct ata_port *ap; + int rc; + + ata_host = kzalloc(sizeof(*ata_host), GFP_KERNEL); + if (!ata_host) { + pr_err("ata host alloc failed.\n"); + return -ENOMEM; + } + + ata_host_init(ata_host, ha->dev, &sas_sata_ops); + + ap = ata_sas_port_alloc(ata_host, &sata_port_info, shost); + if (!ap) { + pr_err("ata_sas_port_alloc failed.\n"); + rc = -ENODEV; + goto free_host; + } + + ap->private_data = found_dev; + ap->cbl = ATA_CBL_SATA; + ap->scsi_host = shost; + rc = ata_sas_port_init(ap); + if (rc) + goto destroy_port; + + rc = ata_sas_tport_add(ata_host->dev, ap); + if (rc) + goto destroy_port; + + found_dev->sata_dev.ata_host = ata_host; + found_dev->sata_dev.ap = ap; + + return 0; + +destroy_port: + ata_sas_port_destroy(ap); +free_host: + ata_host_put(ata_host); + return rc; +} + +void sas_ata_task_abort(struct sas_task *task) +{ + struct ata_queued_cmd *qc = task->uldd_task; + struct completion *waiting; + + /* Bounce SCSI-initiated commands to the SCSI EH */ + if (qc->scsicmd) { + blk_abort_request(qc->scsicmd->request); + return; + } + + /* Internal command, fake a timeout and complete. */ + qc->flags &= ~ATA_QCFLAG_ACTIVE; + qc->flags |= ATA_QCFLAG_FAILED; + qc->err_mask |= AC_ERR_TIMEOUT; + waiting = qc->private_data; + complete(waiting); +} + +static int sas_get_ata_command_set(struct domain_device *dev) +{ + struct dev_to_host_fis *fis = + (struct dev_to_host_fis *) dev->frame_rcvd; + struct ata_taskfile tf; + + if (dev->dev_type == SAS_SATA_PENDING) + return ATA_DEV_UNKNOWN; + + ata_tf_from_fis((const u8 *)fis, &tf); + + return ata_dev_classify(&tf); +} + +void sas_probe_sata(struct asd_sas_port *port) +{ + struct domain_device *dev, *n; + + mutex_lock(&port->ha->disco_mutex); + list_for_each_entry(dev, &port->disco_list, disco_list_node) { + if (!dev_is_sata(dev)) + continue; + + ata_sas_async_probe(dev->sata_dev.ap); + } + mutex_unlock(&port->ha->disco_mutex); + + list_for_each_entry_safe(dev, n, &port->disco_list, disco_list_node) { + if (!dev_is_sata(dev)) + continue; + + sas_ata_wait_eh(dev); + + /* if libata could not bring the link up, don't surface + * the device + */ + if (!ata_dev_enabled(sas_to_ata_dev(dev))) + sas_fail_probe(dev, __func__, -ENODEV); + } + +} + +static void sas_ata_flush_pm_eh(struct asd_sas_port *port, const char *func) +{ + struct domain_device *dev, *n; + + list_for_each_entry_safe(dev, n, &port->dev_list, dev_list_node) { + if (!dev_is_sata(dev)) + continue; + + sas_ata_wait_eh(dev); + + /* if libata failed to power manage the device, tear it down */ + if (ata_dev_disabled(sas_to_ata_dev(dev))) + sas_fail_probe(dev, func, -ENODEV); + } +} + +void sas_suspend_sata(struct asd_sas_port *port) +{ + struct domain_device *dev; + + mutex_lock(&port->ha->disco_mutex); + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + struct sata_device *sata; + + if (!dev_is_sata(dev)) + continue; + + sata = &dev->sata_dev; + if (sata->ap->pm_mesg.event == PM_EVENT_SUSPEND) + continue; + + ata_sas_port_suspend(sata->ap); + } + mutex_unlock(&port->ha->disco_mutex); + + sas_ata_flush_pm_eh(port, __func__); +} + +void sas_resume_sata(struct asd_sas_port *port) +{ + struct domain_device *dev; + + mutex_lock(&port->ha->disco_mutex); + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + struct sata_device *sata; + + if (!dev_is_sata(dev)) + continue; + + sata = &dev->sata_dev; + if (sata->ap->pm_mesg.event == PM_EVENT_ON) + continue; + + ata_sas_port_resume(sata->ap); + } + mutex_unlock(&port->ha->disco_mutex); + + sas_ata_flush_pm_eh(port, __func__); +} + +/** + * sas_discover_sata - discover an STP/SATA domain device + * @dev: pointer to struct domain_device of interest + * + * Devices directly attached to a HA port, have no parents. All other + * devices do, and should have their "parent" pointer set appropriately + * before calling this function. + */ +int sas_discover_sata(struct domain_device *dev) +{ + if (dev->dev_type == SAS_SATA_PM) + return -ENODEV; + + dev->sata_dev.class = sas_get_ata_command_set(dev); + sas_fill_in_rphy(dev, dev->rphy); + + return sas_notify_lldd_dev_found(dev); +} + +static void async_sas_ata_eh(void *data, async_cookie_t cookie) +{ + struct domain_device *dev = data; + struct ata_port *ap = dev->sata_dev.ap; + struct sas_ha_struct *ha = dev->port->ha; + + sas_ata_printk(KERN_DEBUG, dev, "dev error handler\n"); + ata_scsi_port_error_handler(ha->core.shost, ap); + sas_put_device(dev); +} + +void sas_ata_strategy_handler(struct Scsi_Host *shost) +{ + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + ASYNC_DOMAIN_EXCLUSIVE(async); + int i; + + /* it's ok to defer revalidation events during ata eh, these + * disks are in one of three states: + * 1/ present for initial domain discovery, and these + * resets will cause bcn flutters + * 2/ hot removed, we'll discover that after eh fails + * 3/ hot added after initial discovery, lost the race, and need + * to catch the next train. + */ + sas_disable_revalidation(sas_ha); + + spin_lock_irq(&sas_ha->phy_port_lock); + for (i = 0; i < sas_ha->num_phys; i++) { + struct asd_sas_port *port = sas_ha->sas_port[i]; + struct domain_device *dev; + + spin_lock(&port->dev_list_lock); + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + if (!dev_is_sata(dev)) + continue; + + /* hold a reference over eh since we may be + * racing with final remove once all commands + * are completed + */ + kref_get(&dev->kref); + + async_schedule_domain(async_sas_ata_eh, dev, &async); + } + spin_unlock(&port->dev_list_lock); + } + spin_unlock_irq(&sas_ha->phy_port_lock); + + async_synchronize_full_domain(&async); + + sas_enable_revalidation(sas_ha); +} + +void sas_ata_eh(struct Scsi_Host *shost, struct list_head *work_q, + struct list_head *done_q) +{ + struct scsi_cmnd *cmd, *n; + struct domain_device *eh_dev; + + do { + LIST_HEAD(sata_q); + eh_dev = NULL; + + list_for_each_entry_safe(cmd, n, work_q, eh_entry) { + struct domain_device *ddev = cmd_to_domain_dev(cmd); + + if (!dev_is_sata(ddev) || TO_SAS_TASK(cmd)) + continue; + if (eh_dev && eh_dev != ddev) + continue; + eh_dev = ddev; + list_move(&cmd->eh_entry, &sata_q); + } + + if (!list_empty(&sata_q)) { + struct ata_port *ap = eh_dev->sata_dev.ap; + + sas_ata_printk(KERN_DEBUG, eh_dev, "cmd error handler\n"); + ata_scsi_cmd_error_handler(shost, ap, &sata_q); + /* + * ata's error handler may leave the cmd on the list + * so make sure they don't remain on a stack list + * about to go out of scope. + * + * This looks strange, since the commands are + * now part of no list, but the next error + * action will be ata_port_error_handler() + * which takes no list and sweeps them up + * anyway from the ata tag array. + */ + while (!list_empty(&sata_q)) + list_del_init(sata_q.next); + } + } while (eh_dev); +} + +void sas_ata_schedule_reset(struct domain_device *dev) +{ + struct ata_eh_info *ehi; + struct ata_port *ap; + unsigned long flags; + + if (!dev_is_sata(dev)) + return; + + ap = dev->sata_dev.ap; + ehi = &ap->link.eh_info; + + spin_lock_irqsave(ap->lock, flags); + ehi->err_mask |= AC_ERR_TIMEOUT; + ehi->action |= ATA_EH_RESET; + ata_port_schedule_eh(ap); + spin_unlock_irqrestore(ap->lock, flags); +} +EXPORT_SYMBOL_GPL(sas_ata_schedule_reset); + +void sas_ata_wait_eh(struct domain_device *dev) +{ + struct ata_port *ap; + + if (!dev_is_sata(dev)) + return; + + ap = dev->sata_dev.ap; + ata_port_wait_eh(ap); +} diff --git a/drivers/scsi/libsas/sas_discover.c b/drivers/scsi/libsas/sas_discover.c new file mode 100644 index 000000000..161c9b387 --- /dev/null +++ b/drivers/scsi/libsas/sas_discover.c @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Serial Attached SCSI (SAS) Discover process + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include <linux/scatterlist.h> +#include <linux/slab.h> +#include <linux/async.h> +#include <scsi/scsi_host.h> +#include <scsi/scsi_eh.h> +#include "sas_internal.h" + +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include <scsi/sas_ata.h> +#include "../scsi_sas_internal.h" + +/* ---------- Basic task processing for discovery purposes ---------- */ + +void sas_init_dev(struct domain_device *dev) +{ + switch (dev->dev_type) { + case SAS_END_DEVICE: + INIT_LIST_HEAD(&dev->ssp_dev.eh_list_node); + break; + case SAS_EDGE_EXPANDER_DEVICE: + case SAS_FANOUT_EXPANDER_DEVICE: + INIT_LIST_HEAD(&dev->ex_dev.children); + mutex_init(&dev->ex_dev.cmd_mutex); + break; + default: + break; + } +} + +/* ---------- Domain device discovery ---------- */ + +/** + * sas_get_port_device - Discover devices which caused port creation + * @port: pointer to struct sas_port of interest + * + * Devices directly attached to a HA port, have no parent. This is + * how we know they are (domain) "root" devices. All other devices + * do, and should have their "parent" pointer set appropriately as + * soon as a child device is discovered. + */ +static int sas_get_port_device(struct asd_sas_port *port) +{ + struct asd_sas_phy *phy; + struct sas_rphy *rphy; + struct domain_device *dev; + int rc = -ENODEV; + + dev = sas_alloc_device(); + if (!dev) + return -ENOMEM; + + spin_lock_irq(&port->phy_list_lock); + if (list_empty(&port->phy_list)) { + spin_unlock_irq(&port->phy_list_lock); + sas_put_device(dev); + return -ENODEV; + } + phy = container_of(port->phy_list.next, struct asd_sas_phy, port_phy_el); + spin_lock(&phy->frame_rcvd_lock); + memcpy(dev->frame_rcvd, phy->frame_rcvd, min(sizeof(dev->frame_rcvd), + (size_t)phy->frame_rcvd_size)); + spin_unlock(&phy->frame_rcvd_lock); + spin_unlock_irq(&port->phy_list_lock); + + if (dev->frame_rcvd[0] == 0x34 && port->oob_mode == SATA_OOB_MODE) { + struct dev_to_host_fis *fis = + (struct dev_to_host_fis *) dev->frame_rcvd; + if (fis->interrupt_reason == 1 && fis->lbal == 1 && + fis->byte_count_low==0x69 && fis->byte_count_high == 0x96 + && (fis->device & ~0x10) == 0) + dev->dev_type = SAS_SATA_PM; + else + dev->dev_type = SAS_SATA_DEV; + dev->tproto = SAS_PROTOCOL_SATA; + } else if (port->oob_mode == SAS_OOB_MODE) { + struct sas_identify_frame *id = + (struct sas_identify_frame *) dev->frame_rcvd; + dev->dev_type = id->dev_type; + dev->iproto = id->initiator_bits; + dev->tproto = id->target_bits; + } else { + /* If the oob mode is OOB_NOT_CONNECTED, the port is + * disconnected due to race with PHY down. We cannot + * continue to discover this port + */ + sas_put_device(dev); + pr_warn("Port %016llx is disconnected when discovering\n", + SAS_ADDR(port->attached_sas_addr)); + return -ENODEV; + } + + sas_init_dev(dev); + + dev->port = port; + switch (dev->dev_type) { + case SAS_SATA_DEV: + rc = sas_ata_init(dev); + if (rc) { + rphy = NULL; + break; + } + fallthrough; + case SAS_END_DEVICE: + rphy = sas_end_device_alloc(port->port); + break; + case SAS_EDGE_EXPANDER_DEVICE: + rphy = sas_expander_alloc(port->port, + SAS_EDGE_EXPANDER_DEVICE); + break; + case SAS_FANOUT_EXPANDER_DEVICE: + rphy = sas_expander_alloc(port->port, + SAS_FANOUT_EXPANDER_DEVICE); + break; + default: + pr_warn("ERROR: Unidentified device type %d\n", dev->dev_type); + rphy = NULL; + break; + } + + if (!rphy) { + sas_put_device(dev); + return rc; + } + + rphy->identify.phy_identifier = phy->phy->identify.phy_identifier; + memcpy(dev->sas_addr, port->attached_sas_addr, SAS_ADDR_SIZE); + sas_fill_in_rphy(dev, rphy); + sas_hash_addr(dev->hashed_sas_addr, dev->sas_addr); + port->port_dev = dev; + dev->linkrate = port->linkrate; + dev->min_linkrate = port->linkrate; + dev->max_linkrate = port->linkrate; + dev->pathways = port->num_phys; + memset(port->disc.fanout_sas_addr, 0, SAS_ADDR_SIZE); + memset(port->disc.eeds_a, 0, SAS_ADDR_SIZE); + memset(port->disc.eeds_b, 0, SAS_ADDR_SIZE); + port->disc.max_level = 0; + sas_device_set_phy(dev, port->port); + + dev->rphy = rphy; + get_device(&dev->rphy->dev); + + if (dev_is_sata(dev) || dev->dev_type == SAS_END_DEVICE) + list_add_tail(&dev->disco_list_node, &port->disco_list); + else { + spin_lock_irq(&port->dev_list_lock); + list_add_tail(&dev->dev_list_node, &port->dev_list); + spin_unlock_irq(&port->dev_list_lock); + } + + spin_lock_irq(&port->phy_list_lock); + list_for_each_entry(phy, &port->phy_list, port_phy_el) + sas_phy_set_target(phy, dev); + spin_unlock_irq(&port->phy_list_lock); + + return 0; +} + +/* ---------- Discover and Revalidate ---------- */ + +int sas_notify_lldd_dev_found(struct domain_device *dev) +{ + int res = 0; + struct sas_ha_struct *sas_ha = dev->port->ha; + struct Scsi_Host *shost = sas_ha->core.shost; + struct sas_internal *i = to_sas_internal(shost->transportt); + + if (!i->dft->lldd_dev_found) + return 0; + + res = i->dft->lldd_dev_found(dev); + if (res) { + pr_warn("driver on host %s cannot handle device %016llx, error:%d\n", + dev_name(sas_ha->dev), + SAS_ADDR(dev->sas_addr), res); + return res; + } + set_bit(SAS_DEV_FOUND, &dev->state); + kref_get(&dev->kref); + return 0; +} + + +void sas_notify_lldd_dev_gone(struct domain_device *dev) +{ + struct sas_ha_struct *sas_ha = dev->port->ha; + struct Scsi_Host *shost = sas_ha->core.shost; + struct sas_internal *i = to_sas_internal(shost->transportt); + + if (!i->dft->lldd_dev_gone) + return; + + if (test_and_clear_bit(SAS_DEV_FOUND, &dev->state)) { + i->dft->lldd_dev_gone(dev); + sas_put_device(dev); + } +} + +static void sas_probe_devices(struct asd_sas_port *port) +{ + struct domain_device *dev, *n; + + /* devices must be domain members before link recovery and probe */ + list_for_each_entry(dev, &port->disco_list, disco_list_node) { + spin_lock_irq(&port->dev_list_lock); + list_add_tail(&dev->dev_list_node, &port->dev_list); + spin_unlock_irq(&port->dev_list_lock); + } + + sas_probe_sata(port); + + list_for_each_entry_safe(dev, n, &port->disco_list, disco_list_node) { + int err; + + err = sas_rphy_add(dev->rphy); + if (err) + sas_fail_probe(dev, __func__, err); + else + list_del_init(&dev->disco_list_node); + } +} + +static void sas_suspend_devices(struct work_struct *work) +{ + struct asd_sas_phy *phy; + struct domain_device *dev; + struct sas_discovery_event *ev = to_sas_discovery_event(work); + struct asd_sas_port *port = ev->port; + struct Scsi_Host *shost = port->ha->core.shost; + struct sas_internal *si = to_sas_internal(shost->transportt); + + clear_bit(DISCE_SUSPEND, &port->disc.pending); + + sas_suspend_sata(port); + + /* lldd is free to forget the domain_device across the + * suspension, we force the issue here to keep the reference + * counts aligned + */ + list_for_each_entry(dev, &port->dev_list, dev_list_node) + sas_notify_lldd_dev_gone(dev); + + /* we are suspending, so we know events are disabled and + * phy_list is not being mutated + */ + list_for_each_entry(phy, &port->phy_list, port_phy_el) { + if (si->dft->lldd_port_deformed) + si->dft->lldd_port_deformed(phy); + phy->suspended = 1; + port->suspended = 1; + } +} + +static void sas_resume_devices(struct work_struct *work) +{ + struct sas_discovery_event *ev = to_sas_discovery_event(work); + struct asd_sas_port *port = ev->port; + + clear_bit(DISCE_RESUME, &port->disc.pending); + + sas_resume_sata(port); +} + +/** + * sas_discover_end_dev - discover an end device (SSP, etc) + * @dev: pointer to domain device of interest + * + * See comment in sas_discover_sata(). + */ +int sas_discover_end_dev(struct domain_device *dev) +{ + return sas_notify_lldd_dev_found(dev); +} + +/* ---------- Device registration and unregistration ---------- */ + +void sas_free_device(struct kref *kref) +{ + struct domain_device *dev = container_of(kref, typeof(*dev), kref); + + put_device(&dev->rphy->dev); + dev->rphy = NULL; + + if (dev->parent) + sas_put_device(dev->parent); + + sas_port_put_phy(dev->phy); + dev->phy = NULL; + + /* remove the phys and ports, everything else should be gone */ + if (dev_is_expander(dev->dev_type)) + kfree(dev->ex_dev.ex_phy); + + if (dev_is_sata(dev) && dev->sata_dev.ap) { + ata_sas_tport_delete(dev->sata_dev.ap); + ata_sas_port_destroy(dev->sata_dev.ap); + ata_host_put(dev->sata_dev.ata_host); + dev->sata_dev.ata_host = NULL; + dev->sata_dev.ap = NULL; + } + + kfree(dev); +} + +static void sas_unregister_common_dev(struct asd_sas_port *port, struct domain_device *dev) +{ + struct sas_ha_struct *ha = port->ha; + + sas_notify_lldd_dev_gone(dev); + if (!dev->parent) + dev->port->port_dev = NULL; + else + list_del_init(&dev->siblings); + + spin_lock_irq(&port->dev_list_lock); + list_del_init(&dev->dev_list_node); + if (dev_is_sata(dev)) + sas_ata_end_eh(dev->sata_dev.ap); + spin_unlock_irq(&port->dev_list_lock); + + spin_lock_irq(&ha->lock); + if (dev->dev_type == SAS_END_DEVICE && + !list_empty(&dev->ssp_dev.eh_list_node)) { + list_del_init(&dev->ssp_dev.eh_list_node); + ha->eh_active--; + } + spin_unlock_irq(&ha->lock); + + sas_put_device(dev); +} + +void sas_destruct_devices(struct asd_sas_port *port) +{ + struct domain_device *dev, *n; + + list_for_each_entry_safe(dev, n, &port->destroy_list, disco_list_node) { + list_del_init(&dev->disco_list_node); + + sas_remove_children(&dev->rphy->dev); + sas_rphy_delete(dev->rphy); + sas_unregister_common_dev(port, dev); + } +} + +static void sas_destruct_ports(struct asd_sas_port *port) +{ + struct sas_port *sas_port, *p; + + list_for_each_entry_safe(sas_port, p, &port->sas_port_del_list, del_list) { + list_del_init(&sas_port->del_list); + sas_port_delete(sas_port); + } +} + +void sas_unregister_dev(struct asd_sas_port *port, struct domain_device *dev) +{ + if (!test_bit(SAS_DEV_DESTROY, &dev->state) && + !list_empty(&dev->disco_list_node)) { + /* this rphy never saw sas_rphy_add */ + list_del_init(&dev->disco_list_node); + sas_rphy_free(dev->rphy); + sas_unregister_common_dev(port, dev); + return; + } + + if (!test_and_set_bit(SAS_DEV_DESTROY, &dev->state)) { + sas_rphy_unlink(dev->rphy); + list_move_tail(&dev->disco_list_node, &port->destroy_list); + } +} + +void sas_unregister_domain_devices(struct asd_sas_port *port, int gone) +{ + struct domain_device *dev, *n; + + list_for_each_entry_safe_reverse(dev, n, &port->dev_list, dev_list_node) { + if (gone) + set_bit(SAS_DEV_GONE, &dev->state); + sas_unregister_dev(port, dev); + } + + list_for_each_entry_safe(dev, n, &port->disco_list, disco_list_node) + sas_unregister_dev(port, dev); + + port->port->rphy = NULL; + +} + +void sas_device_set_phy(struct domain_device *dev, struct sas_port *port) +{ + struct sas_ha_struct *ha; + struct sas_phy *new_phy; + + if (!dev) + return; + + ha = dev->port->ha; + new_phy = sas_port_get_phy(port); + + /* pin and record last seen phy */ + spin_lock_irq(&ha->phy_port_lock); + if (new_phy) { + sas_port_put_phy(dev->phy); + dev->phy = new_phy; + } + spin_unlock_irq(&ha->phy_port_lock); +} + +/* ---------- Discovery and Revalidation ---------- */ + +/** + * sas_discover_domain - discover the domain + * @work: work structure embedded in port domain device. + * + * NOTE: this process _must_ quit (return) as soon as any connection + * errors are encountered. Connection recovery is done elsewhere. + * Discover process only interrogates devices in order to discover the + * domain. + */ +static void sas_discover_domain(struct work_struct *work) +{ + struct domain_device *dev; + int error = 0; + struct sas_discovery_event *ev = to_sas_discovery_event(work); + struct asd_sas_port *port = ev->port; + + clear_bit(DISCE_DISCOVER_DOMAIN, &port->disc.pending); + + if (port->port_dev) + return; + + error = sas_get_port_device(port); + if (error) + return; + dev = port->port_dev; + + pr_debug("DOING DISCOVERY on port %d, pid:%d\n", port->id, + task_pid_nr(current)); + + switch (dev->dev_type) { + case SAS_END_DEVICE: + error = sas_discover_end_dev(dev); + break; + case SAS_EDGE_EXPANDER_DEVICE: + case SAS_FANOUT_EXPANDER_DEVICE: + error = sas_discover_root_expander(dev); + break; + case SAS_SATA_DEV: + case SAS_SATA_PM: +#ifdef CONFIG_SCSI_SAS_ATA + error = sas_discover_sata(dev); + break; +#else + pr_notice("ATA device seen but CONFIG_SCSI_SAS_ATA=N so cannot attach\n"); + /* Fall through */ +#endif + /* Fall through - only for the #else condition above. */ + default: + error = -ENXIO; + pr_err("unhandled device %d\n", dev->dev_type); + break; + } + + if (error) { + sas_rphy_free(dev->rphy); + list_del_init(&dev->disco_list_node); + spin_lock_irq(&port->dev_list_lock); + list_del_init(&dev->dev_list_node); + spin_unlock_irq(&port->dev_list_lock); + + sas_put_device(dev); + port->port_dev = NULL; + } + + sas_probe_devices(port); + + pr_debug("DONE DISCOVERY on port %d, pid:%d, result:%d\n", port->id, + task_pid_nr(current), error); +} + +static void sas_revalidate_domain(struct work_struct *work) +{ + int res = 0; + struct sas_discovery_event *ev = to_sas_discovery_event(work); + struct asd_sas_port *port = ev->port; + struct sas_ha_struct *ha = port->ha; + struct domain_device *ddev = port->port_dev; + + /* prevent revalidation from finding sata links in recovery */ + mutex_lock(&ha->disco_mutex); + if (test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) { + pr_debug("REVALIDATION DEFERRED on port %d, pid:%d\n", + port->id, task_pid_nr(current)); + goto out; + } + + clear_bit(DISCE_REVALIDATE_DOMAIN, &port->disc.pending); + + pr_debug("REVALIDATING DOMAIN on port %d, pid:%d\n", port->id, + task_pid_nr(current)); + + if (ddev && dev_is_expander(ddev->dev_type)) + res = sas_ex_revalidate_domain(ddev); + + pr_debug("done REVALIDATING DOMAIN on port %d, pid:%d, res 0x%x\n", + port->id, task_pid_nr(current), res); + out: + mutex_unlock(&ha->disco_mutex); + + sas_destruct_devices(port); + sas_destruct_ports(port); + sas_probe_devices(port); +} + +/* ---------- Events ---------- */ + +static void sas_chain_work(struct sas_ha_struct *ha, struct sas_work *sw) +{ + /* chained work is not subject to SA_HA_DRAINING or + * SAS_HA_REGISTERED, because it is either submitted in the + * workqueue, or known to be submitted from a context that is + * not racing against draining + */ + queue_work(ha->disco_q, &sw->work); +} + +static void sas_chain_event(int event, unsigned long *pending, + struct sas_work *sw, + struct sas_ha_struct *ha) +{ + if (!test_and_set_bit(event, pending)) { + unsigned long flags; + + spin_lock_irqsave(&ha->lock, flags); + sas_chain_work(ha, sw); + spin_unlock_irqrestore(&ha->lock, flags); + } +} + +int sas_discover_event(struct asd_sas_port *port, enum discover_event ev) +{ + struct sas_discovery *disc; + + if (!port) + return 0; + disc = &port->disc; + + BUG_ON(ev >= DISC_NUM_EVENTS); + + sas_chain_event(ev, &disc->pending, &disc->disc_work[ev].work, port->ha); + + return 0; +} + +/** + * sas_init_disc - initialize the discovery struct in the port + * @disc: port discovery structure + * @port: pointer to struct port + * + * Called when the ports are being initialized. + */ +void sas_init_disc(struct sas_discovery *disc, struct asd_sas_port *port) +{ + int i; + + static const work_func_t sas_event_fns[DISC_NUM_EVENTS] = { + [DISCE_DISCOVER_DOMAIN] = sas_discover_domain, + [DISCE_REVALIDATE_DOMAIN] = sas_revalidate_domain, + [DISCE_SUSPEND] = sas_suspend_devices, + [DISCE_RESUME] = sas_resume_devices, + }; + + disc->pending = 0; + for (i = 0; i < DISC_NUM_EVENTS; i++) { + INIT_SAS_WORK(&disc->disc_work[i].work, sas_event_fns[i]); + disc->disc_work[i].port = port; + } +} diff --git a/drivers/scsi/libsas/sas_event.c b/drivers/scsi/libsas/sas_event.c new file mode 100644 index 000000000..ba266a172 --- /dev/null +++ b/drivers/scsi/libsas/sas_event.c @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Serial Attached SCSI (SAS) Event processing + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include <linux/export.h> +#include <scsi/scsi_host.h> +#include "sas_internal.h" + +int sas_queue_work(struct sas_ha_struct *ha, struct sas_work *sw) +{ + /* it's added to the defer_q when draining so return succeed */ + int rc = 1; + + if (!test_bit(SAS_HA_REGISTERED, &ha->state)) + return 0; + + if (test_bit(SAS_HA_DRAINING, &ha->state)) { + /* add it to the defer list, if not already pending */ + if (list_empty(&sw->drain_node)) + list_add_tail(&sw->drain_node, &ha->defer_q); + } else + rc = queue_work(ha->event_q, &sw->work); + + return rc; +} + +static int sas_queue_event(int event, struct sas_work *work, + struct sas_ha_struct *ha) +{ + unsigned long flags; + int rc; + + spin_lock_irqsave(&ha->lock, flags); + rc = sas_queue_work(ha, work); + spin_unlock_irqrestore(&ha->lock, flags); + + return rc; +} + + +void __sas_drain_work(struct sas_ha_struct *ha) +{ + struct sas_work *sw, *_sw; + int ret; + + set_bit(SAS_HA_DRAINING, &ha->state); + /* flush submitters */ + spin_lock_irq(&ha->lock); + spin_unlock_irq(&ha->lock); + + drain_workqueue(ha->event_q); + drain_workqueue(ha->disco_q); + + spin_lock_irq(&ha->lock); + clear_bit(SAS_HA_DRAINING, &ha->state); + list_for_each_entry_safe(sw, _sw, &ha->defer_q, drain_node) { + list_del_init(&sw->drain_node); + ret = sas_queue_work(ha, sw); + if (ret != 1) + sas_free_event(to_asd_sas_event(&sw->work)); + + } + spin_unlock_irq(&ha->lock); +} + +int sas_drain_work(struct sas_ha_struct *ha) +{ + int err; + + err = mutex_lock_interruptible(&ha->drain_mutex); + if (err) + return err; + if (test_bit(SAS_HA_REGISTERED, &ha->state)) + __sas_drain_work(ha); + mutex_unlock(&ha->drain_mutex); + + return 0; +} +EXPORT_SYMBOL_GPL(sas_drain_work); + +void sas_disable_revalidation(struct sas_ha_struct *ha) +{ + mutex_lock(&ha->disco_mutex); + set_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state); + mutex_unlock(&ha->disco_mutex); +} + +void sas_enable_revalidation(struct sas_ha_struct *ha) +{ + int i; + + mutex_lock(&ha->disco_mutex); + clear_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state); + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_port *port = ha->sas_port[i]; + const int ev = DISCE_REVALIDATE_DOMAIN; + struct sas_discovery *d = &port->disc; + struct asd_sas_phy *sas_phy; + + if (!test_and_clear_bit(ev, &d->pending)) + continue; + + if (list_empty(&port->phy_list)) + continue; + + sas_phy = container_of(port->phy_list.next, struct asd_sas_phy, + port_phy_el); + sas_notify_port_event(sas_phy, PORTE_BROADCAST_RCVD); + } + mutex_unlock(&ha->disco_mutex); +} + + +static void sas_port_event_worker(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + + sas_port_event_fns[ev->event](work); + sas_free_event(ev); +} + +static void sas_phy_event_worker(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + + sas_phy_event_fns[ev->event](work); + sas_free_event(ev); +} + +static int __sas_notify_port_event(struct asd_sas_phy *phy, + enum port_event event, + struct asd_sas_event *ev) +{ + struct sas_ha_struct *ha = phy->ha; + int ret; + + BUG_ON(event >= PORT_NUM_EVENTS); + + INIT_SAS_EVENT(ev, sas_port_event_worker, phy, event); + + ret = sas_queue_event(event, &ev->work, ha); + if (ret != 1) + sas_free_event(ev); + + return ret; +} + +int sas_notify_port_event_gfp(struct asd_sas_phy *phy, enum port_event event, + gfp_t gfp_flags) +{ + struct asd_sas_event *ev; + + ev = sas_alloc_event_gfp(phy, gfp_flags); + if (!ev) + return -ENOMEM; + + return __sas_notify_port_event(phy, event, ev); +} +EXPORT_SYMBOL_GPL(sas_notify_port_event_gfp); + +int sas_notify_port_event(struct asd_sas_phy *phy, enum port_event event) +{ + struct asd_sas_event *ev; + + ev = sas_alloc_event(phy); + if (!ev) + return -ENOMEM; + + return __sas_notify_port_event(phy, event, ev); +} +EXPORT_SYMBOL_GPL(sas_notify_port_event); + +static inline int __sas_notify_phy_event(struct asd_sas_phy *phy, + enum phy_event event, + struct asd_sas_event *ev) +{ + struct sas_ha_struct *ha = phy->ha; + int ret; + + BUG_ON(event >= PHY_NUM_EVENTS); + + INIT_SAS_EVENT(ev, sas_phy_event_worker, phy, event); + + ret = sas_queue_event(event, &ev->work, ha); + if (ret != 1) + sas_free_event(ev); + + return ret; +} + +int sas_notify_phy_event_gfp(struct asd_sas_phy *phy, enum phy_event event, + gfp_t gfp_flags) +{ + struct asd_sas_event *ev; + + ev = sas_alloc_event_gfp(phy, gfp_flags); + if (!ev) + return -ENOMEM; + + return __sas_notify_phy_event(phy, event, ev); +} +EXPORT_SYMBOL_GPL(sas_notify_phy_event_gfp); + +int sas_notify_phy_event(struct asd_sas_phy *phy, enum phy_event event) +{ + struct asd_sas_event *ev; + + ev = sas_alloc_event(phy); + if (!ev) + return -ENOMEM; + + return __sas_notify_phy_event(phy, event, ev); +} +EXPORT_SYMBOL_GPL(sas_notify_phy_event); diff --git a/drivers/scsi/libsas/sas_expander.c b/drivers/scsi/libsas/sas_expander.c new file mode 100644 index 000000000..8444a4287 --- /dev/null +++ b/drivers/scsi/libsas/sas_expander.c @@ -0,0 +1,2181 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Serial Attached SCSI (SAS) Expander discovery and configuration + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + * + * This file is licensed under GPLv2. + */ + +#include <linux/scatterlist.h> +#include <linux/blkdev.h> +#include <linux/slab.h> +#include <asm/unaligned.h> + +#include "sas_internal.h" + +#include <scsi/sas_ata.h> +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include "../scsi_sas_internal.h" + +static int sas_discover_expander(struct domain_device *dev); +static int sas_configure_routing(struct domain_device *dev, u8 *sas_addr); +static int sas_configure_phy(struct domain_device *dev, int phy_id, + u8 *sas_addr, int include); +static int sas_disable_routing(struct domain_device *dev, u8 *sas_addr); + +/* ---------- SMP task management ---------- */ + +static void smp_task_timedout(struct timer_list *t) +{ + struct sas_task_slow *slow = from_timer(slow, t, timer); + struct sas_task *task = slow->task; + unsigned long flags; + + spin_lock_irqsave(&task->task_state_lock, flags); + if (!(task->task_state_flags & SAS_TASK_STATE_DONE)) { + task->task_state_flags |= SAS_TASK_STATE_ABORTED; + complete(&task->slow_task->completion); + } + spin_unlock_irqrestore(&task->task_state_lock, flags); +} + +static void smp_task_done(struct sas_task *task) +{ + del_timer(&task->slow_task->timer); + complete(&task->slow_task->completion); +} + +/* Give it some long enough timeout. In seconds. */ +#define SMP_TIMEOUT 10 + +static int smp_execute_task_sg(struct domain_device *dev, + struct scatterlist *req, struct scatterlist *resp) +{ + int res, retry; + struct sas_task *task = NULL; + struct sas_internal *i = + to_sas_internal(dev->port->ha->core.shost->transportt); + + mutex_lock(&dev->ex_dev.cmd_mutex); + for (retry = 0; retry < 3; retry++) { + if (test_bit(SAS_DEV_GONE, &dev->state)) { + res = -ECOMM; + break; + } + + task = sas_alloc_slow_task(GFP_KERNEL); + if (!task) { + res = -ENOMEM; + break; + } + task->dev = dev; + task->task_proto = dev->tproto; + task->smp_task.smp_req = *req; + task->smp_task.smp_resp = *resp; + + task->task_done = smp_task_done; + + task->slow_task->timer.function = smp_task_timedout; + task->slow_task->timer.expires = jiffies + SMP_TIMEOUT*HZ; + add_timer(&task->slow_task->timer); + + res = i->dft->lldd_execute_task(task, GFP_KERNEL); + + if (res) { + del_timer_sync(&task->slow_task->timer); + pr_notice("executing SMP task failed:%d\n", res); + break; + } + + wait_for_completion(&task->slow_task->completion); + res = -ECOMM; + if ((task->task_state_flags & SAS_TASK_STATE_ABORTED)) { + pr_notice("smp task timed out or aborted\n"); + i->dft->lldd_abort_task(task); + if (!(task->task_state_flags & SAS_TASK_STATE_DONE)) { + pr_notice("SMP task aborted and not done\n"); + break; + } + } + if (task->task_status.resp == SAS_TASK_COMPLETE && + task->task_status.stat == SAS_SAM_STAT_GOOD) { + res = 0; + break; + } + if (task->task_status.resp == SAS_TASK_COMPLETE && + task->task_status.stat == SAS_DATA_UNDERRUN) { + /* no error, but return the number of bytes of + * underrun */ + res = task->task_status.residual; + break; + } + if (task->task_status.resp == SAS_TASK_COMPLETE && + task->task_status.stat == SAS_DATA_OVERRUN) { + res = -EMSGSIZE; + break; + } + if (task->task_status.resp == SAS_TASK_UNDELIVERED && + task->task_status.stat == SAS_DEVICE_UNKNOWN) + break; + else { + pr_notice("%s: task to dev %016llx response: 0x%x status 0x%x\n", + __func__, + SAS_ADDR(dev->sas_addr), + task->task_status.resp, + task->task_status.stat); + sas_free_task(task); + task = NULL; + } + } + mutex_unlock(&dev->ex_dev.cmd_mutex); + + BUG_ON(retry == 3 && task != NULL); + sas_free_task(task); + return res; +} + +static int smp_execute_task(struct domain_device *dev, void *req, int req_size, + void *resp, int resp_size) +{ + struct scatterlist req_sg; + struct scatterlist resp_sg; + + sg_init_one(&req_sg, req, req_size); + sg_init_one(&resp_sg, resp, resp_size); + return smp_execute_task_sg(dev, &req_sg, &resp_sg); +} + +/* ---------- Allocations ---------- */ + +static inline void *alloc_smp_req(int size) +{ + u8 *p = kzalloc(size, GFP_KERNEL); + if (p) + p[0] = SMP_REQUEST; + return p; +} + +static inline void *alloc_smp_resp(int size) +{ + return kzalloc(size, GFP_KERNEL); +} + +static char sas_route_char(struct domain_device *dev, struct ex_phy *phy) +{ + switch (phy->routing_attr) { + case TABLE_ROUTING: + if (dev->ex_dev.t2t_supp) + return 'U'; + else + return 'T'; + case DIRECT_ROUTING: + return 'D'; + case SUBTRACTIVE_ROUTING: + return 'S'; + default: + return '?'; + } +} + +static enum sas_device_type to_dev_type(struct discover_resp *dr) +{ + /* This is detecting a failure to transmit initial dev to host + * FIS as described in section J.5 of sas-2 r16 + */ + if (dr->attached_dev_type == SAS_PHY_UNUSED && dr->attached_sata_dev && + dr->linkrate >= SAS_LINK_RATE_1_5_GBPS) + return SAS_SATA_PENDING; + else + return dr->attached_dev_type; +} + +static void sas_set_ex_phy(struct domain_device *dev, int phy_id, void *rsp) +{ + enum sas_device_type dev_type; + enum sas_linkrate linkrate; + u8 sas_addr[SAS_ADDR_SIZE]; + struct smp_resp *resp = rsp; + struct discover_resp *dr = &resp->disc; + struct sas_ha_struct *ha = dev->port->ha; + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *phy = &ex->ex_phy[phy_id]; + struct sas_rphy *rphy = dev->rphy; + bool new_phy = !phy->phy; + char *type; + + if (new_phy) { + if (WARN_ON_ONCE(test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state))) + return; + phy->phy = sas_phy_alloc(&rphy->dev, phy_id); + + /* FIXME: error_handling */ + BUG_ON(!phy->phy); + } + + switch (resp->result) { + case SMP_RESP_PHY_VACANT: + phy->phy_state = PHY_VACANT; + break; + default: + phy->phy_state = PHY_NOT_PRESENT; + break; + case SMP_RESP_FUNC_ACC: + phy->phy_state = PHY_EMPTY; /* do not know yet */ + break; + } + + /* check if anything important changed to squelch debug */ + dev_type = phy->attached_dev_type; + linkrate = phy->linkrate; + memcpy(sas_addr, phy->attached_sas_addr, SAS_ADDR_SIZE); + + /* Handle vacant phy - rest of dr data is not valid so skip it */ + if (phy->phy_state == PHY_VACANT) { + memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE); + phy->attached_dev_type = SAS_PHY_UNUSED; + if (!test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) { + phy->phy_id = phy_id; + goto skip; + } else + goto out; + } + + phy->attached_dev_type = to_dev_type(dr); + if (test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) + goto out; + phy->phy_id = phy_id; + phy->linkrate = dr->linkrate; + phy->attached_sata_host = dr->attached_sata_host; + phy->attached_sata_dev = dr->attached_sata_dev; + phy->attached_sata_ps = dr->attached_sata_ps; + phy->attached_iproto = dr->iproto << 1; + phy->attached_tproto = dr->tproto << 1; + /* help some expanders that fail to zero sas_address in the 'no + * device' case + */ + if (phy->attached_dev_type == SAS_PHY_UNUSED || + phy->linkrate < SAS_LINK_RATE_1_5_GBPS) + memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE); + else + memcpy(phy->attached_sas_addr, dr->attached_sas_addr, SAS_ADDR_SIZE); + phy->attached_phy_id = dr->attached_phy_id; + phy->phy_change_count = dr->change_count; + phy->routing_attr = dr->routing_attr; + phy->virtual = dr->virtual; + phy->last_da_index = -1; + + phy->phy->identify.sas_address = SAS_ADDR(phy->attached_sas_addr); + phy->phy->identify.device_type = dr->attached_dev_type; + phy->phy->identify.initiator_port_protocols = phy->attached_iproto; + phy->phy->identify.target_port_protocols = phy->attached_tproto; + if (!phy->attached_tproto && dr->attached_sata_dev) + phy->phy->identify.target_port_protocols = SAS_PROTOCOL_SATA; + phy->phy->identify.phy_identifier = phy_id; + phy->phy->minimum_linkrate_hw = dr->hmin_linkrate; + phy->phy->maximum_linkrate_hw = dr->hmax_linkrate; + phy->phy->minimum_linkrate = dr->pmin_linkrate; + phy->phy->maximum_linkrate = dr->pmax_linkrate; + phy->phy->negotiated_linkrate = phy->linkrate; + phy->phy->enabled = (phy->linkrate != SAS_PHY_DISABLED); + + skip: + if (new_phy) + if (sas_phy_add(phy->phy)) { + sas_phy_free(phy->phy); + return; + } + + out: + switch (phy->attached_dev_type) { + case SAS_SATA_PENDING: + type = "stp pending"; + break; + case SAS_PHY_UNUSED: + type = "no device"; + break; + case SAS_END_DEVICE: + if (phy->attached_iproto) { + if (phy->attached_tproto) + type = "host+target"; + else + type = "host"; + } else { + if (dr->attached_sata_dev) + type = "stp"; + else + type = "ssp"; + } + break; + case SAS_EDGE_EXPANDER_DEVICE: + case SAS_FANOUT_EXPANDER_DEVICE: + type = "smp"; + break; + default: + type = "unknown"; + } + + /* this routine is polled by libata error recovery so filter + * unimportant messages + */ + if (new_phy || phy->attached_dev_type != dev_type || + phy->linkrate != linkrate || + SAS_ADDR(phy->attached_sas_addr) != SAS_ADDR(sas_addr)) + /* pass */; + else + return; + + /* if the attached device type changed and ata_eh is active, + * make sure we run revalidation when eh completes (see: + * sas_enable_revalidation) + */ + if (test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state)) + set_bit(DISCE_REVALIDATE_DOMAIN, &dev->port->disc.pending); + + pr_debug("%sex %016llx phy%02d:%c:%X attached: %016llx (%s)\n", + test_bit(SAS_HA_ATA_EH_ACTIVE, &ha->state) ? "ata: " : "", + SAS_ADDR(dev->sas_addr), phy->phy_id, + sas_route_char(dev, phy), phy->linkrate, + SAS_ADDR(phy->attached_sas_addr), type); +} + +/* check if we have an existing attached ata device on this expander phy */ +struct domain_device *sas_ex_to_ata(struct domain_device *ex_dev, int phy_id) +{ + struct ex_phy *ex_phy = &ex_dev->ex_dev.ex_phy[phy_id]; + struct domain_device *dev; + struct sas_rphy *rphy; + + if (!ex_phy->port) + return NULL; + + rphy = ex_phy->port->rphy; + if (!rphy) + return NULL; + + dev = sas_find_dev_by_rphy(rphy); + + if (dev && dev_is_sata(dev)) + return dev; + + return NULL; +} + +#define DISCOVER_REQ_SIZE 16 +#define DISCOVER_RESP_SIZE 56 + +static int sas_ex_phy_discover_helper(struct domain_device *dev, u8 *disc_req, + u8 *disc_resp, int single) +{ + struct discover_resp *dr; + int res; + + disc_req[9] = single; + + res = smp_execute_task(dev, disc_req, DISCOVER_REQ_SIZE, + disc_resp, DISCOVER_RESP_SIZE); + if (res) + return res; + dr = &((struct smp_resp *)disc_resp)->disc; + if (memcmp(dev->sas_addr, dr->attached_sas_addr, SAS_ADDR_SIZE) == 0) { + pr_notice("Found loopback topology, just ignore it!\n"); + return 0; + } + sas_set_ex_phy(dev, single, disc_resp); + return 0; +} + +int sas_ex_phy_discover(struct domain_device *dev, int single) +{ + struct expander_device *ex = &dev->ex_dev; + int res = 0; + u8 *disc_req; + u8 *disc_resp; + + disc_req = alloc_smp_req(DISCOVER_REQ_SIZE); + if (!disc_req) + return -ENOMEM; + + disc_resp = alloc_smp_resp(DISCOVER_RESP_SIZE); + if (!disc_resp) { + kfree(disc_req); + return -ENOMEM; + } + + disc_req[1] = SMP_DISCOVER; + + if (0 <= single && single < ex->num_phys) { + res = sas_ex_phy_discover_helper(dev, disc_req, disc_resp, single); + } else { + int i; + + for (i = 0; i < ex->num_phys; i++) { + res = sas_ex_phy_discover_helper(dev, disc_req, + disc_resp, i); + if (res) + goto out_err; + } + } +out_err: + kfree(disc_resp); + kfree(disc_req); + return res; +} + +static int sas_expander_discover(struct domain_device *dev) +{ + struct expander_device *ex = &dev->ex_dev; + int res; + + ex->ex_phy = kcalloc(ex->num_phys, sizeof(*ex->ex_phy), GFP_KERNEL); + if (!ex->ex_phy) + return -ENOMEM; + + res = sas_ex_phy_discover(dev, -1); + if (res) + goto out_err; + + return 0; + out_err: + kfree(ex->ex_phy); + ex->ex_phy = NULL; + return res; +} + +#define MAX_EXPANDER_PHYS 128 + +static void ex_assign_report_general(struct domain_device *dev, + struct smp_resp *resp) +{ + struct report_general_resp *rg = &resp->rg; + + dev->ex_dev.ex_change_count = be16_to_cpu(rg->change_count); + dev->ex_dev.max_route_indexes = be16_to_cpu(rg->route_indexes); + dev->ex_dev.num_phys = min(rg->num_phys, (u8)MAX_EXPANDER_PHYS); + dev->ex_dev.t2t_supp = rg->t2t_supp; + dev->ex_dev.conf_route_table = rg->conf_route_table; + dev->ex_dev.configuring = rg->configuring; + memcpy(dev->ex_dev.enclosure_logical_id, rg->enclosure_logical_id, 8); +} + +#define RG_REQ_SIZE 8 +#define RG_RESP_SIZE 32 + +static int sas_ex_general(struct domain_device *dev) +{ + u8 *rg_req; + struct smp_resp *rg_resp; + int res; + int i; + + rg_req = alloc_smp_req(RG_REQ_SIZE); + if (!rg_req) + return -ENOMEM; + + rg_resp = alloc_smp_resp(RG_RESP_SIZE); + if (!rg_resp) { + kfree(rg_req); + return -ENOMEM; + } + + rg_req[1] = SMP_REPORT_GENERAL; + + for (i = 0; i < 5; i++) { + res = smp_execute_task(dev, rg_req, RG_REQ_SIZE, rg_resp, + RG_RESP_SIZE); + + if (res) { + pr_notice("RG to ex %016llx failed:0x%x\n", + SAS_ADDR(dev->sas_addr), res); + goto out; + } else if (rg_resp->result != SMP_RESP_FUNC_ACC) { + pr_debug("RG:ex %016llx returned SMP result:0x%x\n", + SAS_ADDR(dev->sas_addr), rg_resp->result); + res = rg_resp->result; + goto out; + } + + ex_assign_report_general(dev, rg_resp); + + if (dev->ex_dev.configuring) { + pr_debug("RG: ex %016llx self-configuring...\n", + SAS_ADDR(dev->sas_addr)); + schedule_timeout_interruptible(5*HZ); + } else + break; + } +out: + kfree(rg_req); + kfree(rg_resp); + return res; +} + +static void ex_assign_manuf_info(struct domain_device *dev, void + *_mi_resp) +{ + u8 *mi_resp = _mi_resp; + struct sas_rphy *rphy = dev->rphy; + struct sas_expander_device *edev = rphy_to_expander_device(rphy); + + memcpy(edev->vendor_id, mi_resp + 12, SAS_EXPANDER_VENDOR_ID_LEN); + memcpy(edev->product_id, mi_resp + 20, SAS_EXPANDER_PRODUCT_ID_LEN); + memcpy(edev->product_rev, mi_resp + 36, + SAS_EXPANDER_PRODUCT_REV_LEN); + + if (mi_resp[8] & 1) { + memcpy(edev->component_vendor_id, mi_resp + 40, + SAS_EXPANDER_COMPONENT_VENDOR_ID_LEN); + edev->component_id = mi_resp[48] << 8 | mi_resp[49]; + edev->component_revision_id = mi_resp[50]; + } +} + +#define MI_REQ_SIZE 8 +#define MI_RESP_SIZE 64 + +static int sas_ex_manuf_info(struct domain_device *dev) +{ + u8 *mi_req; + u8 *mi_resp; + int res; + + mi_req = alloc_smp_req(MI_REQ_SIZE); + if (!mi_req) + return -ENOMEM; + + mi_resp = alloc_smp_resp(MI_RESP_SIZE); + if (!mi_resp) { + kfree(mi_req); + return -ENOMEM; + } + + mi_req[1] = SMP_REPORT_MANUF_INFO; + + res = smp_execute_task(dev, mi_req, MI_REQ_SIZE, mi_resp,MI_RESP_SIZE); + if (res) { + pr_notice("MI: ex %016llx failed:0x%x\n", + SAS_ADDR(dev->sas_addr), res); + goto out; + } else if (mi_resp[2] != SMP_RESP_FUNC_ACC) { + pr_debug("MI ex %016llx returned SMP result:0x%x\n", + SAS_ADDR(dev->sas_addr), mi_resp[2]); + goto out; + } + + ex_assign_manuf_info(dev, mi_resp); +out: + kfree(mi_req); + kfree(mi_resp); + return res; +} + +#define PC_REQ_SIZE 44 +#define PC_RESP_SIZE 8 + +int sas_smp_phy_control(struct domain_device *dev, int phy_id, + enum phy_func phy_func, + struct sas_phy_linkrates *rates) +{ + u8 *pc_req; + u8 *pc_resp; + int res; + + pc_req = alloc_smp_req(PC_REQ_SIZE); + if (!pc_req) + return -ENOMEM; + + pc_resp = alloc_smp_resp(PC_RESP_SIZE); + if (!pc_resp) { + kfree(pc_req); + return -ENOMEM; + } + + pc_req[1] = SMP_PHY_CONTROL; + pc_req[9] = phy_id; + pc_req[10]= phy_func; + if (rates) { + pc_req[32] = rates->minimum_linkrate << 4; + pc_req[33] = rates->maximum_linkrate << 4; + } + + res = smp_execute_task(dev, pc_req, PC_REQ_SIZE, pc_resp,PC_RESP_SIZE); + if (res) { + pr_err("ex %016llx phy%02d PHY control failed: %d\n", + SAS_ADDR(dev->sas_addr), phy_id, res); + } else if (pc_resp[2] != SMP_RESP_FUNC_ACC) { + pr_err("ex %016llx phy%02d PHY control failed: function result 0x%x\n", + SAS_ADDR(dev->sas_addr), phy_id, pc_resp[2]); + res = pc_resp[2]; + } + kfree(pc_resp); + kfree(pc_req); + return res; +} + +static void sas_ex_disable_phy(struct domain_device *dev, int phy_id) +{ + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *phy = &ex->ex_phy[phy_id]; + + sas_smp_phy_control(dev, phy_id, PHY_FUNC_DISABLE, NULL); + phy->linkrate = SAS_PHY_DISABLED; +} + +static void sas_ex_disable_port(struct domain_device *dev, u8 *sas_addr) +{ + struct expander_device *ex = &dev->ex_dev; + int i; + + for (i = 0; i < ex->num_phys; i++) { + struct ex_phy *phy = &ex->ex_phy[i]; + + if (phy->phy_state == PHY_VACANT || + phy->phy_state == PHY_NOT_PRESENT) + continue; + + if (SAS_ADDR(phy->attached_sas_addr) == SAS_ADDR(sas_addr)) + sas_ex_disable_phy(dev, i); + } +} + +static int sas_dev_present_in_domain(struct asd_sas_port *port, + u8 *sas_addr) +{ + struct domain_device *dev; + + if (SAS_ADDR(port->sas_addr) == SAS_ADDR(sas_addr)) + return 1; + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + if (SAS_ADDR(dev->sas_addr) == SAS_ADDR(sas_addr)) + return 1; + } + return 0; +} + +#define RPEL_REQ_SIZE 16 +#define RPEL_RESP_SIZE 32 +int sas_smp_get_phy_events(struct sas_phy *phy) +{ + int res; + u8 *req; + u8 *resp; + struct sas_rphy *rphy = dev_to_rphy(phy->dev.parent); + struct domain_device *dev = sas_find_dev_by_rphy(rphy); + + req = alloc_smp_req(RPEL_REQ_SIZE); + if (!req) + return -ENOMEM; + + resp = alloc_smp_resp(RPEL_RESP_SIZE); + if (!resp) { + kfree(req); + return -ENOMEM; + } + + req[1] = SMP_REPORT_PHY_ERR_LOG; + req[9] = phy->number; + + res = smp_execute_task(dev, req, RPEL_REQ_SIZE, + resp, RPEL_RESP_SIZE); + + if (res) + goto out; + + phy->invalid_dword_count = get_unaligned_be32(&resp[12]); + phy->running_disparity_error_count = get_unaligned_be32(&resp[16]); + phy->loss_of_dword_sync_count = get_unaligned_be32(&resp[20]); + phy->phy_reset_problem_count = get_unaligned_be32(&resp[24]); + + out: + kfree(req); + kfree(resp); + return res; + +} + +#ifdef CONFIG_SCSI_SAS_ATA + +#define RPS_REQ_SIZE 16 +#define RPS_RESP_SIZE 60 + +int sas_get_report_phy_sata(struct domain_device *dev, int phy_id, + struct smp_resp *rps_resp) +{ + int res; + u8 *rps_req = alloc_smp_req(RPS_REQ_SIZE); + u8 *resp = (u8 *)rps_resp; + + if (!rps_req) + return -ENOMEM; + + rps_req[1] = SMP_REPORT_PHY_SATA; + rps_req[9] = phy_id; + + res = smp_execute_task(dev, rps_req, RPS_REQ_SIZE, + rps_resp, RPS_RESP_SIZE); + + /* 0x34 is the FIS type for the D2H fis. There's a potential + * standards cockup here. sas-2 explicitly specifies the FIS + * should be encoded so that FIS type is in resp[24]. + * However, some expanders endian reverse this. Undo the + * reversal here */ + if (!res && resp[27] == 0x34 && resp[24] != 0x34) { + int i; + + for (i = 0; i < 5; i++) { + int j = 24 + (i*4); + u8 a, b; + a = resp[j + 0]; + b = resp[j + 1]; + resp[j + 0] = resp[j + 3]; + resp[j + 1] = resp[j + 2]; + resp[j + 2] = b; + resp[j + 3] = a; + } + } + + kfree(rps_req); + return res; +} +#endif + +static void sas_ex_get_linkrate(struct domain_device *parent, + struct domain_device *child, + struct ex_phy *parent_phy) +{ + struct expander_device *parent_ex = &parent->ex_dev; + struct sas_port *port; + int i; + + child->pathways = 0; + + port = parent_phy->port; + + for (i = 0; i < parent_ex->num_phys; i++) { + struct ex_phy *phy = &parent_ex->ex_phy[i]; + + if (phy->phy_state == PHY_VACANT || + phy->phy_state == PHY_NOT_PRESENT) + continue; + + if (SAS_ADDR(phy->attached_sas_addr) == + SAS_ADDR(child->sas_addr)) { + + child->min_linkrate = min(parent->min_linkrate, + phy->linkrate); + child->max_linkrate = max(parent->max_linkrate, + phy->linkrate); + child->pathways++; + sas_port_add_phy(port, phy->phy); + } + } + child->linkrate = min(parent_phy->linkrate, child->max_linkrate); + child->pathways = min(child->pathways, parent->pathways); +} + +static struct domain_device *sas_ex_discover_end_dev( + struct domain_device *parent, int phy_id) +{ + struct expander_device *parent_ex = &parent->ex_dev; + struct ex_phy *phy = &parent_ex->ex_phy[phy_id]; + struct domain_device *child = NULL; + struct sas_rphy *rphy; + int res; + + if (phy->attached_sata_host || phy->attached_sata_ps) + return NULL; + + child = sas_alloc_device(); + if (!child) + return NULL; + + kref_get(&parent->kref); + child->parent = parent; + child->port = parent->port; + child->iproto = phy->attached_iproto; + memcpy(child->sas_addr, phy->attached_sas_addr, SAS_ADDR_SIZE); + sas_hash_addr(child->hashed_sas_addr, child->sas_addr); + if (!phy->port) { + phy->port = sas_port_alloc(&parent->rphy->dev, phy_id); + if (unlikely(!phy->port)) + goto out_err; + if (unlikely(sas_port_add(phy->port) != 0)) { + sas_port_free(phy->port); + goto out_err; + } + } + sas_ex_get_linkrate(parent, child, phy); + sas_device_set_phy(child, phy->port); + +#ifdef CONFIG_SCSI_SAS_ATA + if ((phy->attached_tproto & SAS_PROTOCOL_STP) || phy->attached_sata_dev) { + if (child->linkrate > parent->min_linkrate) { + struct sas_phy *cphy = child->phy; + enum sas_linkrate min_prate = cphy->minimum_linkrate, + parent_min_lrate = parent->min_linkrate, + min_linkrate = (min_prate > parent_min_lrate) ? + parent_min_lrate : 0; + struct sas_phy_linkrates rates = { + .maximum_linkrate = parent->min_linkrate, + .minimum_linkrate = min_linkrate, + }; + int ret; + + pr_notice("ex %016llx phy%02d SATA device linkrate > min pathway connection rate, attempting to lower device linkrate\n", + SAS_ADDR(child->sas_addr), phy_id); + ret = sas_smp_phy_control(parent, phy_id, + PHY_FUNC_LINK_RESET, &rates); + if (ret) { + pr_err("ex %016llx phy%02d SATA device could not set linkrate (%d)\n", + SAS_ADDR(child->sas_addr), phy_id, ret); + goto out_free; + } + pr_notice("ex %016llx phy%02d SATA device set linkrate successfully\n", + SAS_ADDR(child->sas_addr), phy_id); + child->linkrate = child->min_linkrate; + } + res = sas_get_ata_info(child, phy); + if (res) + goto out_free; + + sas_init_dev(child); + res = sas_ata_init(child); + if (res) + goto out_free; + rphy = sas_end_device_alloc(phy->port); + if (!rphy) + goto out_free; + rphy->identify.phy_identifier = phy_id; + + child->rphy = rphy; + get_device(&rphy->dev); + + list_add_tail(&child->disco_list_node, &parent->port->disco_list); + + res = sas_discover_sata(child); + if (res) { + pr_notice("sas_discover_sata() for device %16llx at %016llx:%02d returned 0x%x\n", + SAS_ADDR(child->sas_addr), + SAS_ADDR(parent->sas_addr), phy_id, res); + goto out_list_del; + } + } else +#endif + if (phy->attached_tproto & SAS_PROTOCOL_SSP) { + child->dev_type = SAS_END_DEVICE; + rphy = sas_end_device_alloc(phy->port); + /* FIXME: error handling */ + if (unlikely(!rphy)) + goto out_free; + child->tproto = phy->attached_tproto; + sas_init_dev(child); + + child->rphy = rphy; + get_device(&rphy->dev); + rphy->identify.phy_identifier = phy_id; + sas_fill_in_rphy(child, rphy); + + list_add_tail(&child->disco_list_node, &parent->port->disco_list); + + res = sas_discover_end_dev(child); + if (res) { + pr_notice("sas_discover_end_dev() for device %016llx at %016llx:%02d returned 0x%x\n", + SAS_ADDR(child->sas_addr), + SAS_ADDR(parent->sas_addr), phy_id, res); + goto out_list_del; + } + } else { + pr_notice("target proto 0x%x at %016llx:0x%x not handled\n", + phy->attached_tproto, SAS_ADDR(parent->sas_addr), + phy_id); + goto out_free; + } + + list_add_tail(&child->siblings, &parent_ex->children); + return child; + + out_list_del: + sas_rphy_free(child->rphy); + list_del(&child->disco_list_node); + spin_lock_irq(&parent->port->dev_list_lock); + list_del(&child->dev_list_node); + spin_unlock_irq(&parent->port->dev_list_lock); + out_free: + sas_port_delete(phy->port); + out_err: + phy->port = NULL; + sas_put_device(child); + return NULL; +} + +/* See if this phy is part of a wide port */ +static bool sas_ex_join_wide_port(struct domain_device *parent, int phy_id) +{ + struct ex_phy *phy = &parent->ex_dev.ex_phy[phy_id]; + int i; + + for (i = 0; i < parent->ex_dev.num_phys; i++) { + struct ex_phy *ephy = &parent->ex_dev.ex_phy[i]; + + if (ephy == phy) + continue; + + if (!memcmp(phy->attached_sas_addr, ephy->attached_sas_addr, + SAS_ADDR_SIZE) && ephy->port) { + sas_port_add_phy(ephy->port, phy->phy); + phy->port = ephy->port; + phy->phy_state = PHY_DEVICE_DISCOVERED; + return true; + } + } + + return false; +} + +static struct domain_device *sas_ex_discover_expander( + struct domain_device *parent, int phy_id) +{ + struct sas_expander_device *parent_ex = rphy_to_expander_device(parent->rphy); + struct ex_phy *phy = &parent->ex_dev.ex_phy[phy_id]; + struct domain_device *child = NULL; + struct sas_rphy *rphy; + struct sas_expander_device *edev; + struct asd_sas_port *port; + int res; + + if (phy->routing_attr == DIRECT_ROUTING) { + pr_warn("ex %016llx:%02d:D <--> ex %016llx:0x%x is not allowed\n", + SAS_ADDR(parent->sas_addr), phy_id, + SAS_ADDR(phy->attached_sas_addr), + phy->attached_phy_id); + return NULL; + } + child = sas_alloc_device(); + if (!child) + return NULL; + + phy->port = sas_port_alloc(&parent->rphy->dev, phy_id); + /* FIXME: better error handling */ + BUG_ON(sas_port_add(phy->port) != 0); + + + switch (phy->attached_dev_type) { + case SAS_EDGE_EXPANDER_DEVICE: + rphy = sas_expander_alloc(phy->port, + SAS_EDGE_EXPANDER_DEVICE); + break; + case SAS_FANOUT_EXPANDER_DEVICE: + rphy = sas_expander_alloc(phy->port, + SAS_FANOUT_EXPANDER_DEVICE); + break; + default: + rphy = NULL; /* shut gcc up */ + BUG(); + } + port = parent->port; + child->rphy = rphy; + get_device(&rphy->dev); + edev = rphy_to_expander_device(rphy); + child->dev_type = phy->attached_dev_type; + kref_get(&parent->kref); + child->parent = parent; + child->port = port; + child->iproto = phy->attached_iproto; + child->tproto = phy->attached_tproto; + memcpy(child->sas_addr, phy->attached_sas_addr, SAS_ADDR_SIZE); + sas_hash_addr(child->hashed_sas_addr, child->sas_addr); + sas_ex_get_linkrate(parent, child, phy); + edev->level = parent_ex->level + 1; + parent->port->disc.max_level = max(parent->port->disc.max_level, + edev->level); + sas_init_dev(child); + sas_fill_in_rphy(child, rphy); + sas_rphy_add(rphy); + + spin_lock_irq(&parent->port->dev_list_lock); + list_add_tail(&child->dev_list_node, &parent->port->dev_list); + spin_unlock_irq(&parent->port->dev_list_lock); + + res = sas_discover_expander(child); + if (res) { + sas_rphy_delete(rphy); + spin_lock_irq(&parent->port->dev_list_lock); + list_del(&child->dev_list_node); + spin_unlock_irq(&parent->port->dev_list_lock); + sas_put_device(child); + sas_port_delete(phy->port); + phy->port = NULL; + return NULL; + } + list_add_tail(&child->siblings, &parent->ex_dev.children); + return child; +} + +static int sas_ex_discover_dev(struct domain_device *dev, int phy_id) +{ + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *ex_phy = &ex->ex_phy[phy_id]; + struct domain_device *child = NULL; + int res = 0; + + /* Phy state */ + if (ex_phy->linkrate == SAS_SATA_SPINUP_HOLD) { + if (!sas_smp_phy_control(dev, phy_id, PHY_FUNC_LINK_RESET, NULL)) + res = sas_ex_phy_discover(dev, phy_id); + if (res) + return res; + } + + /* Parent and domain coherency */ + if (!dev->parent && (SAS_ADDR(ex_phy->attached_sas_addr) == + SAS_ADDR(dev->port->sas_addr))) { + sas_add_parent_port(dev, phy_id); + return 0; + } + if (dev->parent && (SAS_ADDR(ex_phy->attached_sas_addr) == + SAS_ADDR(dev->parent->sas_addr))) { + sas_add_parent_port(dev, phy_id); + if (ex_phy->routing_attr == TABLE_ROUTING) + sas_configure_phy(dev, phy_id, dev->port->sas_addr, 1); + return 0; + } + + if (sas_dev_present_in_domain(dev->port, ex_phy->attached_sas_addr)) + sas_ex_disable_port(dev, ex_phy->attached_sas_addr); + + if (ex_phy->attached_dev_type == SAS_PHY_UNUSED) { + if (ex_phy->routing_attr == DIRECT_ROUTING) { + memset(ex_phy->attached_sas_addr, 0, SAS_ADDR_SIZE); + sas_configure_routing(dev, ex_phy->attached_sas_addr); + } + return 0; + } else if (ex_phy->linkrate == SAS_LINK_RATE_UNKNOWN) + return 0; + + if (ex_phy->attached_dev_type != SAS_END_DEVICE && + ex_phy->attached_dev_type != SAS_FANOUT_EXPANDER_DEVICE && + ex_phy->attached_dev_type != SAS_EDGE_EXPANDER_DEVICE && + ex_phy->attached_dev_type != SAS_SATA_PENDING) { + pr_warn("unknown device type(0x%x) attached to ex %016llx phy%02d\n", + ex_phy->attached_dev_type, + SAS_ADDR(dev->sas_addr), + phy_id); + return 0; + } + + res = sas_configure_routing(dev, ex_phy->attached_sas_addr); + if (res) { + pr_notice("configure routing for dev %016llx reported 0x%x. Forgotten\n", + SAS_ADDR(ex_phy->attached_sas_addr), res); + sas_disable_routing(dev, ex_phy->attached_sas_addr); + return res; + } + + if (sas_ex_join_wide_port(dev, phy_id)) { + pr_debug("Attaching ex phy%02d to wide port %016llx\n", + phy_id, SAS_ADDR(ex_phy->attached_sas_addr)); + return res; + } + + switch (ex_phy->attached_dev_type) { + case SAS_END_DEVICE: + case SAS_SATA_PENDING: + child = sas_ex_discover_end_dev(dev, phy_id); + break; + case SAS_FANOUT_EXPANDER_DEVICE: + if (SAS_ADDR(dev->port->disc.fanout_sas_addr)) { + pr_debug("second fanout expander %016llx phy%02d attached to ex %016llx phy%02d\n", + SAS_ADDR(ex_phy->attached_sas_addr), + ex_phy->attached_phy_id, + SAS_ADDR(dev->sas_addr), + phy_id); + sas_ex_disable_phy(dev, phy_id); + return res; + } else + memcpy(dev->port->disc.fanout_sas_addr, + ex_phy->attached_sas_addr, SAS_ADDR_SIZE); + fallthrough; + case SAS_EDGE_EXPANDER_DEVICE: + child = sas_ex_discover_expander(dev, phy_id); + break; + default: + break; + } + + if (!child) + pr_notice("ex %016llx phy%02d failed to discover\n", + SAS_ADDR(dev->sas_addr), phy_id); + return res; +} + +static int sas_find_sub_addr(struct domain_device *dev, u8 *sub_addr) +{ + struct expander_device *ex = &dev->ex_dev; + int i; + + for (i = 0; i < ex->num_phys; i++) { + struct ex_phy *phy = &ex->ex_phy[i]; + + if (phy->phy_state == PHY_VACANT || + phy->phy_state == PHY_NOT_PRESENT) + continue; + + if (dev_is_expander(phy->attached_dev_type) && + phy->routing_attr == SUBTRACTIVE_ROUTING) { + + memcpy(sub_addr, phy->attached_sas_addr, SAS_ADDR_SIZE); + + return 1; + } + } + return 0; +} + +static int sas_check_level_subtractive_boundary(struct domain_device *dev) +{ + struct expander_device *ex = &dev->ex_dev; + struct domain_device *child; + u8 sub_addr[SAS_ADDR_SIZE] = {0, }; + + list_for_each_entry(child, &ex->children, siblings) { + if (!dev_is_expander(child->dev_type)) + continue; + if (sub_addr[0] == 0) { + sas_find_sub_addr(child, sub_addr); + continue; + } else { + u8 s2[SAS_ADDR_SIZE]; + + if (sas_find_sub_addr(child, s2) && + (SAS_ADDR(sub_addr) != SAS_ADDR(s2))) { + + pr_notice("ex %016llx->%016llx-?->%016llx diverges from subtractive boundary %016llx\n", + SAS_ADDR(dev->sas_addr), + SAS_ADDR(child->sas_addr), + SAS_ADDR(s2), + SAS_ADDR(sub_addr)); + + sas_ex_disable_port(child, s2); + } + } + } + return 0; +} +/** + * sas_ex_discover_devices - discover devices attached to this expander + * @dev: pointer to the expander domain device + * @single: if you want to do a single phy, else set to -1; + * + * Configure this expander for use with its devices and register the + * devices of this expander. + */ +static int sas_ex_discover_devices(struct domain_device *dev, int single) +{ + struct expander_device *ex = &dev->ex_dev; + int i = 0, end = ex->num_phys; + int res = 0; + + if (0 <= single && single < end) { + i = single; + end = i+1; + } + + for ( ; i < end; i++) { + struct ex_phy *ex_phy = &ex->ex_phy[i]; + + if (ex_phy->phy_state == PHY_VACANT || + ex_phy->phy_state == PHY_NOT_PRESENT || + ex_phy->phy_state == PHY_DEVICE_DISCOVERED) + continue; + + switch (ex_phy->linkrate) { + case SAS_PHY_DISABLED: + case SAS_PHY_RESET_PROBLEM: + case SAS_SATA_PORT_SELECTOR: + continue; + default: + res = sas_ex_discover_dev(dev, i); + if (res) + break; + continue; + } + } + + if (!res) + sas_check_level_subtractive_boundary(dev); + + return res; +} + +static int sas_check_ex_subtractive_boundary(struct domain_device *dev) +{ + struct expander_device *ex = &dev->ex_dev; + int i; + u8 *sub_sas_addr = NULL; + + if (dev->dev_type != SAS_EDGE_EXPANDER_DEVICE) + return 0; + + for (i = 0; i < ex->num_phys; i++) { + struct ex_phy *phy = &ex->ex_phy[i]; + + if (phy->phy_state == PHY_VACANT || + phy->phy_state == PHY_NOT_PRESENT) + continue; + + if (dev_is_expander(phy->attached_dev_type) && + phy->routing_attr == SUBTRACTIVE_ROUTING) { + + if (!sub_sas_addr) + sub_sas_addr = &phy->attached_sas_addr[0]; + else if (SAS_ADDR(sub_sas_addr) != + SAS_ADDR(phy->attached_sas_addr)) { + + pr_notice("ex %016llx phy%02d diverges(%016llx) on subtractive boundary(%016llx). Disabled\n", + SAS_ADDR(dev->sas_addr), i, + SAS_ADDR(phy->attached_sas_addr), + SAS_ADDR(sub_sas_addr)); + sas_ex_disable_phy(dev, i); + } + } + } + return 0; +} + +static void sas_print_parent_topology_bug(struct domain_device *child, + struct ex_phy *parent_phy, + struct ex_phy *child_phy) +{ + static const char *ex_type[] = { + [SAS_EDGE_EXPANDER_DEVICE] = "edge", + [SAS_FANOUT_EXPANDER_DEVICE] = "fanout", + }; + struct domain_device *parent = child->parent; + + pr_notice("%s ex %016llx phy%02d <--> %s ex %016llx phy%02d has %c:%c routing link!\n", + ex_type[parent->dev_type], + SAS_ADDR(parent->sas_addr), + parent_phy->phy_id, + + ex_type[child->dev_type], + SAS_ADDR(child->sas_addr), + child_phy->phy_id, + + sas_route_char(parent, parent_phy), + sas_route_char(child, child_phy)); +} + +static int sas_check_eeds(struct domain_device *child, + struct ex_phy *parent_phy, + struct ex_phy *child_phy) +{ + int res = 0; + struct domain_device *parent = child->parent; + + if (SAS_ADDR(parent->port->disc.fanout_sas_addr) != 0) { + res = -ENODEV; + pr_warn("edge ex %016llx phy S:%02d <--> edge ex %016llx phy S:%02d, while there is a fanout ex %016llx\n", + SAS_ADDR(parent->sas_addr), + parent_phy->phy_id, + SAS_ADDR(child->sas_addr), + child_phy->phy_id, + SAS_ADDR(parent->port->disc.fanout_sas_addr)); + } else if (SAS_ADDR(parent->port->disc.eeds_a) == 0) { + memcpy(parent->port->disc.eeds_a, parent->sas_addr, + SAS_ADDR_SIZE); + memcpy(parent->port->disc.eeds_b, child->sas_addr, + SAS_ADDR_SIZE); + } else if (((SAS_ADDR(parent->port->disc.eeds_a) == + SAS_ADDR(parent->sas_addr)) || + (SAS_ADDR(parent->port->disc.eeds_a) == + SAS_ADDR(child->sas_addr))) + && + ((SAS_ADDR(parent->port->disc.eeds_b) == + SAS_ADDR(parent->sas_addr)) || + (SAS_ADDR(parent->port->disc.eeds_b) == + SAS_ADDR(child->sas_addr)))) + ; + else { + res = -ENODEV; + pr_warn("edge ex %016llx phy%02d <--> edge ex %016llx phy%02d link forms a third EEDS!\n", + SAS_ADDR(parent->sas_addr), + parent_phy->phy_id, + SAS_ADDR(child->sas_addr), + child_phy->phy_id); + } + + return res; +} + +/* Here we spill over 80 columns. It is intentional. + */ +static int sas_check_parent_topology(struct domain_device *child) +{ + struct expander_device *child_ex = &child->ex_dev; + struct expander_device *parent_ex; + int i; + int res = 0; + + if (!child->parent) + return 0; + + if (!dev_is_expander(child->parent->dev_type)) + return 0; + + parent_ex = &child->parent->ex_dev; + + for (i = 0; i < parent_ex->num_phys; i++) { + struct ex_phy *parent_phy = &parent_ex->ex_phy[i]; + struct ex_phy *child_phy; + + if (parent_phy->phy_state == PHY_VACANT || + parent_phy->phy_state == PHY_NOT_PRESENT) + continue; + + if (SAS_ADDR(parent_phy->attached_sas_addr) != SAS_ADDR(child->sas_addr)) + continue; + + child_phy = &child_ex->ex_phy[parent_phy->attached_phy_id]; + + switch (child->parent->dev_type) { + case SAS_EDGE_EXPANDER_DEVICE: + if (child->dev_type == SAS_FANOUT_EXPANDER_DEVICE) { + if (parent_phy->routing_attr != SUBTRACTIVE_ROUTING || + child_phy->routing_attr != TABLE_ROUTING) { + sas_print_parent_topology_bug(child, parent_phy, child_phy); + res = -ENODEV; + } + } else if (parent_phy->routing_attr == SUBTRACTIVE_ROUTING) { + if (child_phy->routing_attr == SUBTRACTIVE_ROUTING) { + res = sas_check_eeds(child, parent_phy, child_phy); + } else if (child_phy->routing_attr != TABLE_ROUTING) { + sas_print_parent_topology_bug(child, parent_phy, child_phy); + res = -ENODEV; + } + } else if (parent_phy->routing_attr == TABLE_ROUTING) { + if (child_phy->routing_attr == SUBTRACTIVE_ROUTING || + (child_phy->routing_attr == TABLE_ROUTING && + child_ex->t2t_supp && parent_ex->t2t_supp)) { + /* All good */; + } else { + sas_print_parent_topology_bug(child, parent_phy, child_phy); + res = -ENODEV; + } + } + break; + case SAS_FANOUT_EXPANDER_DEVICE: + if (parent_phy->routing_attr != TABLE_ROUTING || + child_phy->routing_attr != SUBTRACTIVE_ROUTING) { + sas_print_parent_topology_bug(child, parent_phy, child_phy); + res = -ENODEV; + } + break; + default: + break; + } + } + + return res; +} + +#define RRI_REQ_SIZE 16 +#define RRI_RESP_SIZE 44 + +static int sas_configure_present(struct domain_device *dev, int phy_id, + u8 *sas_addr, int *index, int *present) +{ + int i, res = 0; + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *phy = &ex->ex_phy[phy_id]; + u8 *rri_req; + u8 *rri_resp; + + *present = 0; + *index = 0; + + rri_req = alloc_smp_req(RRI_REQ_SIZE); + if (!rri_req) + return -ENOMEM; + + rri_resp = alloc_smp_resp(RRI_RESP_SIZE); + if (!rri_resp) { + kfree(rri_req); + return -ENOMEM; + } + + rri_req[1] = SMP_REPORT_ROUTE_INFO; + rri_req[9] = phy_id; + + for (i = 0; i < ex->max_route_indexes ; i++) { + *(__be16 *)(rri_req+6) = cpu_to_be16(i); + res = smp_execute_task(dev, rri_req, RRI_REQ_SIZE, rri_resp, + RRI_RESP_SIZE); + if (res) + goto out; + res = rri_resp[2]; + if (res == SMP_RESP_NO_INDEX) { + pr_warn("overflow of indexes: dev %016llx phy%02d index 0x%x\n", + SAS_ADDR(dev->sas_addr), phy_id, i); + goto out; + } else if (res != SMP_RESP_FUNC_ACC) { + pr_notice("%s: dev %016llx phy%02d index 0x%x result 0x%x\n", + __func__, SAS_ADDR(dev->sas_addr), phy_id, + i, res); + goto out; + } + if (SAS_ADDR(sas_addr) != 0) { + if (SAS_ADDR(rri_resp+16) == SAS_ADDR(sas_addr)) { + *index = i; + if ((rri_resp[12] & 0x80) == 0x80) + *present = 0; + else + *present = 1; + goto out; + } else if (SAS_ADDR(rri_resp+16) == 0) { + *index = i; + *present = 0; + goto out; + } + } else if (SAS_ADDR(rri_resp+16) == 0 && + phy->last_da_index < i) { + phy->last_da_index = i; + *index = i; + *present = 0; + goto out; + } + } + res = -1; +out: + kfree(rri_req); + kfree(rri_resp); + return res; +} + +#define CRI_REQ_SIZE 44 +#define CRI_RESP_SIZE 8 + +static int sas_configure_set(struct domain_device *dev, int phy_id, + u8 *sas_addr, int index, int include) +{ + int res; + u8 *cri_req; + u8 *cri_resp; + + cri_req = alloc_smp_req(CRI_REQ_SIZE); + if (!cri_req) + return -ENOMEM; + + cri_resp = alloc_smp_resp(CRI_RESP_SIZE); + if (!cri_resp) { + kfree(cri_req); + return -ENOMEM; + } + + cri_req[1] = SMP_CONF_ROUTE_INFO; + *(__be16 *)(cri_req+6) = cpu_to_be16(index); + cri_req[9] = phy_id; + if (SAS_ADDR(sas_addr) == 0 || !include) + cri_req[12] |= 0x80; + memcpy(cri_req+16, sas_addr, SAS_ADDR_SIZE); + + res = smp_execute_task(dev, cri_req, CRI_REQ_SIZE, cri_resp, + CRI_RESP_SIZE); + if (res) + goto out; + res = cri_resp[2]; + if (res == SMP_RESP_NO_INDEX) { + pr_warn("overflow of indexes: dev %016llx phy%02d index 0x%x\n", + SAS_ADDR(dev->sas_addr), phy_id, index); + } +out: + kfree(cri_req); + kfree(cri_resp); + return res; +} + +static int sas_configure_phy(struct domain_device *dev, int phy_id, + u8 *sas_addr, int include) +{ + int index; + int present; + int res; + + res = sas_configure_present(dev, phy_id, sas_addr, &index, &present); + if (res) + return res; + if (include ^ present) + return sas_configure_set(dev, phy_id, sas_addr, index,include); + + return res; +} + +/** + * sas_configure_parent - configure routing table of parent + * @parent: parent expander + * @child: child expander + * @sas_addr: SAS port identifier of device directly attached to child + * @include: whether or not to include @child in the expander routing table + */ +static int sas_configure_parent(struct domain_device *parent, + struct domain_device *child, + u8 *sas_addr, int include) +{ + struct expander_device *ex_parent = &parent->ex_dev; + int res = 0; + int i; + + if (parent->parent) { + res = sas_configure_parent(parent->parent, parent, sas_addr, + include); + if (res) + return res; + } + + if (ex_parent->conf_route_table == 0) { + pr_debug("ex %016llx has self-configuring routing table\n", + SAS_ADDR(parent->sas_addr)); + return 0; + } + + for (i = 0; i < ex_parent->num_phys; i++) { + struct ex_phy *phy = &ex_parent->ex_phy[i]; + + if ((phy->routing_attr == TABLE_ROUTING) && + (SAS_ADDR(phy->attached_sas_addr) == + SAS_ADDR(child->sas_addr))) { + res = sas_configure_phy(parent, i, sas_addr, include); + if (res) + return res; + } + } + + return res; +} + +/** + * sas_configure_routing - configure routing + * @dev: expander device + * @sas_addr: port identifier of device directly attached to the expander device + */ +static int sas_configure_routing(struct domain_device *dev, u8 *sas_addr) +{ + if (dev->parent) + return sas_configure_parent(dev->parent, dev, sas_addr, 1); + return 0; +} + +static int sas_disable_routing(struct domain_device *dev, u8 *sas_addr) +{ + if (dev->parent) + return sas_configure_parent(dev->parent, dev, sas_addr, 0); + return 0; +} + +/** + * sas_discover_expander - expander discovery + * @dev: pointer to expander domain device + * + * See comment in sas_discover_sata(). + */ +static int sas_discover_expander(struct domain_device *dev) +{ + int res; + + res = sas_notify_lldd_dev_found(dev); + if (res) + return res; + + res = sas_ex_general(dev); + if (res) + goto out_err; + res = sas_ex_manuf_info(dev); + if (res) + goto out_err; + + res = sas_expander_discover(dev); + if (res) { + pr_warn("expander %016llx discovery failed(0x%x)\n", + SAS_ADDR(dev->sas_addr), res); + goto out_err; + } + + sas_check_ex_subtractive_boundary(dev); + res = sas_check_parent_topology(dev); + if (res) + goto out_err; + return 0; +out_err: + sas_notify_lldd_dev_gone(dev); + return res; +} + +static int sas_ex_level_discovery(struct asd_sas_port *port, const int level) +{ + int res = 0; + struct domain_device *dev; + + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + if (dev_is_expander(dev->dev_type)) { + struct sas_expander_device *ex = + rphy_to_expander_device(dev->rphy); + + if (level == ex->level) + res = sas_ex_discover_devices(dev, -1); + else if (level > 0) + res = sas_ex_discover_devices(port->port_dev, -1); + + } + } + + return res; +} + +static int sas_ex_bfs_disc(struct asd_sas_port *port) +{ + int res; + int level; + + do { + level = port->disc.max_level; + res = sas_ex_level_discovery(port, level); + mb(); + } while (level < port->disc.max_level); + + return res; +} + +int sas_discover_root_expander(struct domain_device *dev) +{ + int res; + struct sas_expander_device *ex = rphy_to_expander_device(dev->rphy); + + res = sas_rphy_add(dev->rphy); + if (res) + goto out_err; + + ex->level = dev->port->disc.max_level; /* 0 */ + res = sas_discover_expander(dev); + if (res) + goto out_err2; + + sas_ex_bfs_disc(dev->port); + + return res; + +out_err2: + sas_rphy_remove(dev->rphy); +out_err: + return res; +} + +/* ---------- Domain revalidation ---------- */ + +static int sas_get_phy_discover(struct domain_device *dev, + int phy_id, struct smp_resp *disc_resp) +{ + int res; + u8 *disc_req; + + disc_req = alloc_smp_req(DISCOVER_REQ_SIZE); + if (!disc_req) + return -ENOMEM; + + disc_req[1] = SMP_DISCOVER; + disc_req[9] = phy_id; + + res = smp_execute_task(dev, disc_req, DISCOVER_REQ_SIZE, + disc_resp, DISCOVER_RESP_SIZE); + if (res) + goto out; + else if (disc_resp->result != SMP_RESP_FUNC_ACC) { + res = disc_resp->result; + goto out; + } +out: + kfree(disc_req); + return res; +} + +static int sas_get_phy_change_count(struct domain_device *dev, + int phy_id, int *pcc) +{ + int res; + struct smp_resp *disc_resp; + + disc_resp = alloc_smp_resp(DISCOVER_RESP_SIZE); + if (!disc_resp) + return -ENOMEM; + + res = sas_get_phy_discover(dev, phy_id, disc_resp); + if (!res) + *pcc = disc_resp->disc.change_count; + + kfree(disc_resp); + return res; +} + +static int sas_get_phy_attached_dev(struct domain_device *dev, int phy_id, + u8 *sas_addr, enum sas_device_type *type) +{ + int res; + struct smp_resp *disc_resp; + struct discover_resp *dr; + + disc_resp = alloc_smp_resp(DISCOVER_RESP_SIZE); + if (!disc_resp) + return -ENOMEM; + dr = &disc_resp->disc; + + res = sas_get_phy_discover(dev, phy_id, disc_resp); + if (res == 0) { + memcpy(sas_addr, disc_resp->disc.attached_sas_addr, + SAS_ADDR_SIZE); + *type = to_dev_type(dr); + if (*type == 0) + memset(sas_addr, 0, SAS_ADDR_SIZE); + } + kfree(disc_resp); + return res; +} + +static int sas_find_bcast_phy(struct domain_device *dev, int *phy_id, + int from_phy, bool update) +{ + struct expander_device *ex = &dev->ex_dev; + int res = 0; + int i; + + for (i = from_phy; i < ex->num_phys; i++) { + int phy_change_count = 0; + + res = sas_get_phy_change_count(dev, i, &phy_change_count); + switch (res) { + case SMP_RESP_PHY_VACANT: + case SMP_RESP_NO_PHY: + continue; + case SMP_RESP_FUNC_ACC: + break; + default: + return res; + } + + if (phy_change_count != ex->ex_phy[i].phy_change_count) { + if (update) + ex->ex_phy[i].phy_change_count = + phy_change_count; + *phy_id = i; + return 0; + } + } + return 0; +} + +static int sas_get_ex_change_count(struct domain_device *dev, int *ecc) +{ + int res; + u8 *rg_req; + struct smp_resp *rg_resp; + + rg_req = alloc_smp_req(RG_REQ_SIZE); + if (!rg_req) + return -ENOMEM; + + rg_resp = alloc_smp_resp(RG_RESP_SIZE); + if (!rg_resp) { + kfree(rg_req); + return -ENOMEM; + } + + rg_req[1] = SMP_REPORT_GENERAL; + + res = smp_execute_task(dev, rg_req, RG_REQ_SIZE, rg_resp, + RG_RESP_SIZE); + if (res) + goto out; + if (rg_resp->result != SMP_RESP_FUNC_ACC) { + res = rg_resp->result; + goto out; + } + + *ecc = be16_to_cpu(rg_resp->rg.change_count); +out: + kfree(rg_resp); + kfree(rg_req); + return res; +} +/** + * sas_find_bcast_dev - find the device issue BROADCAST(CHANGE). + * @dev:domain device to be detect. + * @src_dev: the device which originated BROADCAST(CHANGE). + * + * Add self-configuration expander support. Suppose two expander cascading, + * when the first level expander is self-configuring, hotplug the disks in + * second level expander, BROADCAST(CHANGE) will not only be originated + * in the second level expander, but also be originated in the first level + * expander (see SAS protocol SAS 2r-14, 7.11 for detail), it is to say, + * expander changed count in two level expanders will all increment at least + * once, but the phy which chang count has changed is the source device which + * we concerned. + */ + +static int sas_find_bcast_dev(struct domain_device *dev, + struct domain_device **src_dev) +{ + struct expander_device *ex = &dev->ex_dev; + int ex_change_count = -1; + int phy_id = -1; + int res; + struct domain_device *ch; + + res = sas_get_ex_change_count(dev, &ex_change_count); + if (res) + goto out; + if (ex_change_count != -1 && ex_change_count != ex->ex_change_count) { + /* Just detect if this expander phys phy change count changed, + * in order to determine if this expander originate BROADCAST, + * and do not update phy change count field in our structure. + */ + res = sas_find_bcast_phy(dev, &phy_id, 0, false); + if (phy_id != -1) { + *src_dev = dev; + ex->ex_change_count = ex_change_count; + pr_info("ex %016llx phy%02d change count has changed\n", + SAS_ADDR(dev->sas_addr), phy_id); + return res; + } else + pr_info("ex %016llx phys DID NOT change\n", + SAS_ADDR(dev->sas_addr)); + } + list_for_each_entry(ch, &ex->children, siblings) { + if (dev_is_expander(ch->dev_type)) { + res = sas_find_bcast_dev(ch, src_dev); + if (*src_dev) + return res; + } + } +out: + return res; +} + +static void sas_unregister_ex_tree(struct asd_sas_port *port, struct domain_device *dev) +{ + struct expander_device *ex = &dev->ex_dev; + struct domain_device *child, *n; + + list_for_each_entry_safe(child, n, &ex->children, siblings) { + set_bit(SAS_DEV_GONE, &child->state); + if (dev_is_expander(child->dev_type)) + sas_unregister_ex_tree(port, child); + else + sas_unregister_dev(port, child); + } + sas_unregister_dev(port, dev); +} + +static void sas_unregister_devs_sas_addr(struct domain_device *parent, + int phy_id, bool last) +{ + struct expander_device *ex_dev = &parent->ex_dev; + struct ex_phy *phy = &ex_dev->ex_phy[phy_id]; + struct domain_device *child, *n, *found = NULL; + if (last) { + list_for_each_entry_safe(child, n, + &ex_dev->children, siblings) { + if (SAS_ADDR(child->sas_addr) == + SAS_ADDR(phy->attached_sas_addr)) { + set_bit(SAS_DEV_GONE, &child->state); + if (dev_is_expander(child->dev_type)) + sas_unregister_ex_tree(parent->port, child); + else + sas_unregister_dev(parent->port, child); + found = child; + break; + } + } + sas_disable_routing(parent, phy->attached_sas_addr); + } + memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE); + if (phy->port) { + sas_port_delete_phy(phy->port, phy->phy); + sas_device_set_phy(found, phy->port); + if (phy->port->num_phys == 0) + list_add_tail(&phy->port->del_list, + &parent->port->sas_port_del_list); + phy->port = NULL; + } +} + +static int sas_discover_bfs_by_root_level(struct domain_device *root, + const int level) +{ + struct expander_device *ex_root = &root->ex_dev; + struct domain_device *child; + int res = 0; + + list_for_each_entry(child, &ex_root->children, siblings) { + if (dev_is_expander(child->dev_type)) { + struct sas_expander_device *ex = + rphy_to_expander_device(child->rphy); + + if (level > ex->level) + res = sas_discover_bfs_by_root_level(child, + level); + else if (level == ex->level) + res = sas_ex_discover_devices(child, -1); + } + } + return res; +} + +static int sas_discover_bfs_by_root(struct domain_device *dev) +{ + int res; + struct sas_expander_device *ex = rphy_to_expander_device(dev->rphy); + int level = ex->level+1; + + res = sas_ex_discover_devices(dev, -1); + if (res) + goto out; + do { + res = sas_discover_bfs_by_root_level(dev, level); + mb(); + level += 1; + } while (level <= dev->port->disc.max_level); +out: + return res; +} + +static int sas_discover_new(struct domain_device *dev, int phy_id) +{ + struct ex_phy *ex_phy = &dev->ex_dev.ex_phy[phy_id]; + struct domain_device *child; + int res; + + pr_debug("ex %016llx phy%02d new device attached\n", + SAS_ADDR(dev->sas_addr), phy_id); + res = sas_ex_phy_discover(dev, phy_id); + if (res) + return res; + + if (sas_ex_join_wide_port(dev, phy_id)) + return 0; + + res = sas_ex_discover_devices(dev, phy_id); + if (res) + return res; + list_for_each_entry(child, &dev->ex_dev.children, siblings) { + if (SAS_ADDR(child->sas_addr) == + SAS_ADDR(ex_phy->attached_sas_addr)) { + if (dev_is_expander(child->dev_type)) + res = sas_discover_bfs_by_root(child); + break; + } + } + return res; +} + +static bool dev_type_flutter(enum sas_device_type new, enum sas_device_type old) +{ + if (old == new) + return true; + + /* treat device directed resets as flutter, if we went + * SAS_END_DEVICE to SAS_SATA_PENDING the link needs recovery + */ + if ((old == SAS_SATA_PENDING && new == SAS_END_DEVICE) || + (old == SAS_END_DEVICE && new == SAS_SATA_PENDING)) + return true; + + return false; +} + +static int sas_rediscover_dev(struct domain_device *dev, int phy_id, + bool last, int sibling) +{ + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *phy = &ex->ex_phy[phy_id]; + enum sas_device_type type = SAS_PHY_UNUSED; + u8 sas_addr[SAS_ADDR_SIZE]; + char msg[80] = ""; + int res; + + if (!last) + sprintf(msg, ", part of a wide port with phy%02d", sibling); + + pr_debug("ex %016llx rediscovering phy%02d%s\n", + SAS_ADDR(dev->sas_addr), phy_id, msg); + + memset(sas_addr, 0, SAS_ADDR_SIZE); + res = sas_get_phy_attached_dev(dev, phy_id, sas_addr, &type); + switch (res) { + case SMP_RESP_NO_PHY: + phy->phy_state = PHY_NOT_PRESENT; + sas_unregister_devs_sas_addr(dev, phy_id, last); + return res; + case SMP_RESP_PHY_VACANT: + phy->phy_state = PHY_VACANT; + sas_unregister_devs_sas_addr(dev, phy_id, last); + return res; + case SMP_RESP_FUNC_ACC: + break; + case -ECOMM: + break; + default: + return res; + } + + if ((SAS_ADDR(sas_addr) == 0) || (res == -ECOMM)) { + phy->phy_state = PHY_EMPTY; + sas_unregister_devs_sas_addr(dev, phy_id, last); + /* + * Even though the PHY is empty, for convenience we discover + * the PHY to update the PHY info, like negotiated linkrate. + */ + sas_ex_phy_discover(dev, phy_id); + return res; + } else if (SAS_ADDR(sas_addr) == SAS_ADDR(phy->attached_sas_addr) && + dev_type_flutter(type, phy->attached_dev_type)) { + struct domain_device *ata_dev = sas_ex_to_ata(dev, phy_id); + char *action = ""; + + sas_ex_phy_discover(dev, phy_id); + + if (ata_dev && phy->attached_dev_type == SAS_SATA_PENDING) + action = ", needs recovery"; + pr_debug("ex %016llx phy%02d broadcast flutter%s\n", + SAS_ADDR(dev->sas_addr), phy_id, action); + return res; + } + + /* we always have to delete the old device when we went here */ + pr_info("ex %016llx phy%02d replace %016llx\n", + SAS_ADDR(dev->sas_addr), phy_id, + SAS_ADDR(phy->attached_sas_addr)); + sas_unregister_devs_sas_addr(dev, phy_id, last); + + return sas_discover_new(dev, phy_id); +} + +/** + * sas_rediscover - revalidate the domain. + * @dev:domain device to be detect. + * @phy_id: the phy id will be detected. + * + * NOTE: this process _must_ quit (return) as soon as any connection + * errors are encountered. Connection recovery is done elsewhere. + * Discover process only interrogates devices in order to discover the + * domain.For plugging out, we un-register the device only when it is + * the last phy in the port, for other phys in this port, we just delete it + * from the port.For inserting, we do discovery when it is the + * first phy,for other phys in this port, we add it to the port to + * forming the wide-port. + */ +static int sas_rediscover(struct domain_device *dev, const int phy_id) +{ + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *changed_phy = &ex->ex_phy[phy_id]; + int res = 0; + int i; + bool last = true; /* is this the last phy of the port */ + + pr_debug("ex %016llx phy%02d originated BROADCAST(CHANGE)\n", + SAS_ADDR(dev->sas_addr), phy_id); + + if (SAS_ADDR(changed_phy->attached_sas_addr) != 0) { + for (i = 0; i < ex->num_phys; i++) { + struct ex_phy *phy = &ex->ex_phy[i]; + + if (i == phy_id) + continue; + if (SAS_ADDR(phy->attached_sas_addr) == + SAS_ADDR(changed_phy->attached_sas_addr)) { + last = false; + break; + } + } + res = sas_rediscover_dev(dev, phy_id, last, i); + } else + res = sas_discover_new(dev, phy_id); + return res; +} + +/** + * sas_ex_revalidate_domain - revalidate the domain + * @port_dev: port domain device. + * + * NOTE: this process _must_ quit (return) as soon as any connection + * errors are encountered. Connection recovery is done elsewhere. + * Discover process only interrogates devices in order to discover the + * domain. + */ +int sas_ex_revalidate_domain(struct domain_device *port_dev) +{ + int res; + struct domain_device *dev = NULL; + + res = sas_find_bcast_dev(port_dev, &dev); + if (res == 0 && dev) { + struct expander_device *ex = &dev->ex_dev; + int i = 0, phy_id; + + do { + phy_id = -1; + res = sas_find_bcast_phy(dev, &phy_id, i, true); + if (phy_id == -1) + break; + res = sas_rediscover(dev, phy_id); + i = phy_id + 1; + } while (i < ex->num_phys); + } + return res; +} + +void sas_smp_handler(struct bsg_job *job, struct Scsi_Host *shost, + struct sas_rphy *rphy) +{ + struct domain_device *dev; + unsigned int rcvlen = 0; + int ret = -EINVAL; + + /* no rphy means no smp target support (ie aic94xx host) */ + if (!rphy) + return sas_smp_host_handler(job, shost); + + switch (rphy->identify.device_type) { + case SAS_EDGE_EXPANDER_DEVICE: + case SAS_FANOUT_EXPANDER_DEVICE: + break; + default: + pr_err("%s: can we send a smp request to a device?\n", + __func__); + goto out; + } + + dev = sas_find_dev_by_rphy(rphy); + if (!dev) { + pr_err("%s: fail to find a domain_device?\n", __func__); + goto out; + } + + /* do we need to support multiple segments? */ + if (job->request_payload.sg_cnt > 1 || + job->reply_payload.sg_cnt > 1) { + pr_info("%s: multiple segments req %u, rsp %u\n", + __func__, job->request_payload.payload_len, + job->reply_payload.payload_len); + goto out; + } + + ret = smp_execute_task_sg(dev, job->request_payload.sg_list, + job->reply_payload.sg_list); + if (ret >= 0) { + /* bsg_job_done() requires the length received */ + rcvlen = job->reply_payload.payload_len - ret; + ret = 0; + } + +out: + bsg_job_done(job, ret, rcvlen); +} diff --git a/drivers/scsi/libsas/sas_host_smp.c b/drivers/scsi/libsas/sas_host_smp.c new file mode 100644 index 000000000..eca2a6bf3 --- /dev/null +++ b/drivers/scsi/libsas/sas_host_smp.c @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Serial Attached SCSI (SAS) Expander discovery and configuration + * + * Copyright (C) 2007 James E.J. Bottomley + * <James.Bottomley@HansenPartnership.com> + */ +#include <linux/scatterlist.h> +#include <linux/blkdev.h> +#include <linux/slab.h> +#include <linux/export.h> + +#include "sas_internal.h" + +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include "../scsi_sas_internal.h" + +static void sas_host_smp_discover(struct sas_ha_struct *sas_ha, u8 *resp_data, + u8 phy_id) +{ + struct sas_phy *phy; + struct sas_rphy *rphy; + + if (phy_id >= sas_ha->num_phys) { + resp_data[2] = SMP_RESP_NO_PHY; + return; + } + resp_data[2] = SMP_RESP_FUNC_ACC; + + phy = sas_ha->sas_phy[phy_id]->phy; + resp_data[9] = phy_id; + resp_data[13] = phy->negotiated_linkrate; + memcpy(resp_data + 16, sas_ha->sas_addr, SAS_ADDR_SIZE); + memcpy(resp_data + 24, sas_ha->sas_phy[phy_id]->attached_sas_addr, + SAS_ADDR_SIZE); + resp_data[40] = (phy->minimum_linkrate << 4) | + phy->minimum_linkrate_hw; + resp_data[41] = (phy->maximum_linkrate << 4) | + phy->maximum_linkrate_hw; + + if (!sas_ha->sas_phy[phy_id]->port || + !sas_ha->sas_phy[phy_id]->port->port_dev) + return; + + rphy = sas_ha->sas_phy[phy_id]->port->port_dev->rphy; + resp_data[12] = rphy->identify.device_type << 4; + resp_data[14] = rphy->identify.initiator_port_protocols; + resp_data[15] = rphy->identify.target_port_protocols; +} + +/** + * to_sas_gpio_gp_bit - given the gpio frame data find the byte/bit position of 'od' + * @od: od bit to find + * @data: incoming bitstream (from frame) + * @index: requested data register index (from frame) + * @count: total number of registers in the bitstream (from frame) + * @bit: bit position of 'od' in the returned byte + * + * returns NULL if 'od' is not in 'data' + * + * From SFF-8485 v0.7: + * "In GPIO_TX[1], bit 0 of byte 3 contains the first bit (i.e., OD0.0) + * and bit 7 of byte 0 contains the 32nd bit (i.e., OD10.1). + * + * In GPIO_TX[2], bit 0 of byte 3 contains the 33rd bit (i.e., OD10.2) + * and bit 7 of byte 0 contains the 64th bit (i.e., OD21.0)." + * + * The general-purpose (raw-bitstream) RX registers have the same layout + * although 'od' is renamed 'id' for 'input data'. + * + * SFF-8489 defines the behavior of the LEDs in response to the 'od' values. + */ +static u8 *to_sas_gpio_gp_bit(unsigned int od, u8 *data, u8 index, u8 count, u8 *bit) +{ + unsigned int reg; + u8 byte; + + /* gp registers start at index 1 */ + if (index == 0) + return NULL; + + index--; /* make index 0-based */ + if (od < index * 32) + return NULL; + + od -= index * 32; + reg = od >> 5; + + if (reg >= count) + return NULL; + + od &= (1 << 5) - 1; + byte = 3 - (od >> 3); + *bit = od & ((1 << 3) - 1); + + return &data[reg * 4 + byte]; +} + +int try_test_sas_gpio_gp_bit(unsigned int od, u8 *data, u8 index, u8 count) +{ + u8 *byte; + u8 bit; + + byte = to_sas_gpio_gp_bit(od, data, index, count, &bit); + if (!byte) + return -1; + + return (*byte >> bit) & 1; +} +EXPORT_SYMBOL(try_test_sas_gpio_gp_bit); + +static int sas_host_smp_write_gpio(struct sas_ha_struct *sas_ha, u8 *resp_data, + u8 reg_type, u8 reg_index, u8 reg_count, + u8 *req_data) +{ + struct sas_internal *i = to_sas_internal(sas_ha->core.shost->transportt); + int written; + + if (i->dft->lldd_write_gpio == NULL) { + resp_data[2] = SMP_RESP_FUNC_UNK; + return 0; + } + + written = i->dft->lldd_write_gpio(sas_ha, reg_type, reg_index, + reg_count, req_data); + + if (written < 0) { + resp_data[2] = SMP_RESP_FUNC_FAILED; + written = 0; + } else + resp_data[2] = SMP_RESP_FUNC_ACC; + + return written; +} + +static void sas_report_phy_sata(struct sas_ha_struct *sas_ha, u8 *resp_data, + u8 phy_id) +{ + struct sas_rphy *rphy; + struct dev_to_host_fis *fis; + int i; + + if (phy_id >= sas_ha->num_phys) { + resp_data[2] = SMP_RESP_NO_PHY; + return; + } + + resp_data[2] = SMP_RESP_PHY_NO_SATA; + + if (!sas_ha->sas_phy[phy_id]->port) + return; + + rphy = sas_ha->sas_phy[phy_id]->port->port_dev->rphy; + fis = (struct dev_to_host_fis *) + sas_ha->sas_phy[phy_id]->port->port_dev->frame_rcvd; + if (rphy->identify.target_port_protocols != SAS_PROTOCOL_SATA) + return; + + resp_data[2] = SMP_RESP_FUNC_ACC; + resp_data[9] = phy_id; + memcpy(resp_data + 16, sas_ha->sas_phy[phy_id]->attached_sas_addr, + SAS_ADDR_SIZE); + + /* check to see if we have a valid d2h fis */ + if (fis->fis_type != 0x34) + return; + + /* the d2h fis is required by the standard to be in LE format */ + for (i = 0; i < 20; i += 4) { + u8 *dst = resp_data + 24 + i, *src = + &sas_ha->sas_phy[phy_id]->port->port_dev->frame_rcvd[i]; + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; + } +} + +static void sas_phy_control(struct sas_ha_struct *sas_ha, u8 phy_id, + u8 phy_op, enum sas_linkrate min, + enum sas_linkrate max, u8 *resp_data) +{ + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + struct sas_phy_linkrates rates; + struct asd_sas_phy *asd_phy; + + if (phy_id >= sas_ha->num_phys) { + resp_data[2] = SMP_RESP_NO_PHY; + return; + } + + asd_phy = sas_ha->sas_phy[phy_id]; + switch (phy_op) { + case PHY_FUNC_NOP: + case PHY_FUNC_LINK_RESET: + case PHY_FUNC_HARD_RESET: + case PHY_FUNC_DISABLE: + case PHY_FUNC_CLEAR_ERROR_LOG: + case PHY_FUNC_CLEAR_AFFIL: + case PHY_FUNC_TX_SATA_PS_SIGNAL: + break; + + default: + resp_data[2] = SMP_RESP_PHY_UNK_OP; + return; + } + + rates.minimum_linkrate = min; + rates.maximum_linkrate = max; + + /* filter reset requests through libata eh */ + if (phy_op == PHY_FUNC_LINK_RESET && sas_try_ata_reset(asd_phy) == 0) { + resp_data[2] = SMP_RESP_FUNC_ACC; + return; + } + + if (i->dft->lldd_control_phy(asd_phy, phy_op, &rates)) + resp_data[2] = SMP_RESP_FUNC_FAILED; + else + resp_data[2] = SMP_RESP_FUNC_ACC; +} + +void sas_smp_host_handler(struct bsg_job *job, struct Scsi_Host *shost) +{ + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + u8 *req_data, *resp_data; + unsigned int reslen = 0; + int error = -EINVAL; + + /* eight is the minimum size for request and response frames */ + if (job->request_payload.payload_len < 8 || + job->reply_payload.payload_len < 8) + goto out; + + error = -ENOMEM; + req_data = kzalloc(job->request_payload.payload_len, GFP_KERNEL); + if (!req_data) + goto out; + sg_copy_to_buffer(job->request_payload.sg_list, + job->request_payload.sg_cnt, req_data, + job->request_payload.payload_len); + + /* make sure frame can always be built ... we copy + * back only the requested length */ + resp_data = kzalloc(max(job->reply_payload.payload_len, 128U), + GFP_KERNEL); + if (!resp_data) + goto out_free_req; + + error = -EINVAL; + if (req_data[0] != SMP_REQUEST) + goto out_free_resp; + + /* set up default don't know response */ + resp_data[0] = SMP_RESPONSE; + resp_data[1] = req_data[1]; + resp_data[2] = SMP_RESP_FUNC_UNK; + + switch (req_data[1]) { + case SMP_REPORT_GENERAL: + resp_data[2] = SMP_RESP_FUNC_ACC; + resp_data[9] = sas_ha->num_phys; + reslen = 32; + break; + + case SMP_REPORT_MANUF_INFO: + resp_data[2] = SMP_RESP_FUNC_ACC; + memcpy(resp_data + 12, shost->hostt->name, + SAS_EXPANDER_VENDOR_ID_LEN); + memcpy(resp_data + 20, "libsas virt phy", + SAS_EXPANDER_PRODUCT_ID_LEN); + reslen = 64; + break; + + case SMP_READ_GPIO_REG: + /* FIXME: need GPIO support in the transport class */ + break; + + case SMP_DISCOVER: + if (job->request_payload.payload_len < 16) + goto out_free_resp; + sas_host_smp_discover(sas_ha, resp_data, req_data[9]); + reslen = 56; + break; + + case SMP_REPORT_PHY_ERR_LOG: + /* FIXME: could implement this with additional + * libsas callbacks providing the HW supports it */ + break; + + case SMP_REPORT_PHY_SATA: + if (job->request_payload.payload_len < 16) + goto out_free_resp; + sas_report_phy_sata(sas_ha, resp_data, req_data[9]); + reslen = 60; + break; + + case SMP_REPORT_ROUTE_INFO: + /* Can't implement; hosts have no routes */ + break; + + case SMP_WRITE_GPIO_REG: { + /* SFF-8485 v0.7 */ + const int base_frame_size = 11; + int to_write = req_data[4]; + + if (job->request_payload.payload_len < + base_frame_size + to_write * 4) { + resp_data[2] = SMP_RESP_INV_FRM_LEN; + break; + } + + to_write = sas_host_smp_write_gpio(sas_ha, resp_data, req_data[2], + req_data[3], to_write, &req_data[8]); + reslen = 8; + break; + } + + case SMP_CONF_ROUTE_INFO: + /* Can't implement; hosts have no routes */ + break; + + case SMP_PHY_CONTROL: + if (job->request_payload.payload_len < 44) + goto out_free_resp; + sas_phy_control(sas_ha, req_data[9], req_data[10], + req_data[32] >> 4, req_data[33] >> 4, + resp_data); + reslen = 8; + break; + + case SMP_PHY_TEST_FUNCTION: + /* FIXME: should this be implemented? */ + break; + + default: + /* probably a 2.0 function */ + break; + } + + sg_copy_from_buffer(job->reply_payload.sg_list, + job->reply_payload.sg_cnt, resp_data, + job->reply_payload.payload_len); + + error = 0; +out_free_resp: + kfree(resp_data); +out_free_req: + kfree(req_data); +out: + bsg_job_done(job, error, reslen); +} diff --git a/drivers/scsi/libsas/sas_init.c b/drivers/scsi/libsas/sas_init.c new file mode 100644 index 000000000..f8ae1f0f1 --- /dev/null +++ b/drivers/scsi/libsas/sas_init.c @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Serial Attached SCSI (SAS) Transport Layer initialization + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/spinlock.h> +#include <scsi/sas_ata.h> +#include <scsi/scsi_host.h> +#include <scsi/scsi_device.h> +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> + +#include "sas_internal.h" + +#include "../scsi_sas_internal.h" + +static struct kmem_cache *sas_task_cache; +static struct kmem_cache *sas_event_cache; + +struct sas_task *sas_alloc_task(gfp_t flags) +{ + struct sas_task *task = kmem_cache_zalloc(sas_task_cache, flags); + + if (task) { + spin_lock_init(&task->task_state_lock); + task->task_state_flags = SAS_TASK_STATE_PENDING; + } + + return task; +} +EXPORT_SYMBOL_GPL(sas_alloc_task); + +struct sas_task *sas_alloc_slow_task(gfp_t flags) +{ + struct sas_task *task = sas_alloc_task(flags); + struct sas_task_slow *slow = kmalloc(sizeof(*slow), flags); + + if (!task || !slow) { + if (task) + kmem_cache_free(sas_task_cache, task); + kfree(slow); + return NULL; + } + + task->slow_task = slow; + slow->task = task; + timer_setup(&slow->timer, NULL, 0); + init_completion(&slow->completion); + + return task; +} +EXPORT_SYMBOL_GPL(sas_alloc_slow_task); + +void sas_free_task(struct sas_task *task) +{ + if (task) { + kfree(task->slow_task); + kmem_cache_free(sas_task_cache, task); + } +} +EXPORT_SYMBOL_GPL(sas_free_task); + +/*------------ SAS addr hash -----------*/ +void sas_hash_addr(u8 *hashed, const u8 *sas_addr) +{ + const u32 poly = 0x00DB2777; + u32 r = 0; + int i; + + for (i = 0; i < SAS_ADDR_SIZE; i++) { + int b; + + for (b = (SAS_ADDR_SIZE - 1); b >= 0; b--) { + r <<= 1; + if ((1 << b) & sas_addr[i]) { + if (!(r & 0x01000000)) + r ^= poly; + } else if (r & 0x01000000) { + r ^= poly; + } + } + } + + hashed[0] = (r >> 16) & 0xFF; + hashed[1] = (r >> 8) & 0xFF; + hashed[2] = r & 0xFF; +} + +int sas_register_ha(struct sas_ha_struct *sas_ha) +{ + char name[64]; + int error = 0; + + mutex_init(&sas_ha->disco_mutex); + spin_lock_init(&sas_ha->phy_port_lock); + sas_hash_addr(sas_ha->hashed_sas_addr, sas_ha->sas_addr); + + set_bit(SAS_HA_REGISTERED, &sas_ha->state); + spin_lock_init(&sas_ha->lock); + mutex_init(&sas_ha->drain_mutex); + init_waitqueue_head(&sas_ha->eh_wait_q); + INIT_LIST_HEAD(&sas_ha->defer_q); + INIT_LIST_HEAD(&sas_ha->eh_dev_q); + + sas_ha->event_thres = SAS_PHY_SHUTDOWN_THRES; + + error = sas_register_phys(sas_ha); + if (error) { + pr_notice("couldn't register sas phys:%d\n", error); + return error; + } + + error = sas_register_ports(sas_ha); + if (error) { + pr_notice("couldn't register sas ports:%d\n", error); + goto Undo_phys; + } + + error = -ENOMEM; + snprintf(name, sizeof(name), "%s_event_q", dev_name(sas_ha->dev)); + sas_ha->event_q = create_singlethread_workqueue(name); + if (!sas_ha->event_q) + goto Undo_ports; + + snprintf(name, sizeof(name), "%s_disco_q", dev_name(sas_ha->dev)); + sas_ha->disco_q = create_singlethread_workqueue(name); + if (!sas_ha->disco_q) + goto Undo_event_q; + + INIT_LIST_HEAD(&sas_ha->eh_done_q); + INIT_LIST_HEAD(&sas_ha->eh_ata_q); + + return 0; + +Undo_event_q: + destroy_workqueue(sas_ha->event_q); +Undo_ports: + sas_unregister_ports(sas_ha); +Undo_phys: + + return error; +} + +static void sas_disable_events(struct sas_ha_struct *sas_ha) +{ + /* Set the state to unregistered to avoid further unchained + * events to be queued, and flush any in-progress drainers + */ + mutex_lock(&sas_ha->drain_mutex); + spin_lock_irq(&sas_ha->lock); + clear_bit(SAS_HA_REGISTERED, &sas_ha->state); + spin_unlock_irq(&sas_ha->lock); + __sas_drain_work(sas_ha); + mutex_unlock(&sas_ha->drain_mutex); +} + +int sas_unregister_ha(struct sas_ha_struct *sas_ha) +{ + sas_disable_events(sas_ha); + sas_unregister_ports(sas_ha); + + /* flush unregistration work */ + mutex_lock(&sas_ha->drain_mutex); + __sas_drain_work(sas_ha); + mutex_unlock(&sas_ha->drain_mutex); + + destroy_workqueue(sas_ha->disco_q); + destroy_workqueue(sas_ha->event_q); + + return 0; +} + +static int sas_get_linkerrors(struct sas_phy *phy) +{ + if (scsi_is_sas_phy_local(phy)) { + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + struct asd_sas_phy *asd_phy = sas_ha->sas_phy[phy->number]; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + return i->dft->lldd_control_phy(asd_phy, PHY_FUNC_GET_EVENTS, NULL); + } + + return sas_smp_get_phy_events(phy); +} + +int sas_try_ata_reset(struct asd_sas_phy *asd_phy) +{ + struct domain_device *dev = NULL; + + /* try to route user requested link resets through libata */ + if (asd_phy->port) + dev = asd_phy->port->port_dev; + + /* validate that dev has been probed */ + if (dev) + dev = sas_find_dev_by_rphy(dev->rphy); + + if (dev && dev_is_sata(dev)) { + sas_ata_schedule_reset(dev); + sas_ata_wait_eh(dev); + return 0; + } + + return -ENODEV; +} + +/* + * transport_sas_phy_reset - reset a phy and permit libata to manage the link + * + * phy reset request via sysfs in host workqueue context so we know we + * can block on eh and safely traverse the domain_device topology + */ +static int transport_sas_phy_reset(struct sas_phy *phy, int hard_reset) +{ + enum phy_func reset_type; + + if (hard_reset) + reset_type = PHY_FUNC_HARD_RESET; + else + reset_type = PHY_FUNC_LINK_RESET; + + if (scsi_is_sas_phy_local(phy)) { + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + struct asd_sas_phy *asd_phy = sas_ha->sas_phy[phy->number]; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + if (!hard_reset && sas_try_ata_reset(asd_phy) == 0) + return 0; + return i->dft->lldd_control_phy(asd_phy, reset_type, NULL); + } else { + struct sas_rphy *rphy = dev_to_rphy(phy->dev.parent); + struct domain_device *ddev = sas_find_dev_by_rphy(rphy); + struct domain_device *ata_dev = sas_ex_to_ata(ddev, phy->number); + + if (ata_dev && !hard_reset) { + sas_ata_schedule_reset(ata_dev); + sas_ata_wait_eh(ata_dev); + return 0; + } else + return sas_smp_phy_control(ddev, phy->number, reset_type, NULL); + } +} + +static int sas_phy_enable(struct sas_phy *phy, int enable) +{ + int ret; + enum phy_func cmd; + + if (enable) + cmd = PHY_FUNC_LINK_RESET; + else + cmd = PHY_FUNC_DISABLE; + + if (scsi_is_sas_phy_local(phy)) { + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + struct asd_sas_phy *asd_phy = sas_ha->sas_phy[phy->number]; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + if (enable) + ret = transport_sas_phy_reset(phy, 0); + else + ret = i->dft->lldd_control_phy(asd_phy, cmd, NULL); + } else { + struct sas_rphy *rphy = dev_to_rphy(phy->dev.parent); + struct domain_device *ddev = sas_find_dev_by_rphy(rphy); + + if (enable) + ret = transport_sas_phy_reset(phy, 0); + else + ret = sas_smp_phy_control(ddev, phy->number, cmd, NULL); + } + return ret; +} + +int sas_phy_reset(struct sas_phy *phy, int hard_reset) +{ + int ret; + enum phy_func reset_type; + + if (!phy->enabled) + return -ENODEV; + + if (hard_reset) + reset_type = PHY_FUNC_HARD_RESET; + else + reset_type = PHY_FUNC_LINK_RESET; + + if (scsi_is_sas_phy_local(phy)) { + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + struct asd_sas_phy *asd_phy = sas_ha->sas_phy[phy->number]; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + ret = i->dft->lldd_control_phy(asd_phy, reset_type, NULL); + } else { + struct sas_rphy *rphy = dev_to_rphy(phy->dev.parent); + struct domain_device *ddev = sas_find_dev_by_rphy(rphy); + ret = sas_smp_phy_control(ddev, phy->number, reset_type, NULL); + } + return ret; +} + +int sas_set_phy_speed(struct sas_phy *phy, + struct sas_phy_linkrates *rates) +{ + int ret; + + if ((rates->minimum_linkrate && + rates->minimum_linkrate > phy->maximum_linkrate) || + (rates->maximum_linkrate && + rates->maximum_linkrate < phy->minimum_linkrate)) + return -EINVAL; + + if (rates->minimum_linkrate && + rates->minimum_linkrate < phy->minimum_linkrate_hw) + rates->minimum_linkrate = phy->minimum_linkrate_hw; + + if (rates->maximum_linkrate && + rates->maximum_linkrate > phy->maximum_linkrate_hw) + rates->maximum_linkrate = phy->maximum_linkrate_hw; + + if (scsi_is_sas_phy_local(phy)) { + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(shost); + struct asd_sas_phy *asd_phy = sas_ha->sas_phy[phy->number]; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + ret = i->dft->lldd_control_phy(asd_phy, PHY_FUNC_SET_LINK_RATE, + rates); + } else { + struct sas_rphy *rphy = dev_to_rphy(phy->dev.parent); + struct domain_device *ddev = sas_find_dev_by_rphy(rphy); + ret = sas_smp_phy_control(ddev, phy->number, + PHY_FUNC_LINK_RESET, rates); + + } + + return ret; +} + +void sas_prep_resume_ha(struct sas_ha_struct *ha) +{ + int i; + + set_bit(SAS_HA_REGISTERED, &ha->state); + + /* clear out any stale link events/data from the suspension path */ + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_phy *phy = ha->sas_phy[i]; + + memset(phy->attached_sas_addr, 0, SAS_ADDR_SIZE); + phy->frame_rcvd_size = 0; + } +} +EXPORT_SYMBOL(sas_prep_resume_ha); + +static int phys_suspended(struct sas_ha_struct *ha) +{ + int i, rc = 0; + + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_phy *phy = ha->sas_phy[i]; + + if (phy->suspended) + rc++; + } + + return rc; +} + +void sas_resume_ha(struct sas_ha_struct *ha) +{ + const unsigned long tmo = msecs_to_jiffies(25000); + int i; + + /* deform ports on phys that did not resume + * at this point we may be racing the phy coming back (as posted + * by the lldd). So we post the event and once we are in the + * libsas context check that the phy remains suspended before + * tearing it down. + */ + i = phys_suspended(ha); + if (i) + dev_info(ha->dev, "waiting up to 25 seconds for %d phy%s to resume\n", + i, i > 1 ? "s" : ""); + wait_event_timeout(ha->eh_wait_q, phys_suspended(ha) == 0, tmo); + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_phy *phy = ha->sas_phy[i]; + + if (phy->suspended) { + dev_warn(&phy->phy->dev, "resume timeout\n"); + sas_notify_phy_event(phy, PHYE_RESUME_TIMEOUT); + } + } + + /* all phys are back up or timed out, turn on i/o so we can + * flush out disks that did not return + */ + scsi_unblock_requests(ha->core.shost); + sas_drain_work(ha); +} +EXPORT_SYMBOL(sas_resume_ha); + +void sas_suspend_ha(struct sas_ha_struct *ha) +{ + int i; + + sas_disable_events(ha); + scsi_block_requests(ha->core.shost); + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_port *port = ha->sas_port[i]; + + sas_discover_event(port, DISCE_SUSPEND); + } + + /* flush suspend events while unregistered */ + mutex_lock(&ha->drain_mutex); + __sas_drain_work(ha); + mutex_unlock(&ha->drain_mutex); +} +EXPORT_SYMBOL(sas_suspend_ha); + +static void sas_phy_release(struct sas_phy *phy) +{ + kfree(phy->hostdata); + phy->hostdata = NULL; +} + +static void phy_reset_work(struct work_struct *work) +{ + struct sas_phy_data *d = container_of(work, typeof(*d), reset_work.work); + + d->reset_result = transport_sas_phy_reset(d->phy, d->hard_reset); +} + +static void phy_enable_work(struct work_struct *work) +{ + struct sas_phy_data *d = container_of(work, typeof(*d), enable_work.work); + + d->enable_result = sas_phy_enable(d->phy, d->enable); +} + +static int sas_phy_setup(struct sas_phy *phy) +{ + struct sas_phy_data *d = kzalloc(sizeof(*d), GFP_KERNEL); + + if (!d) + return -ENOMEM; + + mutex_init(&d->event_lock); + INIT_SAS_WORK(&d->reset_work, phy_reset_work); + INIT_SAS_WORK(&d->enable_work, phy_enable_work); + d->phy = phy; + phy->hostdata = d; + + return 0; +} + +static int queue_phy_reset(struct sas_phy *phy, int hard_reset) +{ + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + struct sas_phy_data *d = phy->hostdata; + int rc; + + if (!d) + return -ENOMEM; + + /* libsas workqueue coordinates ata-eh reset with discovery */ + mutex_lock(&d->event_lock); + d->reset_result = 0; + d->hard_reset = hard_reset; + + spin_lock_irq(&ha->lock); + sas_queue_work(ha, &d->reset_work); + spin_unlock_irq(&ha->lock); + + rc = sas_drain_work(ha); + if (rc == 0) + rc = d->reset_result; + mutex_unlock(&d->event_lock); + + return rc; +} + +static int queue_phy_enable(struct sas_phy *phy, int enable) +{ + struct Scsi_Host *shost = dev_to_shost(phy->dev.parent); + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + struct sas_phy_data *d = phy->hostdata; + int rc; + + if (!d) + return -ENOMEM; + + /* libsas workqueue coordinates ata-eh reset with discovery */ + mutex_lock(&d->event_lock); + d->enable_result = 0; + d->enable = enable; + + spin_lock_irq(&ha->lock); + sas_queue_work(ha, &d->enable_work); + spin_unlock_irq(&ha->lock); + + rc = sas_drain_work(ha); + if (rc == 0) + rc = d->enable_result; + mutex_unlock(&d->event_lock); + + return rc; +} + +static struct sas_function_template sft = { + .phy_enable = queue_phy_enable, + .phy_reset = queue_phy_reset, + .phy_setup = sas_phy_setup, + .phy_release = sas_phy_release, + .set_phy_speed = sas_set_phy_speed, + .get_linkerrors = sas_get_linkerrors, + .smp_handler = sas_smp_handler, +}; + +static inline ssize_t phy_event_threshold_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct Scsi_Host *shost = class_to_shost(dev); + struct sas_ha_struct *sha = SHOST_TO_SAS_HA(shost); + + return scnprintf(buf, PAGE_SIZE, "%u\n", sha->event_thres); +} + +static inline ssize_t phy_event_threshold_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct Scsi_Host *shost = class_to_shost(dev); + struct sas_ha_struct *sha = SHOST_TO_SAS_HA(shost); + + sha->event_thres = simple_strtol(buf, NULL, 10); + + /* threshold cannot be set too small */ + if (sha->event_thres < 32) + sha->event_thres = 32; + + return count; +} + +DEVICE_ATTR(phy_event_threshold, + S_IRUGO|S_IWUSR, + phy_event_threshold_show, + phy_event_threshold_store); +EXPORT_SYMBOL_GPL(dev_attr_phy_event_threshold); + +struct scsi_transport_template * +sas_domain_attach_transport(struct sas_domain_function_template *dft) +{ + struct scsi_transport_template *stt = sas_attach_transport(&sft); + struct sas_internal *i; + + if (!stt) + return stt; + + i = to_sas_internal(stt); + i->dft = dft; + stt->create_work_queue = 1; + stt->eh_strategy_handler = sas_scsi_recover_host; + + return stt; +} +EXPORT_SYMBOL_GPL(sas_domain_attach_transport); + +static struct asd_sas_event *__sas_alloc_event(struct asd_sas_phy *phy, + gfp_t gfp_flags) +{ + struct asd_sas_event *event; + struct sas_ha_struct *sas_ha = phy->ha; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + event = kmem_cache_zalloc(sas_event_cache, gfp_flags); + if (!event) + return NULL; + + atomic_inc(&phy->event_nr); + + if (atomic_read(&phy->event_nr) > phy->ha->event_thres) { + if (i->dft->lldd_control_phy) { + if (cmpxchg(&phy->in_shutdown, 0, 1) == 0) { + pr_notice("The phy%d bursting events, shut it down.\n", + phy->id); + sas_notify_phy_event_gfp(phy, PHYE_SHUTDOWN, + gfp_flags); + } + } else { + /* Do not support PHY control, stop allocating events */ + WARN_ONCE(1, "PHY control not supported.\n"); + kmem_cache_free(sas_event_cache, event); + atomic_dec(&phy->event_nr); + event = NULL; + } + } + + return event; +} + +struct asd_sas_event *sas_alloc_event(struct asd_sas_phy *phy) +{ + return __sas_alloc_event(phy, in_interrupt() ? GFP_ATOMIC : GFP_KERNEL); +} + +struct asd_sas_event *sas_alloc_event_gfp(struct asd_sas_phy *phy, + gfp_t gfp_flags) +{ + return __sas_alloc_event(phy, gfp_flags); +} + +void sas_free_event(struct asd_sas_event *event) +{ + struct asd_sas_phy *phy = event->phy; + + kmem_cache_free(sas_event_cache, event); + atomic_dec(&phy->event_nr); +} + +/* ---------- SAS Class register/unregister ---------- */ + +static int __init sas_class_init(void) +{ + sas_task_cache = KMEM_CACHE(sas_task, SLAB_HWCACHE_ALIGN); + if (!sas_task_cache) + goto out; + + sas_event_cache = KMEM_CACHE(asd_sas_event, SLAB_HWCACHE_ALIGN); + if (!sas_event_cache) + goto free_task_kmem; + + return 0; +free_task_kmem: + kmem_cache_destroy(sas_task_cache); +out: + return -ENOMEM; +} + +static void __exit sas_class_exit(void) +{ + kmem_cache_destroy(sas_task_cache); + kmem_cache_destroy(sas_event_cache); +} + +MODULE_AUTHOR("Luben Tuikov <luben_tuikov@adaptec.com>"); +MODULE_DESCRIPTION("SAS Transport Layer"); +MODULE_LICENSE("GPL v2"); + +module_init(sas_class_init); +module_exit(sas_class_exit); + +EXPORT_SYMBOL_GPL(sas_register_ha); +EXPORT_SYMBOL_GPL(sas_unregister_ha); diff --git a/drivers/scsi/libsas/sas_internal.h b/drivers/scsi/libsas/sas_internal.h new file mode 100644 index 000000000..52e09c3e2 --- /dev/null +++ b/drivers/scsi/libsas/sas_internal.h @@ -0,0 +1,195 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Serial Attached SCSI (SAS) class internal header file + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#ifndef _SAS_INTERNAL_H_ +#define _SAS_INTERNAL_H_ + +#include <scsi/scsi.h> +#include <scsi/scsi_host.h> +#include <scsi/scsi_transport_sas.h> +#include <scsi/libsas.h> +#include <scsi/sas_ata.h> + +#ifdef pr_fmt +#undef pr_fmt +#endif + +#define SAS_FMT "sas: " + +#define pr_fmt(fmt) SAS_FMT fmt + +#define TO_SAS_TASK(_scsi_cmd) ((void *)(_scsi_cmd)->host_scribble) +#define ASSIGN_SAS_TASK(_sc, _t) do { (_sc)->host_scribble = (void *) _t; } while (0) + +struct sas_phy_data { + /* let reset be performed in sas_queue_work() context */ + struct sas_phy *phy; + struct mutex event_lock; + int hard_reset; + int reset_result; + struct sas_work reset_work; + int enable; + int enable_result; + struct sas_work enable_work; +}; + +void sas_scsi_recover_host(struct Scsi_Host *shost); + +int sas_show_class(enum sas_class class, char *buf); +int sas_show_proto(enum sas_protocol proto, char *buf); +int sas_show_linkrate(enum sas_linkrate linkrate, char *buf); +int sas_show_oob_mode(enum sas_oob_mode oob_mode, char *buf); + +int sas_register_phys(struct sas_ha_struct *sas_ha); +void sas_unregister_phys(struct sas_ha_struct *sas_ha); + +struct asd_sas_event *sas_alloc_event(struct asd_sas_phy *phy); +struct asd_sas_event *sas_alloc_event_gfp(struct asd_sas_phy *phy, + gfp_t gfp_flags); +void sas_free_event(struct asd_sas_event *event); + +int sas_register_ports(struct sas_ha_struct *sas_ha); +void sas_unregister_ports(struct sas_ha_struct *sas_ha); + +void sas_disable_revalidation(struct sas_ha_struct *ha); +void sas_enable_revalidation(struct sas_ha_struct *ha); +void __sas_drain_work(struct sas_ha_struct *ha); + +void sas_deform_port(struct asd_sas_phy *phy, int gone); + +void sas_porte_bytes_dmaed(struct work_struct *work); +void sas_porte_broadcast_rcvd(struct work_struct *work); +void sas_porte_link_reset_err(struct work_struct *work); +void sas_porte_timer_event(struct work_struct *work); +void sas_porte_hard_reset(struct work_struct *work); +int sas_queue_work(struct sas_ha_struct *ha, struct sas_work *sw); + +int sas_notify_lldd_dev_found(struct domain_device *); +void sas_notify_lldd_dev_gone(struct domain_device *); + +void sas_smp_handler(struct bsg_job *job, struct Scsi_Host *shost, + struct sas_rphy *rphy); +int sas_smp_phy_control(struct domain_device *dev, int phy_id, + enum phy_func phy_func, struct sas_phy_linkrates *); +int sas_smp_get_phy_events(struct sas_phy *phy); + +int sas_notify_phy_event(struct asd_sas_phy *phy, enum phy_event event); +int sas_notify_phy_event_gfp(struct asd_sas_phy *phy, enum phy_event event, + gfp_t flags); +void sas_device_set_phy(struct domain_device *dev, struct sas_port *port); +struct domain_device *sas_find_dev_by_rphy(struct sas_rphy *rphy); +struct domain_device *sas_ex_to_ata(struct domain_device *ex_dev, int phy_id); +int sas_ex_phy_discover(struct domain_device *dev, int single); +int sas_get_report_phy_sata(struct domain_device *dev, int phy_id, + struct smp_resp *rps_resp); +int sas_try_ata_reset(struct asd_sas_phy *phy); +void sas_hae_reset(struct work_struct *work); + +void sas_free_device(struct kref *kref); +void sas_destruct_devices(struct asd_sas_port *port); + +extern const work_func_t sas_phy_event_fns[PHY_NUM_EVENTS]; +extern const work_func_t sas_port_event_fns[PORT_NUM_EVENTS]; + +#ifdef CONFIG_SCSI_SAS_HOST_SMP +extern void sas_smp_host_handler(struct bsg_job *job, struct Scsi_Host *shost); +#else +static inline void sas_smp_host_handler(struct bsg_job *job, + struct Scsi_Host *shost) +{ + shost_printk(KERN_ERR, shost, + "Cannot send SMP to a sas host (not enabled in CONFIG)\n"); + bsg_job_done(job, -EINVAL, 0); +} +#endif + +static inline void sas_fail_probe(struct domain_device *dev, const char *func, int err) +{ + pr_warn("%s: for %s device %016llx returned %d\n", + func, dev->parent ? "exp-attached" : + "direct-attached", + SAS_ADDR(dev->sas_addr), err); + sas_unregister_dev(dev->port, dev); +} + +static inline void sas_fill_in_rphy(struct domain_device *dev, + struct sas_rphy *rphy) +{ + rphy->identify.sas_address = SAS_ADDR(dev->sas_addr); + rphy->identify.initiator_port_protocols = dev->iproto; + rphy->identify.target_port_protocols = dev->tproto; + switch (dev->dev_type) { + case SAS_SATA_DEV: + /* FIXME: need sata device type */ + case SAS_END_DEVICE: + case SAS_SATA_PENDING: + rphy->identify.device_type = SAS_END_DEVICE; + break; + case SAS_EDGE_EXPANDER_DEVICE: + rphy->identify.device_type = SAS_EDGE_EXPANDER_DEVICE; + break; + case SAS_FANOUT_EXPANDER_DEVICE: + rphy->identify.device_type = SAS_FANOUT_EXPANDER_DEVICE; + break; + default: + rphy->identify.device_type = SAS_PHY_UNUSED; + break; + } +} + +static inline void sas_phy_set_target(struct asd_sas_phy *p, struct domain_device *dev) +{ + struct sas_phy *phy = p->phy; + + if (dev) { + if (dev_is_sata(dev)) + phy->identify.device_type = SAS_END_DEVICE; + else + phy->identify.device_type = dev->dev_type; + phy->identify.target_port_protocols = dev->tproto; + } else { + phy->identify.device_type = SAS_PHY_UNUSED; + phy->identify.target_port_protocols = 0; + } +} + +static inline void sas_add_parent_port(struct domain_device *dev, int phy_id) +{ + struct expander_device *ex = &dev->ex_dev; + struct ex_phy *ex_phy = &ex->ex_phy[phy_id]; + + if (!ex->parent_port) { + ex->parent_port = sas_port_alloc(&dev->rphy->dev, phy_id); + /* FIXME: error handling */ + BUG_ON(!ex->parent_port); + BUG_ON(sas_port_add(ex->parent_port)); + sas_port_mark_backlink(ex->parent_port); + } + sas_port_add_phy(ex->parent_port, ex_phy->phy); +} + +static inline struct domain_device *sas_alloc_device(void) +{ + struct domain_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL); + + if (dev) { + INIT_LIST_HEAD(&dev->siblings); + INIT_LIST_HEAD(&dev->dev_list_node); + INIT_LIST_HEAD(&dev->disco_list_node); + kref_init(&dev->kref); + spin_lock_init(&dev->done_lock); + } + return dev; +} + +static inline void sas_put_device(struct domain_device *dev) +{ + kref_put(&dev->kref, sas_free_device); +} + +#endif /* _SAS_INTERNAL_H_ */ diff --git a/drivers/scsi/libsas/sas_phy.c b/drivers/scsi/libsas/sas_phy.c new file mode 100644 index 000000000..4ca4b1f30 --- /dev/null +++ b/drivers/scsi/libsas/sas_phy.c @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Serial Attached SCSI (SAS) Phy class + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include "sas_internal.h" +#include <scsi/scsi_host.h> +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include "../scsi_sas_internal.h" + +/* ---------- Phy events ---------- */ + +static void sas_phye_loss_of_signal(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + phy->error = 0; + sas_deform_port(phy, 1); +} + +static void sas_phye_oob_done(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + phy->error = 0; +} + +static void sas_phye_oob_error(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + struct sas_ha_struct *sas_ha = phy->ha; + struct asd_sas_port *port = phy->port; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + sas_deform_port(phy, 1); + + if (!port && phy->enabled && i->dft->lldd_control_phy) { + phy->error++; + switch (phy->error) { + case 1: + case 2: + i->dft->lldd_control_phy(phy, PHY_FUNC_HARD_RESET, + NULL); + break; + case 3: + default: + phy->error = 0; + phy->enabled = 0; + i->dft->lldd_control_phy(phy, PHY_FUNC_DISABLE, NULL); + break; + } + } +} + +static void sas_phye_spinup_hold(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + struct sas_ha_struct *sas_ha = phy->ha; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + phy->error = 0; + i->dft->lldd_control_phy(phy, PHY_FUNC_RELEASE_SPINUP_HOLD, NULL); +} + +static void sas_phye_resume_timeout(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + /* phew, lldd got the phy back in the nick of time */ + if (!phy->suspended) { + dev_info(&phy->phy->dev, "resume timeout cancelled\n"); + return; + } + + phy->error = 0; + phy->suspended = 0; + sas_deform_port(phy, 1); +} + + +static void sas_phye_shutdown(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + struct sas_ha_struct *sas_ha = phy->ha; + struct sas_internal *i = + to_sas_internal(sas_ha->core.shost->transportt); + + if (phy->enabled) { + int ret; + + phy->error = 0; + phy->enabled = 0; + ret = i->dft->lldd_control_phy(phy, PHY_FUNC_DISABLE, NULL); + if (ret) + pr_notice("lldd disable phy%d returned %d\n", phy->id, + ret); + } else + pr_notice("phy%d is not enabled, cannot shutdown\n", phy->id); + phy->in_shutdown = 0; +} + +/* ---------- Phy class registration ---------- */ + +int sas_register_phys(struct sas_ha_struct *sas_ha) +{ + int i; + + /* Now register the phys. */ + for (i = 0; i < sas_ha->num_phys; i++) { + struct asd_sas_phy *phy = sas_ha->sas_phy[i]; + + phy->error = 0; + atomic_set(&phy->event_nr, 0); + INIT_LIST_HEAD(&phy->port_phy_el); + + phy->port = NULL; + phy->ha = sas_ha; + spin_lock_init(&phy->frame_rcvd_lock); + spin_lock_init(&phy->sas_prim_lock); + phy->frame_rcvd_size = 0; + + phy->phy = sas_phy_alloc(&sas_ha->core.shost->shost_gendev, i); + if (!phy->phy) + return -ENOMEM; + + phy->phy->identify.initiator_port_protocols = + phy->iproto; + phy->phy->identify.target_port_protocols = phy->tproto; + phy->phy->identify.sas_address = SAS_ADDR(sas_ha->sas_addr); + phy->phy->identify.phy_identifier = i; + phy->phy->minimum_linkrate_hw = SAS_LINK_RATE_UNKNOWN; + phy->phy->maximum_linkrate_hw = SAS_LINK_RATE_UNKNOWN; + phy->phy->minimum_linkrate = SAS_LINK_RATE_UNKNOWN; + phy->phy->maximum_linkrate = SAS_LINK_RATE_UNKNOWN; + phy->phy->negotiated_linkrate = SAS_LINK_RATE_UNKNOWN; + + sas_phy_add(phy->phy); + } + + return 0; +} + +const work_func_t sas_phy_event_fns[PHY_NUM_EVENTS] = { + [PHYE_LOSS_OF_SIGNAL] = sas_phye_loss_of_signal, + [PHYE_OOB_DONE] = sas_phye_oob_done, + [PHYE_OOB_ERROR] = sas_phye_oob_error, + [PHYE_SPINUP_HOLD] = sas_phye_spinup_hold, + [PHYE_RESUME_TIMEOUT] = sas_phye_resume_timeout, + [PHYE_SHUTDOWN] = sas_phye_shutdown, +}; diff --git a/drivers/scsi/libsas/sas_port.c b/drivers/scsi/libsas/sas_port.c new file mode 100644 index 000000000..e3d03d744 --- /dev/null +++ b/drivers/scsi/libsas/sas_port.c @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Serial Attached SCSI (SAS) Port class + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include "sas_internal.h" + +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include "../scsi_sas_internal.h" + +static bool phy_is_wideport_member(struct asd_sas_port *port, struct asd_sas_phy *phy) +{ + struct sas_ha_struct *sas_ha = phy->ha; + + if (memcmp(port->attached_sas_addr, phy->attached_sas_addr, + SAS_ADDR_SIZE) != 0 || (sas_ha->strict_wide_ports && + memcmp(port->sas_addr, phy->sas_addr, SAS_ADDR_SIZE) != 0)) + return false; + return true; +} + +static void sas_resume_port(struct asd_sas_phy *phy) +{ + struct domain_device *dev, *n; + struct asd_sas_port *port = phy->port; + struct sas_ha_struct *sas_ha = phy->ha; + struct sas_internal *si = to_sas_internal(sas_ha->core.shost->transportt); + + if (si->dft->lldd_port_formed) + si->dft->lldd_port_formed(phy); + + if (port->suspended) + port->suspended = 0; + else { + /* we only need to handle "link returned" actions once */ + return; + } + + /* if the port came back: + * 1/ presume every device came back + * 2/ force the next revalidation to check all expander phys + */ + list_for_each_entry_safe(dev, n, &port->dev_list, dev_list_node) { + int i, rc; + + rc = sas_notify_lldd_dev_found(dev); + if (rc) { + sas_unregister_dev(port, dev); + sas_destruct_devices(port); + continue; + } + + if (dev_is_expander(dev->dev_type)) { + dev->ex_dev.ex_change_count = -1; + for (i = 0; i < dev->ex_dev.num_phys; i++) { + struct ex_phy *phy = &dev->ex_dev.ex_phy[i]; + + phy->phy_change_count = -1; + } + } + } + + sas_discover_event(port, DISCE_RESUME); +} + +/** + * sas_form_port - add this phy to a port + * @phy: the phy of interest + * + * This function adds this phy to an existing port, thus creating a wide + * port, or it creates a port and adds the phy to the port. + */ +static void sas_form_port(struct asd_sas_phy *phy) +{ + int i; + struct sas_ha_struct *sas_ha = phy->ha; + struct asd_sas_port *port = phy->port; + struct domain_device *port_dev; + struct sas_internal *si = + to_sas_internal(sas_ha->core.shost->transportt); + unsigned long flags; + + if (port) { + if (!phy_is_wideport_member(port, phy)) + sas_deform_port(phy, 0); + else if (phy->suspended) { + phy->suspended = 0; + sas_resume_port(phy); + + /* phy came back, try to cancel the timeout */ + wake_up(&sas_ha->eh_wait_q); + return; + } else { + pr_info("%s: phy%d belongs to port%d already(%d)!\n", + __func__, phy->id, phy->port->id, + phy->port->num_phys); + return; + } + } + + /* see if the phy should be part of a wide port */ + spin_lock_irqsave(&sas_ha->phy_port_lock, flags); + for (i = 0; i < sas_ha->num_phys; i++) { + port = sas_ha->sas_port[i]; + spin_lock(&port->phy_list_lock); + if (*(u64 *) port->sas_addr && + phy_is_wideport_member(port, phy) && port->num_phys > 0) { + /* wide port */ + pr_debug("phy%d matched wide port%d\n", phy->id, + port->id); + break; + } + spin_unlock(&port->phy_list_lock); + } + /* The phy does not match any existing port, create a new one */ + if (i == sas_ha->num_phys) { + for (i = 0; i < sas_ha->num_phys; i++) { + port = sas_ha->sas_port[i]; + spin_lock(&port->phy_list_lock); + if (*(u64 *)port->sas_addr == 0 + && port->num_phys == 0) { + memcpy(port->sas_addr, phy->sas_addr, + SAS_ADDR_SIZE); + break; + } + spin_unlock(&port->phy_list_lock); + } + } + + if (i >= sas_ha->num_phys) { + pr_err("%s: couldn't find a free port, bug?\n", __func__); + spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags); + return; + } + + /* add the phy to the port */ + port_dev = port->port_dev; + list_add_tail(&phy->port_phy_el, &port->phy_list); + sas_phy_set_target(phy, port_dev); + phy->port = port; + port->num_phys++; + port->phy_mask |= (1U << phy->id); + + if (*(u64 *)port->attached_sas_addr == 0) { + port->class = phy->class; + memcpy(port->attached_sas_addr, phy->attached_sas_addr, + SAS_ADDR_SIZE); + port->iproto = phy->iproto; + port->tproto = phy->tproto; + port->oob_mode = phy->oob_mode; + port->linkrate = phy->linkrate; + } else + port->linkrate = max(port->linkrate, phy->linkrate); + spin_unlock(&port->phy_list_lock); + spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags); + + if (!port->port) { + port->port = sas_port_alloc(phy->phy->dev.parent, port->id); + BUG_ON(!port->port); + sas_port_add(port->port); + } + sas_port_add_phy(port->port, phy->phy); + + pr_debug("%s added to %s, phy_mask:0x%x (%016llx)\n", + dev_name(&phy->phy->dev), dev_name(&port->port->dev), + port->phy_mask, + SAS_ADDR(port->attached_sas_addr)); + + if (port_dev) + port_dev->pathways = port->num_phys; + + /* Tell the LLDD about this port formation. */ + if (si->dft->lldd_port_formed) + si->dft->lldd_port_formed(phy); + + sas_discover_event(phy->port, DISCE_DISCOVER_DOMAIN); + /* Only insert a revalidate event after initial discovery */ + if (port_dev && dev_is_expander(port_dev->dev_type)) { + struct expander_device *ex_dev = &port_dev->ex_dev; + + ex_dev->ex_change_count = -1; + sas_discover_event(port, DISCE_REVALIDATE_DOMAIN); + } + flush_workqueue(sas_ha->disco_q); +} + +/** + * sas_deform_port - remove this phy from the port it belongs to + * @phy: the phy of interest + * @gone: whether or not the PHY is gone + * + * This is called when the physical link to the other phy has been + * lost (on this phy), in Event thread context. We cannot delay here. + */ +void sas_deform_port(struct asd_sas_phy *phy, int gone) +{ + struct sas_ha_struct *sas_ha = phy->ha; + struct asd_sas_port *port = phy->port; + struct sas_internal *si = + to_sas_internal(sas_ha->core.shost->transportt); + struct domain_device *dev; + unsigned long flags; + + if (!port) + return; /* done by a phy event */ + + dev = port->port_dev; + if (dev) + dev->pathways--; + + if (port->num_phys == 1) { + sas_unregister_domain_devices(port, gone); + sas_destruct_devices(port); + sas_port_delete(port->port); + port->port = NULL; + } else { + sas_port_delete_phy(port->port, phy->phy); + sas_device_set_phy(dev, port->port); + } + + if (si->dft->lldd_port_deformed) + si->dft->lldd_port_deformed(phy); + + spin_lock_irqsave(&sas_ha->phy_port_lock, flags); + spin_lock(&port->phy_list_lock); + + list_del_init(&phy->port_phy_el); + sas_phy_set_target(phy, NULL); + phy->port = NULL; + port->num_phys--; + port->phy_mask &= ~(1U << phy->id); + + if (port->num_phys == 0) { + INIT_LIST_HEAD(&port->phy_list); + memset(port->sas_addr, 0, SAS_ADDR_SIZE); + memset(port->attached_sas_addr, 0, SAS_ADDR_SIZE); + port->class = 0; + port->iproto = 0; + port->tproto = 0; + port->oob_mode = 0; + port->phy_mask = 0; + } + spin_unlock(&port->phy_list_lock); + spin_unlock_irqrestore(&sas_ha->phy_port_lock, flags); + + /* Only insert revalidate event if the port still has members */ + if (port->port && dev && dev_is_expander(dev->dev_type)) { + struct expander_device *ex_dev = &dev->ex_dev; + + ex_dev->ex_change_count = -1; + sas_discover_event(port, DISCE_REVALIDATE_DOMAIN); + } + flush_workqueue(sas_ha->disco_q); + + return; +} + +/* ---------- SAS port events ---------- */ + +void sas_porte_bytes_dmaed(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + sas_form_port(phy); +} + +void sas_porte_broadcast_rcvd(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + unsigned long flags; + u32 prim; + + spin_lock_irqsave(&phy->sas_prim_lock, flags); + prim = phy->sas_prim; + spin_unlock_irqrestore(&phy->sas_prim_lock, flags); + + pr_debug("broadcast received: %d\n", prim); + sas_discover_event(phy->port, DISCE_REVALIDATE_DOMAIN); + + if (phy->port) + flush_workqueue(phy->port->ha->disco_q); +} + +void sas_porte_link_reset_err(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + sas_deform_port(phy, 1); +} + +void sas_porte_timer_event(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + sas_deform_port(phy, 1); +} + +void sas_porte_hard_reset(struct work_struct *work) +{ + struct asd_sas_event *ev = to_asd_sas_event(work); + struct asd_sas_phy *phy = ev->phy; + + sas_deform_port(phy, 1); +} + +/* ---------- SAS port registration ---------- */ + +static void sas_init_port(struct asd_sas_port *port, + struct sas_ha_struct *sas_ha, int i) +{ + memset(port, 0, sizeof(*port)); + port->id = i; + INIT_LIST_HEAD(&port->dev_list); + INIT_LIST_HEAD(&port->disco_list); + INIT_LIST_HEAD(&port->destroy_list); + INIT_LIST_HEAD(&port->sas_port_del_list); + spin_lock_init(&port->phy_list_lock); + INIT_LIST_HEAD(&port->phy_list); + port->ha = sas_ha; + + spin_lock_init(&port->dev_list_lock); +} + +int sas_register_ports(struct sas_ha_struct *sas_ha) +{ + int i; + + /* initialize the ports and discovery */ + for (i = 0; i < sas_ha->num_phys; i++) { + struct asd_sas_port *port = sas_ha->sas_port[i]; + + sas_init_port(port, sas_ha, i); + sas_init_disc(&port->disc, port); + } + return 0; +} + +void sas_unregister_ports(struct sas_ha_struct *sas_ha) +{ + int i; + + for (i = 0; i < sas_ha->num_phys; i++) + if (sas_ha->sas_phy[i]->port) + sas_deform_port(sas_ha->sas_phy[i], 0); + +} + +const work_func_t sas_port_event_fns[PORT_NUM_EVENTS] = { + [PORTE_BYTES_DMAED] = sas_porte_bytes_dmaed, + [PORTE_BROADCAST_RCVD] = sas_porte_broadcast_rcvd, + [PORTE_LINK_RESET_ERR] = sas_porte_link_reset_err, + [PORTE_TIMER_EVENT] = sas_porte_timer_event, + [PORTE_HARD_RESET] = sas_porte_hard_reset, +}; diff --git a/drivers/scsi/libsas/sas_scsi_host.c b/drivers/scsi/libsas/sas_scsi_host.c new file mode 100644 index 000000000..ee44a0d77 --- /dev/null +++ b/drivers/scsi/libsas/sas_scsi_host.c @@ -0,0 +1,970 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Serial Attached SCSI (SAS) class SCSI Host glue. + * + * Copyright (C) 2005 Adaptec, Inc. All rights reserved. + * Copyright (C) 2005 Luben Tuikov <luben_tuikov@adaptec.com> + */ + +#include <linux/kthread.h> +#include <linux/firmware.h> +#include <linux/export.h> +#include <linux/ctype.h> +#include <linux/kernel.h> + +#include "sas_internal.h" + +#include <scsi/scsi_host.h> +#include <scsi/scsi_device.h> +#include <scsi/scsi_tcq.h> +#include <scsi/scsi.h> +#include <scsi/scsi_eh.h> +#include <scsi/scsi_transport.h> +#include <scsi/scsi_transport_sas.h> +#include <scsi/sas_ata.h> +#include "../scsi_sas_internal.h" +#include "../scsi_transport_api.h" +#include "../scsi_priv.h" + +#include <linux/err.h> +#include <linux/blkdev.h> +#include <linux/freezer.h> +#include <linux/gfp.h> +#include <linux/scatterlist.h> +#include <linux/libata.h> + +/* record final status and free the task */ +static void sas_end_task(struct scsi_cmnd *sc, struct sas_task *task) +{ + struct task_status_struct *ts = &task->task_status; + int hs = 0, stat = 0; + + if (ts->resp == SAS_TASK_UNDELIVERED) { + /* transport error */ + hs = DID_NO_CONNECT; + } else { /* ts->resp == SAS_TASK_COMPLETE */ + /* task delivered, what happened afterwards? */ + switch (ts->stat) { + case SAS_DEV_NO_RESPONSE: + case SAS_INTERRUPTED: + case SAS_PHY_DOWN: + case SAS_NAK_R_ERR: + case SAS_OPEN_TO: + hs = DID_NO_CONNECT; + break; + case SAS_DATA_UNDERRUN: + scsi_set_resid(sc, ts->residual); + if (scsi_bufflen(sc) - scsi_get_resid(sc) < sc->underflow) + hs = DID_ERROR; + break; + case SAS_DATA_OVERRUN: + hs = DID_ERROR; + break; + case SAS_QUEUE_FULL: + hs = DID_SOFT_ERROR; /* retry */ + break; + case SAS_DEVICE_UNKNOWN: + hs = DID_BAD_TARGET; + break; + case SAS_SG_ERR: + hs = DID_PARITY; + break; + case SAS_OPEN_REJECT: + if (ts->open_rej_reason == SAS_OREJ_RSVD_RETRY) + hs = DID_SOFT_ERROR; /* retry */ + else + hs = DID_ERROR; + break; + case SAS_PROTO_RESPONSE: + pr_notice("LLDD:%s sent SAS_PROTO_RESP for an SSP task; please report this\n", + task->dev->port->ha->sas_ha_name); + break; + case SAS_ABORTED_TASK: + hs = DID_ABORT; + break; + case SAM_STAT_CHECK_CONDITION: + memcpy(sc->sense_buffer, ts->buf, + min(SCSI_SENSE_BUFFERSIZE, ts->buf_valid_size)); + stat = SAM_STAT_CHECK_CONDITION; + break; + default: + stat = ts->stat; + break; + } + } + + sc->result = (hs << 16) | stat; + ASSIGN_SAS_TASK(sc, NULL); + sas_free_task(task); +} + +static void sas_scsi_task_done(struct sas_task *task) +{ + struct scsi_cmnd *sc = task->uldd_task; + struct domain_device *dev = task->dev; + struct sas_ha_struct *ha = dev->port->ha; + unsigned long flags; + + spin_lock_irqsave(&dev->done_lock, flags); + if (test_bit(SAS_HA_FROZEN, &ha->state)) + task = NULL; + else + ASSIGN_SAS_TASK(sc, NULL); + spin_unlock_irqrestore(&dev->done_lock, flags); + + if (unlikely(!task)) { + /* task will be completed by the error handler */ + pr_debug("task done but aborted\n"); + return; + } + + if (unlikely(!sc)) { + pr_debug("task_done called with non existing SCSI cmnd!\n"); + sas_free_task(task); + return; + } + + sas_end_task(sc, task); + sc->scsi_done(sc); +} + +static struct sas_task *sas_create_task(struct scsi_cmnd *cmd, + struct domain_device *dev, + gfp_t gfp_flags) +{ + struct sas_task *task = sas_alloc_task(gfp_flags); + struct scsi_lun lun; + + if (!task) + return NULL; + + task->uldd_task = cmd; + ASSIGN_SAS_TASK(cmd, task); + + task->dev = dev; + task->task_proto = task->dev->tproto; /* BUG_ON(!SSP) */ + + task->ssp_task.retry_count = 1; + int_to_scsilun(cmd->device->lun, &lun); + memcpy(task->ssp_task.LUN, &lun.scsi_lun, 8); + task->ssp_task.task_attr = TASK_ATTR_SIMPLE; + task->ssp_task.cmd = cmd; + + task->scatter = scsi_sglist(cmd); + task->num_scatter = scsi_sg_count(cmd); + task->total_xfer_len = scsi_bufflen(cmd); + task->data_dir = cmd->sc_data_direction; + + task->task_done = sas_scsi_task_done; + + return task; +} + +int sas_queuecommand(struct Scsi_Host *host, struct scsi_cmnd *cmd) +{ + struct sas_internal *i = to_sas_internal(host->transportt); + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_task *task; + int res = 0; + + /* If the device fell off, no sense in issuing commands */ + if (test_bit(SAS_DEV_GONE, &dev->state)) { + cmd->result = DID_BAD_TARGET << 16; + goto out_done; + } + + if (dev_is_sata(dev)) { + spin_lock_irq(dev->sata_dev.ap->lock); + res = ata_sas_queuecmd(cmd, dev->sata_dev.ap); + spin_unlock_irq(dev->sata_dev.ap->lock); + return res; + } + + task = sas_create_task(cmd, dev, GFP_ATOMIC); + if (!task) + return SCSI_MLQUEUE_HOST_BUSY; + + res = i->dft->lldd_execute_task(task, GFP_ATOMIC); + if (res) + goto out_free_task; + return 0; + +out_free_task: + pr_debug("lldd_execute_task returned: %d\n", res); + ASSIGN_SAS_TASK(cmd, NULL); + sas_free_task(task); + if (res == -SAS_QUEUE_FULL) + cmd->result = DID_SOFT_ERROR << 16; /* retry */ + else + cmd->result = DID_ERROR << 16; +out_done: + cmd->scsi_done(cmd); + return 0; +} + +static void sas_eh_finish_cmd(struct scsi_cmnd *cmd) +{ + struct sas_ha_struct *sas_ha = SHOST_TO_SAS_HA(cmd->device->host); + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_task *task = TO_SAS_TASK(cmd); + + /* At this point, we only get called following an actual abort + * of the task, so we should be guaranteed not to be racing with + * any completions from the LLD. Task is freed after this. + */ + sas_end_task(cmd, task); + + if (dev_is_sata(dev)) { + /* defer commands to libata so that libata EH can + * handle ata qcs correctly + */ + list_move_tail(&cmd->eh_entry, &sas_ha->eh_ata_q); + return; + } + + /* now finish the command and move it on to the error + * handler done list, this also takes it off the + * error handler pending list. + */ + scsi_eh_finish_cmd(cmd, &sas_ha->eh_done_q); +} + +static void sas_scsi_clear_queue_lu(struct list_head *error_q, struct scsi_cmnd *my_cmd) +{ + struct scsi_cmnd *cmd, *n; + + list_for_each_entry_safe(cmd, n, error_q, eh_entry) { + if (cmd->device->sdev_target == my_cmd->device->sdev_target && + cmd->device->lun == my_cmd->device->lun) + sas_eh_finish_cmd(cmd); + } +} + +static void sas_scsi_clear_queue_I_T(struct list_head *error_q, + struct domain_device *dev) +{ + struct scsi_cmnd *cmd, *n; + + list_for_each_entry_safe(cmd, n, error_q, eh_entry) { + struct domain_device *x = cmd_to_domain_dev(cmd); + + if (x == dev) + sas_eh_finish_cmd(cmd); + } +} + +static void sas_scsi_clear_queue_port(struct list_head *error_q, + struct asd_sas_port *port) +{ + struct scsi_cmnd *cmd, *n; + + list_for_each_entry_safe(cmd, n, error_q, eh_entry) { + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct asd_sas_port *x = dev->port; + + if (x == port) + sas_eh_finish_cmd(cmd); + } +} + +enum task_disposition { + TASK_IS_DONE, + TASK_IS_ABORTED, + TASK_IS_AT_LU, + TASK_IS_NOT_AT_LU, + TASK_ABORT_FAILED, +}; + +static enum task_disposition sas_scsi_find_task(struct sas_task *task) +{ + unsigned long flags; + int i, res; + struct sas_internal *si = + to_sas_internal(task->dev->port->ha->core.shost->transportt); + + for (i = 0; i < 5; i++) { + pr_notice("%s: aborting task 0x%p\n", __func__, task); + res = si->dft->lldd_abort_task(task); + + spin_lock_irqsave(&task->task_state_lock, flags); + if (task->task_state_flags & SAS_TASK_STATE_DONE) { + spin_unlock_irqrestore(&task->task_state_lock, flags); + pr_debug("%s: task 0x%p is done\n", __func__, task); + return TASK_IS_DONE; + } + spin_unlock_irqrestore(&task->task_state_lock, flags); + + if (res == TMF_RESP_FUNC_COMPLETE) { + pr_notice("%s: task 0x%p is aborted\n", + __func__, task); + return TASK_IS_ABORTED; + } else if (si->dft->lldd_query_task) { + pr_notice("%s: querying task 0x%p\n", __func__, task); + res = si->dft->lldd_query_task(task); + switch (res) { + case TMF_RESP_FUNC_SUCC: + pr_notice("%s: task 0x%p at LU\n", __func__, + task); + return TASK_IS_AT_LU; + case TMF_RESP_FUNC_COMPLETE: + pr_notice("%s: task 0x%p not at LU\n", + __func__, task); + return TASK_IS_NOT_AT_LU; + case TMF_RESP_FUNC_FAILED: + pr_notice("%s: task 0x%p failed to abort\n", + __func__, task); + return TASK_ABORT_FAILED; + } + + } + } + return res; +} + +static int sas_recover_lu(struct domain_device *dev, struct scsi_cmnd *cmd) +{ + int res = TMF_RESP_FUNC_FAILED; + struct scsi_lun lun; + struct sas_internal *i = + to_sas_internal(dev->port->ha->core.shost->transportt); + + int_to_scsilun(cmd->device->lun, &lun); + + pr_notice("eh: device %016llx LUN 0x%llx has the task\n", + SAS_ADDR(dev->sas_addr), + cmd->device->lun); + + if (i->dft->lldd_abort_task_set) + res = i->dft->lldd_abort_task_set(dev, lun.scsi_lun); + + if (res == TMF_RESP_FUNC_FAILED) { + if (i->dft->lldd_clear_task_set) + res = i->dft->lldd_clear_task_set(dev, lun.scsi_lun); + } + + if (res == TMF_RESP_FUNC_FAILED) { + if (i->dft->lldd_lu_reset) + res = i->dft->lldd_lu_reset(dev, lun.scsi_lun); + } + + return res; +} + +static int sas_recover_I_T(struct domain_device *dev) +{ + int res = TMF_RESP_FUNC_FAILED; + struct sas_internal *i = + to_sas_internal(dev->port->ha->core.shost->transportt); + + pr_notice("I_T nexus reset for dev %016llx\n", + SAS_ADDR(dev->sas_addr)); + + if (i->dft->lldd_I_T_nexus_reset) + res = i->dft->lldd_I_T_nexus_reset(dev); + + return res; +} + +/* take a reference on the last known good phy for this device */ +struct sas_phy *sas_get_local_phy(struct domain_device *dev) +{ + struct sas_ha_struct *ha = dev->port->ha; + struct sas_phy *phy; + unsigned long flags; + + /* a published domain device always has a valid phy, it may be + * stale, but it is never NULL + */ + BUG_ON(!dev->phy); + + spin_lock_irqsave(&ha->phy_port_lock, flags); + phy = dev->phy; + get_device(&phy->dev); + spin_unlock_irqrestore(&ha->phy_port_lock, flags); + + return phy; +} +EXPORT_SYMBOL_GPL(sas_get_local_phy); + +static void sas_wait_eh(struct domain_device *dev) +{ + struct sas_ha_struct *ha = dev->port->ha; + DEFINE_WAIT(wait); + + if (dev_is_sata(dev)) { + ata_port_wait_eh(dev->sata_dev.ap); + return; + } + retry: + spin_lock_irq(&ha->lock); + + while (test_bit(SAS_DEV_EH_PENDING, &dev->state)) { + prepare_to_wait(&ha->eh_wait_q, &wait, TASK_UNINTERRUPTIBLE); + spin_unlock_irq(&ha->lock); + schedule(); + spin_lock_irq(&ha->lock); + } + finish_wait(&ha->eh_wait_q, &wait); + + spin_unlock_irq(&ha->lock); + + /* make sure SCSI EH is complete */ + if (scsi_host_in_recovery(ha->core.shost)) { + msleep(10); + goto retry; + } +} + +static int sas_queue_reset(struct domain_device *dev, int reset_type, + u64 lun, int wait) +{ + struct sas_ha_struct *ha = dev->port->ha; + int scheduled = 0, tries = 100; + + /* ata: promote lun reset to bus reset */ + if (dev_is_sata(dev)) { + sas_ata_schedule_reset(dev); + if (wait) + sas_ata_wait_eh(dev); + return SUCCESS; + } + + while (!scheduled && tries--) { + spin_lock_irq(&ha->lock); + if (!test_bit(SAS_DEV_EH_PENDING, &dev->state) && + !test_bit(reset_type, &dev->state)) { + scheduled = 1; + ha->eh_active++; + list_add_tail(&dev->ssp_dev.eh_list_node, &ha->eh_dev_q); + set_bit(SAS_DEV_EH_PENDING, &dev->state); + set_bit(reset_type, &dev->state); + int_to_scsilun(lun, &dev->ssp_dev.reset_lun); + scsi_schedule_eh(ha->core.shost); + } + spin_unlock_irq(&ha->lock); + + if (wait) + sas_wait_eh(dev); + + if (scheduled) + return SUCCESS; + } + + pr_warn("%s reset of %s failed\n", + reset_type == SAS_DEV_LU_RESET ? "LUN" : "Bus", + dev_name(&dev->rphy->dev)); + + return FAILED; +} + +int sas_eh_abort_handler(struct scsi_cmnd *cmd) +{ + int res = TMF_RESP_FUNC_FAILED; + struct sas_task *task = TO_SAS_TASK(cmd); + struct Scsi_Host *host = cmd->device->host; + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_internal *i = to_sas_internal(host->transportt); + unsigned long flags; + + if (!i->dft->lldd_abort_task) + return FAILED; + + spin_lock_irqsave(host->host_lock, flags); + /* We cannot do async aborts for SATA devices */ + if (dev_is_sata(dev) && !host->host_eh_scheduled) { + spin_unlock_irqrestore(host->host_lock, flags); + return FAILED; + } + spin_unlock_irqrestore(host->host_lock, flags); + + if (task) + res = i->dft->lldd_abort_task(task); + else + pr_notice("no task to abort\n"); + if (res == TMF_RESP_FUNC_SUCC || res == TMF_RESP_FUNC_COMPLETE) + return SUCCESS; + + return FAILED; +} +EXPORT_SYMBOL_GPL(sas_eh_abort_handler); + +/* Attempt to send a LUN reset message to a device */ +int sas_eh_device_reset_handler(struct scsi_cmnd *cmd) +{ + int res; + struct scsi_lun lun; + struct Scsi_Host *host = cmd->device->host; + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_internal *i = to_sas_internal(host->transportt); + + if (current != host->ehandler) + return sas_queue_reset(dev, SAS_DEV_LU_RESET, cmd->device->lun, 0); + + int_to_scsilun(cmd->device->lun, &lun); + + if (!i->dft->lldd_lu_reset) + return FAILED; + + res = i->dft->lldd_lu_reset(dev, lun.scsi_lun); + if (res == TMF_RESP_FUNC_SUCC || res == TMF_RESP_FUNC_COMPLETE) + return SUCCESS; + + return FAILED; +} + +int sas_eh_target_reset_handler(struct scsi_cmnd *cmd) +{ + int res; + struct Scsi_Host *host = cmd->device->host; + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_internal *i = to_sas_internal(host->transportt); + + if (current != host->ehandler) + return sas_queue_reset(dev, SAS_DEV_RESET, 0, 0); + + if (!i->dft->lldd_I_T_nexus_reset) + return FAILED; + + res = i->dft->lldd_I_T_nexus_reset(dev); + if (res == TMF_RESP_FUNC_SUCC || res == TMF_RESP_FUNC_COMPLETE || + res == -ENODEV) + return SUCCESS; + + return FAILED; +} + +/* Try to reset a device */ +static int try_to_reset_cmd_device(struct scsi_cmnd *cmd) +{ + int res; + struct Scsi_Host *shost = cmd->device->host; + + if (!shost->hostt->eh_device_reset_handler) + goto try_target_reset; + + res = shost->hostt->eh_device_reset_handler(cmd); + if (res == SUCCESS) + return res; + +try_target_reset: + if (shost->hostt->eh_target_reset_handler) + return shost->hostt->eh_target_reset_handler(cmd); + + return FAILED; +} + +static void sas_eh_handle_sas_errors(struct Scsi_Host *shost, struct list_head *work_q) +{ + struct scsi_cmnd *cmd, *n; + enum task_disposition res = TASK_IS_DONE; + int tmf_resp, need_reset; + struct sas_internal *i = to_sas_internal(shost->transportt); + unsigned long flags; + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + LIST_HEAD(done); + + /* clean out any commands that won the completion vs eh race */ + list_for_each_entry_safe(cmd, n, work_q, eh_entry) { + struct domain_device *dev = cmd_to_domain_dev(cmd); + struct sas_task *task; + + spin_lock_irqsave(&dev->done_lock, flags); + /* by this point the lldd has either observed + * SAS_HA_FROZEN and is leaving the task alone, or has + * won the race with eh and decided to complete it + */ + task = TO_SAS_TASK(cmd); + spin_unlock_irqrestore(&dev->done_lock, flags); + + if (!task) + list_move_tail(&cmd->eh_entry, &done); + } + + Again: + list_for_each_entry_safe(cmd, n, work_q, eh_entry) { + struct sas_task *task = TO_SAS_TASK(cmd); + + list_del_init(&cmd->eh_entry); + + spin_lock_irqsave(&task->task_state_lock, flags); + need_reset = task->task_state_flags & SAS_TASK_NEED_DEV_RESET; + spin_unlock_irqrestore(&task->task_state_lock, flags); + + if (need_reset) { + pr_notice("%s: task 0x%p requests reset\n", + __func__, task); + goto reset; + } + + pr_debug("trying to find task 0x%p\n", task); + res = sas_scsi_find_task(task); + + switch (res) { + case TASK_IS_DONE: + pr_notice("%s: task 0x%p is done\n", __func__, + task); + sas_eh_finish_cmd(cmd); + continue; + case TASK_IS_ABORTED: + pr_notice("%s: task 0x%p is aborted\n", + __func__, task); + sas_eh_finish_cmd(cmd); + continue; + case TASK_IS_AT_LU: + pr_info("task 0x%p is at LU: lu recover\n", task); + reset: + tmf_resp = sas_recover_lu(task->dev, cmd); + if (tmf_resp == TMF_RESP_FUNC_COMPLETE) { + pr_notice("dev %016llx LU 0x%llx is recovered\n", + SAS_ADDR(task->dev), + cmd->device->lun); + sas_eh_finish_cmd(cmd); + sas_scsi_clear_queue_lu(work_q, cmd); + goto Again; + } + fallthrough; + case TASK_IS_NOT_AT_LU: + case TASK_ABORT_FAILED: + pr_notice("task 0x%p is not at LU: I_T recover\n", + task); + tmf_resp = sas_recover_I_T(task->dev); + if (tmf_resp == TMF_RESP_FUNC_COMPLETE || + tmf_resp == -ENODEV) { + struct domain_device *dev = task->dev; + pr_notice("I_T %016llx recovered\n", + SAS_ADDR(task->dev->sas_addr)); + sas_eh_finish_cmd(cmd); + sas_scsi_clear_queue_I_T(work_q, dev); + goto Again; + } + /* Hammer time :-) */ + try_to_reset_cmd_device(cmd); + if (i->dft->lldd_clear_nexus_port) { + struct asd_sas_port *port = task->dev->port; + pr_debug("clearing nexus for port:%d\n", + port->id); + res = i->dft->lldd_clear_nexus_port(port); + if (res == TMF_RESP_FUNC_COMPLETE) { + pr_notice("clear nexus port:%d succeeded\n", + port->id); + sas_eh_finish_cmd(cmd); + sas_scsi_clear_queue_port(work_q, + port); + goto Again; + } + } + if (i->dft->lldd_clear_nexus_ha) { + pr_debug("clear nexus ha\n"); + res = i->dft->lldd_clear_nexus_ha(ha); + if (res == TMF_RESP_FUNC_COMPLETE) { + pr_notice("clear nexus ha succeeded\n"); + sas_eh_finish_cmd(cmd); + goto clear_q; + } + } + /* If we are here -- this means that no amount + * of effort could recover from errors. Quite + * possibly the HA just disappeared. + */ + pr_err("error from device %016llx, LUN 0x%llx couldn't be recovered in any way\n", + SAS_ADDR(task->dev->sas_addr), + cmd->device->lun); + + sas_eh_finish_cmd(cmd); + goto clear_q; + } + } + out: + list_splice_tail(&done, work_q); + list_splice_tail_init(&ha->eh_ata_q, work_q); + return; + + clear_q: + pr_debug("--- Exit %s -- clear_q\n", __func__); + list_for_each_entry_safe(cmd, n, work_q, eh_entry) + sas_eh_finish_cmd(cmd); + goto out; +} + +static void sas_eh_handle_resets(struct Scsi_Host *shost) +{ + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + struct sas_internal *i = to_sas_internal(shost->transportt); + + /* handle directed resets to sas devices */ + spin_lock_irq(&ha->lock); + while (!list_empty(&ha->eh_dev_q)) { + struct domain_device *dev; + struct ssp_device *ssp; + + ssp = list_entry(ha->eh_dev_q.next, typeof(*ssp), eh_list_node); + list_del_init(&ssp->eh_list_node); + dev = container_of(ssp, typeof(*dev), ssp_dev); + kref_get(&dev->kref); + WARN_ONCE(dev_is_sata(dev), "ssp reset to ata device?\n"); + + spin_unlock_irq(&ha->lock); + + if (test_and_clear_bit(SAS_DEV_LU_RESET, &dev->state)) + i->dft->lldd_lu_reset(dev, ssp->reset_lun.scsi_lun); + + if (test_and_clear_bit(SAS_DEV_RESET, &dev->state)) + i->dft->lldd_I_T_nexus_reset(dev); + + sas_put_device(dev); + spin_lock_irq(&ha->lock); + clear_bit(SAS_DEV_EH_PENDING, &dev->state); + ha->eh_active--; + } + spin_unlock_irq(&ha->lock); +} + + +void sas_scsi_recover_host(struct Scsi_Host *shost) +{ + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + LIST_HEAD(eh_work_q); + int tries = 0; + bool retry; + +retry: + tries++; + retry = true; + spin_lock_irq(shost->host_lock); + list_splice_init(&shost->eh_cmd_q, &eh_work_q); + spin_unlock_irq(shost->host_lock); + + pr_notice("Enter %s busy: %d failed: %d\n", + __func__, scsi_host_busy(shost), shost->host_failed); + /* + * Deal with commands that still have SAS tasks (i.e. they didn't + * complete via the normal sas_task completion mechanism), + * SAS_HA_FROZEN gives eh dominion over all sas_task completion. + */ + set_bit(SAS_HA_FROZEN, &ha->state); + sas_eh_handle_sas_errors(shost, &eh_work_q); + clear_bit(SAS_HA_FROZEN, &ha->state); + if (list_empty(&eh_work_q)) + goto out; + + /* + * Now deal with SCSI commands that completed ok but have a an error + * code (and hopefully sense data) attached. This is roughly what + * scsi_unjam_host does, but we skip scsi_eh_abort_cmds because any + * command we see here has no sas_task and is thus unknown to the HA. + */ + sas_ata_eh(shost, &eh_work_q, &ha->eh_done_q); + if (!scsi_eh_get_sense(&eh_work_q, &ha->eh_done_q)) + scsi_eh_ready_devs(shost, &eh_work_q, &ha->eh_done_q); + +out: + sas_eh_handle_resets(shost); + + /* now link into libata eh --- if we have any ata devices */ + sas_ata_strategy_handler(shost); + + scsi_eh_flush_done_q(&ha->eh_done_q); + + /* check if any new eh work was scheduled during the last run */ + spin_lock_irq(&ha->lock); + if (ha->eh_active == 0) { + shost->host_eh_scheduled = 0; + retry = false; + } + spin_unlock_irq(&ha->lock); + + if (retry) + goto retry; + + pr_notice("--- Exit %s: busy: %d failed: %d tries: %d\n", + __func__, scsi_host_busy(shost), + shost->host_failed, tries); +} + +int sas_ioctl(struct scsi_device *sdev, unsigned int cmd, void __user *arg) +{ + struct domain_device *dev = sdev_to_domain_dev(sdev); + + if (dev_is_sata(dev)) + return ata_sas_scsi_ioctl(dev->sata_dev.ap, sdev, cmd, arg); + + return -EINVAL; +} + +struct domain_device *sas_find_dev_by_rphy(struct sas_rphy *rphy) +{ + struct Scsi_Host *shost = dev_to_shost(rphy->dev.parent); + struct sas_ha_struct *ha = SHOST_TO_SAS_HA(shost); + struct domain_device *found_dev = NULL; + int i; + unsigned long flags; + + spin_lock_irqsave(&ha->phy_port_lock, flags); + for (i = 0; i < ha->num_phys; i++) { + struct asd_sas_port *port = ha->sas_port[i]; + struct domain_device *dev; + + spin_lock(&port->dev_list_lock); + list_for_each_entry(dev, &port->dev_list, dev_list_node) { + if (rphy == dev->rphy) { + found_dev = dev; + spin_unlock(&port->dev_list_lock); + goto found; + } + } + spin_unlock(&port->dev_list_lock); + } + found: + spin_unlock_irqrestore(&ha->phy_port_lock, flags); + + return found_dev; +} + +int sas_target_alloc(struct scsi_target *starget) +{ + struct sas_rphy *rphy = dev_to_rphy(starget->dev.parent); + struct domain_device *found_dev = sas_find_dev_by_rphy(rphy); + + if (!found_dev) + return -ENODEV; + + kref_get(&found_dev->kref); + starget->hostdata = found_dev; + return 0; +} + +#define SAS_DEF_QD 256 + +int sas_slave_configure(struct scsi_device *scsi_dev) +{ + struct domain_device *dev = sdev_to_domain_dev(scsi_dev); + + BUG_ON(dev->rphy->identify.device_type != SAS_END_DEVICE); + + if (dev_is_sata(dev)) { + ata_sas_slave_configure(scsi_dev, dev->sata_dev.ap); + return 0; + } + + sas_read_port_mode_page(scsi_dev); + + if (scsi_dev->tagged_supported) { + scsi_change_queue_depth(scsi_dev, SAS_DEF_QD); + } else { + pr_notice("device %016llx, LUN 0x%llx doesn't support TCQ\n", + SAS_ADDR(dev->sas_addr), scsi_dev->lun); + scsi_change_queue_depth(scsi_dev, 1); + } + + scsi_dev->allow_restart = 1; + + return 0; +} + +int sas_change_queue_depth(struct scsi_device *sdev, int depth) +{ + struct domain_device *dev = sdev_to_domain_dev(sdev); + + if (dev_is_sata(dev)) + return __ata_change_queue_depth(dev->sata_dev.ap, sdev, depth); + + if (!sdev->tagged_supported) + depth = 1; + return scsi_change_queue_depth(sdev, depth); +} + +int sas_bios_param(struct scsi_device *scsi_dev, + struct block_device *bdev, + sector_t capacity, int *hsc) +{ + hsc[0] = 255; + hsc[1] = 63; + sector_div(capacity, 255*63); + hsc[2] = capacity; + + return 0; +} + +/* + * Tell an upper layer that it needs to initiate an abort for a given task. + * This should only ever be called by an LLDD. + */ +void sas_task_abort(struct sas_task *task) +{ + struct scsi_cmnd *sc = task->uldd_task; + + /* Escape for libsas internal commands */ + if (!sc) { + struct sas_task_slow *slow = task->slow_task; + + if (!slow) + return; + if (!del_timer(&slow->timer)) + return; + slow->timer.function(&slow->timer); + return; + } + + if (dev_is_sata(task->dev)) + sas_ata_task_abort(task); + else + blk_abort_request(sc->request); +} + +int sas_slave_alloc(struct scsi_device *sdev) +{ + if (dev_is_sata(sdev_to_domain_dev(sdev)) && sdev->lun) + return -ENXIO; + + return 0; +} + +void sas_target_destroy(struct scsi_target *starget) +{ + struct domain_device *found_dev = starget->hostdata; + + if (!found_dev) + return; + + starget->hostdata = NULL; + sas_put_device(found_dev); +} + +#define SAS_STRING_ADDR_SIZE 16 + +int sas_request_addr(struct Scsi_Host *shost, u8 *addr) +{ + int res; + const struct firmware *fw; + + res = request_firmware(&fw, "sas_addr", &shost->shost_gendev); + if (res) + return res; + + if (fw->size < SAS_STRING_ADDR_SIZE) { + res = -ENODEV; + goto out; + } + + res = hex2bin(addr, fw->data, strnlen(fw->data, SAS_ADDR_SIZE * 2) / 2); + if (res) + goto out; + +out: + release_firmware(fw); + return res; +} +EXPORT_SYMBOL_GPL(sas_request_addr); + +EXPORT_SYMBOL_GPL(sas_queuecommand); +EXPORT_SYMBOL_GPL(sas_target_alloc); +EXPORT_SYMBOL_GPL(sas_slave_configure); +EXPORT_SYMBOL_GPL(sas_change_queue_depth); +EXPORT_SYMBOL_GPL(sas_bios_param); +EXPORT_SYMBOL_GPL(sas_task_abort); +EXPORT_SYMBOL_GPL(sas_phy_reset); +EXPORT_SYMBOL_GPL(sas_eh_device_reset_handler); +EXPORT_SYMBOL_GPL(sas_eh_target_reset_handler); +EXPORT_SYMBOL_GPL(sas_slave_alloc); +EXPORT_SYMBOL_GPL(sas_target_destroy); +EXPORT_SYMBOL_GPL(sas_ioctl); diff --git a/drivers/scsi/libsas/sas_task.c b/drivers/scsi/libsas/sas_task.c new file mode 100644 index 000000000..2966ead1d --- /dev/null +++ b/drivers/scsi/libsas/sas_task.c @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include "sas_internal.h" + +#include <linux/kernel.h> +#include <linux/export.h> +#include <scsi/sas.h> +#include <scsi/libsas.h> + +/* fill task_status_struct based on SSP response frame */ +void sas_ssp_task_response(struct device *dev, struct sas_task *task, + struct ssp_response_iu *iu) +{ + struct task_status_struct *tstat = &task->task_status; + + tstat->resp = SAS_TASK_COMPLETE; + + if (iu->datapres == 0) + tstat->stat = iu->status; + else if (iu->datapres == 1) + tstat->stat = iu->resp_data[3]; + else if (iu->datapres == 2) { + tstat->stat = SAS_SAM_STAT_CHECK_CONDITION; + tstat->buf_valid_size = + min_t(int, SAS_STATUS_BUF_SIZE, + be32_to_cpu(iu->sense_data_len)); + memcpy(tstat->buf, iu->sense_data, tstat->buf_valid_size); + + if (iu->status != SAM_STAT_CHECK_CONDITION) + dev_warn(dev, "dev %016llx sent sense data, but stat(0x%x) is not CHECK CONDITION\n", + SAS_ADDR(task->dev->sas_addr), iu->status); + } + else + /* when datapres contains corrupt/unknown value... */ + tstat->stat = SAS_SAM_STAT_CHECK_CONDITION; +} +EXPORT_SYMBOL_GPL(sas_ssp_task_response); + |