nvme-fc: Add Disconnect Association Rcv support
authorJames Smart <jsmart2021@gmail.com>
Tue, 31 Mar 2020 16:49:54 +0000 (09:49 -0700)
committerJens Axboe <axboe@kernel.dk>
Sat, 9 May 2020 22:18:33 +0000 (16:18 -0600)
The nvme-fc host transport did not support the reception of a
FC-NVME LS. Reception is necessary to implement full compliance
with FC-NVME-2.

Populate the LS receive handler, and specifically the handling
of a Disconnect Association LS. The response to the LS, if it
matched a controller, must be sent after the aborts for any
I/O on any connection have been sent.

Signed-off-by: James Smart <jsmart2021@gmail.com>
Reviewed-by: Hannes Reinecke <hare@suse.de>
Signed-off-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
drivers/nvme/host/fc.c

index c069ab0..1921d21 100644 (file)
@@ -62,6 +62,17 @@ struct nvmefc_ls_req_op {
        bool                    req_queued;
 };
 
+struct nvmefc_ls_rcv_op {
+       struct nvme_fc_rport            *rport;
+       struct nvmefc_ls_rsp            *lsrsp;
+       union nvmefc_ls_requests        *rqstbuf;
+       union nvmefc_ls_responses       *rspbuf;
+       u16                             rqstdatalen;
+       bool                            handled;
+       dma_addr_t                      rspdma;
+       struct list_head                lsrcv_list;     /* rport->ls_rcv_list */
+} __aligned(sizeof(u64));      /* alignment for other things alloc'd with */
+
 enum nvme_fcpop_state {
        FCPOP_STATE_UNINIT      = 0,
        FCPOP_STATE_IDLE        = 1,
@@ -118,6 +129,7 @@ struct nvme_fc_rport {
        struct list_head                endp_list; /* for lport->endp_list */
        struct list_head                ctrl_list;
        struct list_head                ls_req_list;
+       struct list_head                ls_rcv_list;
        struct list_head                disc_list;
        struct device                   *dev;   /* physical device for dma */
        struct nvme_fc_lport            *lport;
@@ -125,6 +137,7 @@ struct nvme_fc_rport {
        struct kref                     ref;
        atomic_t                        act_ctrl_cnt;
        unsigned long                   dev_loss_end;
+       struct work_struct              lsrcv_work;
 } __aligned(sizeof(u64));      /* alignment for other things alloc'd with */
 
 /* fc_ctrl flags values - specified as bit positions */
@@ -142,6 +155,7 @@ struct nvme_fc_ctrl {
        bool                    ioq_live;
        atomic_t                err_work_active;
        u64                     association_id;
+       struct nvmefc_ls_rcv_op *rcv_disconn;
 
        struct list_head        ctrl_list;      /* rport->ctrl_list */
 
@@ -219,6 +233,9 @@ static struct device *fc_udev_device;
 static void __nvme_fc_delete_hw_queue(struct nvme_fc_ctrl *,
                        struct nvme_fc_queue *, unsigned int);
 
+static void nvme_fc_handle_ls_rqst_work(struct work_struct *work);
+
+
 static void
 nvme_fc_free_lport(struct kref *ref)
 {
@@ -704,6 +721,7 @@ nvme_fc_register_remoteport(struct nvme_fc_local_port *localport,
        atomic_set(&newrec->act_ctrl_cnt, 0);
        spin_lock_init(&newrec->lock);
        newrec->remoteport.localport = &lport->localport;
+       INIT_LIST_HEAD(&newrec->ls_rcv_list);
        newrec->dev = lport->dev;
        newrec->lport = lport;
        if (lport->ops->remote_priv_sz)
@@ -717,6 +735,7 @@ nvme_fc_register_remoteport(struct nvme_fc_local_port *localport,
        newrec->remoteport.port_state = FC_OBJSTATE_ONLINE;
        newrec->remoteport.port_num = idx;
        __nvme_fc_set_dev_loss_tmo(newrec, pinfo);
+       INIT_WORK(&newrec->lsrcv_work, nvme_fc_handle_ls_rqst_work);
 
        spin_lock_irqsave(&nvme_fc_lock, flags);
        list_add_tail(&newrec->endp_list, &lport->endp_list);
@@ -1006,6 +1025,7 @@ fc_dma_unmap_sg(struct device *dev, struct scatterlist *sg, int nents,
 static void nvme_fc_ctrl_put(struct nvme_fc_ctrl *);
 static int nvme_fc_ctrl_get(struct nvme_fc_ctrl *);
 
+static void nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl, char *errmsg);
 
 static void
 __nvme_fc_finish_ls_req(struct nvmefc_ls_req_op *lsop)
@@ -1154,6 +1174,7 @@ nvme_fc_connect_admin_queue(struct nvme_fc_ctrl *ctrl,
        struct nvmefc_ls_req *lsreq;
        struct fcnvme_ls_cr_assoc_rqst *assoc_rqst;
        struct fcnvme_ls_cr_assoc_acc *assoc_acc;
+       unsigned long flags;
        int ret, fcret = 0;
 
        lsop = kzalloc((sizeof(*lsop) +
@@ -1243,11 +1264,13 @@ nvme_fc_connect_admin_queue(struct nvme_fc_ctrl *ctrl,
                        "q %d Create Association LS failed: %s\n",
                        queue->qnum, validation_errors[fcret]);
        } else {
+               spin_lock_irqsave(&ctrl->lock, flags);
                ctrl->association_id =
                        be64_to_cpu(assoc_acc->associd.association_id);
                queue->connection_id =
                        be64_to_cpu(assoc_acc->connectid.connection_id);
                set_bit(NVME_FC_Q_CONNECTED, &queue->flags);
+               spin_unlock_irqrestore(&ctrl->lock, flags);
        }
 
 out_free_buffer:
@@ -1428,6 +1451,247 @@ nvme_fc_xmt_disconnect_assoc(struct nvme_fc_ctrl *ctrl)
                kfree(lsop);
 }
 
+static void
+nvme_fc_xmt_ls_rsp_done(struct nvmefc_ls_rsp *lsrsp)
+{
+       struct nvmefc_ls_rcv_op *lsop = lsrsp->nvme_fc_private;
+       struct nvme_fc_rport *rport = lsop->rport;
+       struct nvme_fc_lport *lport = rport->lport;
+       unsigned long flags;
+
+       spin_lock_irqsave(&rport->lock, flags);
+       list_del(&lsop->lsrcv_list);
+       spin_unlock_irqrestore(&rport->lock, flags);
+
+       fc_dma_sync_single_for_cpu(lport->dev, lsop->rspdma,
+                               sizeof(*lsop->rspbuf), DMA_TO_DEVICE);
+       fc_dma_unmap_single(lport->dev, lsop->rspdma,
+                       sizeof(*lsop->rspbuf), DMA_TO_DEVICE);
+
+       kfree(lsop);
+
+       nvme_fc_rport_put(rport);
+}
+
+static void
+nvme_fc_xmt_ls_rsp(struct nvmefc_ls_rcv_op *lsop)
+{
+       struct nvme_fc_rport *rport = lsop->rport;
+       struct nvme_fc_lport *lport = rport->lport;
+       struct fcnvme_ls_rqst_w0 *w0 = &lsop->rqstbuf->w0;
+       int ret;
+
+       fc_dma_sync_single_for_device(lport->dev, lsop->rspdma,
+                                 sizeof(*lsop->rspbuf), DMA_TO_DEVICE);
+
+       ret = lport->ops->xmt_ls_rsp(&lport->localport, &rport->remoteport,
+                                    lsop->lsrsp);
+       if (ret) {
+               dev_warn(lport->dev,
+                       "LLDD rejected LS RSP xmt: LS %d status %d\n",
+                       w0->ls_cmd, ret);
+               nvme_fc_xmt_ls_rsp_done(lsop->lsrsp);
+               return;
+       }
+}
+
+static struct nvme_fc_ctrl *
+nvme_fc_match_disconn_ls(struct nvme_fc_rport *rport,
+                     struct nvmefc_ls_rcv_op *lsop)
+{
+       struct fcnvme_ls_disconnect_assoc_rqst *rqst =
+                                       &lsop->rqstbuf->rq_dis_assoc;
+       struct nvme_fc_ctrl *ctrl, *ret = NULL;
+       struct nvmefc_ls_rcv_op *oldls = NULL;
+       u64 association_id = be64_to_cpu(rqst->associd.association_id);
+       unsigned long flags;
+
+       spin_lock_irqsave(&rport->lock, flags);
+
+       list_for_each_entry(ctrl, &rport->ctrl_list, ctrl_list) {
+               if (!nvme_fc_ctrl_get(ctrl))
+                       continue;
+               spin_lock(&ctrl->lock);
+               if (association_id == ctrl->association_id) {
+                       oldls = ctrl->rcv_disconn;
+                       ctrl->rcv_disconn = lsop;
+                       ret = ctrl;
+               }
+               spin_unlock(&ctrl->lock);
+               if (ret)
+                       /* leave the ctrl get reference */
+                       break;
+               nvme_fc_ctrl_put(ctrl);
+       }
+
+       spin_unlock_irqrestore(&rport->lock, flags);
+
+       /* transmit a response for anything that was pending */
+       if (oldls) {
+               dev_info(rport->lport->dev,
+                       "NVME-FC{%d}: Multiple Disconnect Association "
+                       "LS's received\n", ctrl->cnum);
+               /* overwrite good response with bogus failure */
+               oldls->lsrsp->rsplen = nvme_fc_format_rjt(oldls->rspbuf,
+                                               sizeof(*oldls->rspbuf),
+                                               rqst->w0.ls_cmd,
+                                               FCNVME_RJT_RC_UNAB,
+                                               FCNVME_RJT_EXP_NONE, 0);
+               nvme_fc_xmt_ls_rsp(oldls);
+       }
+
+       return ret;
+}
+
+/*
+ * returns true to mean LS handled and ls_rsp can be sent
+ * returns false to defer ls_rsp xmt (will be done as part of
+ *     association termination)
+ */
+static bool
+nvme_fc_ls_disconnect_assoc(struct nvmefc_ls_rcv_op *lsop)
+{
+       struct nvme_fc_rport *rport = lsop->rport;
+       struct fcnvme_ls_disconnect_assoc_rqst *rqst =
+                                       &lsop->rqstbuf->rq_dis_assoc;
+       struct fcnvme_ls_disconnect_assoc_acc *acc =
+                                       &lsop->rspbuf->rsp_dis_assoc;
+       struct nvme_fc_ctrl *ctrl = NULL;
+       int ret = 0;
+
+       memset(acc, 0, sizeof(*acc));
+
+       ret = nvmefc_vldt_lsreq_discon_assoc(lsop->rqstdatalen, rqst);
+       if (!ret) {
+               /* match an active association */
+               ctrl = nvme_fc_match_disconn_ls(rport, lsop);
+               if (!ctrl)
+                       ret = VERR_NO_ASSOC;
+       }
+
+       if (ret) {
+               dev_info(rport->lport->dev,
+                       "Disconnect LS failed: %s\n",
+                       validation_errors[ret]);
+               lsop->lsrsp->rsplen = nvme_fc_format_rjt(acc,
+                                       sizeof(*acc), rqst->w0.ls_cmd,
+                                       (ret == VERR_NO_ASSOC) ?
+                                               FCNVME_RJT_RC_INV_ASSOC :
+                                               FCNVME_RJT_RC_LOGIC,
+                                       FCNVME_RJT_EXP_NONE, 0);
+               return true;
+       }
+
+       /* format an ACCept response */
+
+       lsop->lsrsp->rsplen = sizeof(*acc);
+
+       nvme_fc_format_rsp_hdr(acc, FCNVME_LS_ACC,
+                       fcnvme_lsdesc_len(
+                               sizeof(struct fcnvme_ls_disconnect_assoc_acc)),
+                       FCNVME_LS_DISCONNECT_ASSOC);
+
+       /*
+        * the transmit of the response will occur after the exchanges
+        * for the association have been ABTS'd by
+        * nvme_fc_delete_association().
+        */
+
+       /* fail the association */
+       nvme_fc_error_recovery(ctrl, "Disconnect Association LS received");
+
+       /* release the reference taken by nvme_fc_match_disconn_ls() */
+       nvme_fc_ctrl_put(ctrl);
+
+       return false;
+}
+
+/*
+ * Actual Processing routine for received FC-NVME LS Requests from the LLD
+ * returns true if a response should be sent afterward, false if rsp will
+ * be sent asynchronously.
+ */
+static bool
+nvme_fc_handle_ls_rqst(struct nvmefc_ls_rcv_op *lsop)
+{
+       struct fcnvme_ls_rqst_w0 *w0 = &lsop->rqstbuf->w0;
+       bool ret = true;
+
+       lsop->lsrsp->nvme_fc_private = lsop;
+       lsop->lsrsp->rspbuf = lsop->rspbuf;
+       lsop->lsrsp->rspdma = lsop->rspdma;
+       lsop->lsrsp->done = nvme_fc_xmt_ls_rsp_done;
+       /* Be preventative. handlers will later set to valid length */
+       lsop->lsrsp->rsplen = 0;
+
+       /*
+        * handlers:
+        *   parse request input, execute the request, and format the
+        *   LS response
+        */
+       switch (w0->ls_cmd) {
+       case FCNVME_LS_DISCONNECT_ASSOC:
+               ret = nvme_fc_ls_disconnect_assoc(lsop);
+               break;
+       case FCNVME_LS_DISCONNECT_CONN:
+               lsop->lsrsp->rsplen = nvme_fc_format_rjt(lsop->rspbuf,
+                               sizeof(*lsop->rspbuf), w0->ls_cmd,
+                               FCNVME_RJT_RC_UNSUP, FCNVME_RJT_EXP_NONE, 0);
+               break;
+       case FCNVME_LS_CREATE_ASSOCIATION:
+       case FCNVME_LS_CREATE_CONNECTION:
+               lsop->lsrsp->rsplen = nvme_fc_format_rjt(lsop->rspbuf,
+                               sizeof(*lsop->rspbuf), w0->ls_cmd,
+                               FCNVME_RJT_RC_LOGIC, FCNVME_RJT_EXP_NONE, 0);
+               break;
+       default:
+               lsop->lsrsp->rsplen = nvme_fc_format_rjt(lsop->rspbuf,
+                               sizeof(*lsop->rspbuf), w0->ls_cmd,
+                               FCNVME_RJT_RC_INVAL, FCNVME_RJT_EXP_NONE, 0);
+               break;
+       }
+
+       return(ret);
+}
+
+static void
+nvme_fc_handle_ls_rqst_work(struct work_struct *work)
+{
+       struct nvme_fc_rport *rport =
+               container_of(work, struct nvme_fc_rport, lsrcv_work);
+       struct fcnvme_ls_rqst_w0 *w0;
+       struct nvmefc_ls_rcv_op *lsop;
+       unsigned long flags;
+       bool sendrsp;
+
+restart:
+       sendrsp = true;
+       spin_lock_irqsave(&rport->lock, flags);
+       list_for_each_entry(lsop, &rport->ls_rcv_list, lsrcv_list) {
+               if (lsop->handled)
+                       continue;
+
+               lsop->handled = true;
+               if (rport->remoteport.port_state == FC_OBJSTATE_ONLINE) {
+                       spin_unlock_irqrestore(&rport->lock, flags);
+                       sendrsp = nvme_fc_handle_ls_rqst(lsop);
+               } else {
+                       spin_unlock_irqrestore(&rport->lock, flags);
+                       w0 = &lsop->rqstbuf->w0;
+                       lsop->lsrsp->rsplen = nvme_fc_format_rjt(
+                                               lsop->rspbuf,
+                                               sizeof(*lsop->rspbuf),
+                                               w0->ls_cmd,
+                                               FCNVME_RJT_RC_UNAB,
+                                               FCNVME_RJT_EXP_NONE, 0);
+               }
+               if (sendrsp)
+                       nvme_fc_xmt_ls_rsp(lsop);
+               goto restart;
+       }
+       spin_unlock_irqrestore(&rport->lock, flags);
+}
+
 /**
  * nvme_fc_rcv_ls_req - transport entry point called by an LLDD
  *                       upon the reception of a NVME LS request.
@@ -1454,20 +1718,92 @@ nvme_fc_rcv_ls_req(struct nvme_fc_remote_port *portptr,
 {
        struct nvme_fc_rport *rport = remoteport_to_rport(portptr);
        struct nvme_fc_lport *lport = rport->lport;
+       struct fcnvme_ls_rqst_w0 *w0 = (struct fcnvme_ls_rqst_w0 *)lsreqbuf;
+       struct nvmefc_ls_rcv_op *lsop;
+       unsigned long flags;
+       int ret;
+
+       nvme_fc_rport_get(rport);
 
        /* validate there's a routine to transmit a response */
-       if (!lport->ops->xmt_ls_rsp)
-               return(-EINVAL);
+       if (!lport->ops->xmt_ls_rsp) {
+               dev_info(lport->dev,
+                       "RCV %s LS failed: no LLDD xmt_ls_rsp\n",
+                       (w0->ls_cmd <= NVME_FC_LAST_LS_CMD_VALUE) ?
+                               nvmefc_ls_names[w0->ls_cmd] : "");
+               ret = -EINVAL;
+               goto out_put;
+       }
+
+       if (lsreqbuf_len > sizeof(union nvmefc_ls_requests)) {
+               dev_info(lport->dev,
+                       "RCV %s LS failed: payload too large\n",
+                       (w0->ls_cmd <= NVME_FC_LAST_LS_CMD_VALUE) ?
+                               nvmefc_ls_names[w0->ls_cmd] : "");
+               ret = -E2BIG;
+               goto out_put;
+       }
+
+       lsop = kzalloc(sizeof(*lsop) +
+                       sizeof(union nvmefc_ls_requests) +
+                       sizeof(union nvmefc_ls_responses),
+                       GFP_KERNEL);
+       if (!lsop) {
+               dev_info(lport->dev,
+                       "RCV %s LS failed: No memory\n",
+                       (w0->ls_cmd <= NVME_FC_LAST_LS_CMD_VALUE) ?
+                               nvmefc_ls_names[w0->ls_cmd] : "");
+               ret = -ENOMEM;
+               goto out_put;
+       }
+       lsop->rqstbuf = (union nvmefc_ls_requests *)&lsop[1];
+       lsop->rspbuf = (union nvmefc_ls_responses *)&lsop->rqstbuf[1];
+
+       lsop->rspdma = fc_dma_map_single(lport->dev, lsop->rspbuf,
+                                       sizeof(*lsop->rspbuf),
+                                       DMA_TO_DEVICE);
+       if (fc_dma_mapping_error(lport->dev, lsop->rspdma)) {
+               dev_info(lport->dev,
+                       "RCV %s LS failed: DMA mapping failure\n",
+                       (w0->ls_cmd <= NVME_FC_LAST_LS_CMD_VALUE) ?
+                               nvmefc_ls_names[w0->ls_cmd] : "");
+               ret = -EFAULT;
+               goto out_free;
+       }
+
+       lsop->rport = rport;
+       lsop->lsrsp = lsrsp;
+
+       memcpy(lsop->rqstbuf, lsreqbuf, lsreqbuf_len);
+       lsop->rqstdatalen = lsreqbuf_len;
+
+       spin_lock_irqsave(&rport->lock, flags);
+       if (rport->remoteport.port_state != FC_OBJSTATE_ONLINE) {
+               spin_unlock_irqrestore(&rport->lock, flags);
+               ret = -ENOTCONN;
+               goto out_unmap;
+       }
+       list_add_tail(&lsop->lsrcv_list, &rport->ls_rcv_list);
+       spin_unlock_irqrestore(&rport->lock, flags);
+
+       schedule_work(&rport->lsrcv_work);
 
        return 0;
+
+out_unmap:
+       fc_dma_unmap_single(lport->dev, lsop->rspdma,
+                       sizeof(*lsop->rspbuf), DMA_TO_DEVICE);
+out_free:
+       kfree(lsop);
+out_put:
+       nvme_fc_rport_put(rport);
+       return ret;
 }
 EXPORT_SYMBOL_GPL(nvme_fc_rcv_ls_req);
 
 
 /* *********************** NVME Ctrl Routines **************************** */
 
-static void nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl, char *errmsg);
-
 static void
 __nvme_fc_exit_request(struct nvme_fc_ctrl *ctrl,
                struct nvme_fc_fcp_op *op)
@@ -2612,6 +2948,8 @@ static int
 nvme_fc_create_association(struct nvme_fc_ctrl *ctrl)
 {
        struct nvmf_ctrl_options *opts = ctrl->ctrl.opts;
+       struct nvmefc_ls_rcv_op *disls = NULL;
+       unsigned long flags;
        int ret;
        bool changed;
 
@@ -2729,7 +3067,13 @@ out_term_aen_ops:
 out_disconnect_admin_queue:
        /* send a Disconnect(association) LS to fc-nvme target */
        nvme_fc_xmt_disconnect_assoc(ctrl);
+       spin_lock_irqsave(&ctrl->lock, flags);
        ctrl->association_id = 0;
+       disls = ctrl->rcv_disconn;
+       ctrl->rcv_disconn = NULL;
+       spin_unlock_irqrestore(&ctrl->lock, flags);
+       if (disls)
+               nvme_fc_xmt_ls_rsp(disls);
 out_delete_hw_queue:
        __nvme_fc_delete_hw_queue(ctrl, &ctrl->queues[0], 0);
 out_free_queue:
@@ -2749,6 +3093,7 @@ out_free_queue:
 static void
 nvme_fc_delete_association(struct nvme_fc_ctrl *ctrl)
 {
+       struct nvmefc_ls_rcv_op *disls = NULL;
        unsigned long flags;
 
        if (!test_and_clear_bit(ASSOC_ACTIVE, &ctrl->flags))
@@ -2820,7 +3165,17 @@ nvme_fc_delete_association(struct nvme_fc_ctrl *ctrl)
        if (ctrl->association_id)
                nvme_fc_xmt_disconnect_assoc(ctrl);
 
+       spin_lock_irqsave(&ctrl->lock, flags);
        ctrl->association_id = 0;
+       disls = ctrl->rcv_disconn;
+       ctrl->rcv_disconn = NULL;
+       spin_unlock_irqrestore(&ctrl->lock, flags);
+       if (disls)
+               /*
+                * if a Disconnect Request was waiting for a response, send
+                * now that all ABTS's have been issued (and are complete).
+                */
+               nvme_fc_xmt_ls_rsp(disls);
 
        if (ctrl->ctrl.tagset) {
                nvme_fc_delete_hw_io_queues(ctrl);